Compare commits
16 Commits
v0.0.17
...
b9a73106ed
Author | SHA1 | Date | |
---|---|---|---|
b9a73106ed | |||
c674cca482 | |||
81d1228b92 | |||
6ae61d5b81 | |||
9cb872eec2 | |||
68e8c010b7 | |||
9671413906 | |||
4c8d24c319 | |||
e50144bd34 | |||
9f3171e3f1 | |||
cc92748747 | |||
0a0b0c1adb | |||
92a74026a6 | |||
3fa1c6c420 | |||
b04eccdbda | |||
9ce30dee70 |
@ -3,9 +3,9 @@
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
VERSION_CODE := 17
|
||||
VERSION_NUMBER := 0.0.17
|
||||
VERSION_NAME := Please enjoy responsibly.
|
||||
VERSION_CODE := 18
|
||||
VERSION_NUMBER := 0.0.18-wip
|
||||
VERSION_NAME := Celebrating totality for upwards of 3m1.4s.
|
||||
|
||||
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450200.zip
|
||||
LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🐌",
|
||||
"previous": "&Xs1X5TzLCk6KVr+5IDc80JAHYxJyoD10cXKBUYpFqWQ=.sha256"
|
||||
"previous": "&sR39JhvUCRlOv7hdEWV81RWWkyoPGLg6u4w+BfqUE9s=.sha256"
|
||||
}
|
||||
|
@ -1,19 +1,23 @@
|
||||
function textNode(text) {
|
||||
const node = new commonmark.Node("text", undefined);
|
||||
const node = new commonmark.Node('text', undefined);
|
||||
node.literal = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function linkNode(text, link) {
|
||||
const linkNode = new commonmark.Node("link", undefined);
|
||||
const linkNode = new commonmark.Node('link', undefined);
|
||||
if (link.startsWith('#')) {
|
||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||
} else {
|
||||
linkNode.destination = link;
|
||||
}
|
||||
linkNode.appendChild(textNode(text));
|
||||
return linkNode;
|
||||
}
|
||||
|
||||
function splitMatches(text, regexp) {
|
||||
// Regexp must be sticky.
|
||||
regexp = new RegExp(regexp, "gm");
|
||||
regexp = new RegExp(regexp, 'gm');
|
||||
|
||||
let i = 0;
|
||||
const result = [];
|
||||
@ -39,13 +43,13 @@ function splitMatches(text, regexp) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const regex = new RegExp("(?<!\\w)#[\\w-]+");
|
||||
const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
|
||||
|
||||
function split(textNodes) {
|
||||
const text = textNodes.map(n => n.literal).join("");
|
||||
const text = textNodes.map((n) => n.literal).join('');
|
||||
const parts = splitMatches(text, regex);
|
||||
|
||||
return parts.map(part => {
|
||||
return parts.map((part) => {
|
||||
if (part[1]) {
|
||||
return linkNode(part[0], part[0]);
|
||||
} else {
|
||||
@ -61,17 +65,17 @@ export function transform(parsed) {
|
||||
let nodes = [];
|
||||
while ((event = walker.next())) {
|
||||
const node = event.node;
|
||||
if (event.entering && node.type === "text") {
|
||||
if (event.entering && node.type === 'text') {
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
.forEach((newNode) => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
|
||||
nodes.forEach(n => n.unlink());
|
||||
nodes.forEach((n) => n.unlink());
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
@ -80,10 +84,10 @@ export function transform(parsed) {
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
.forEach((newNode) => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
nodes.forEach(n => n.unlink());
|
||||
nodes.forEach((n) => n.unlink());
|
||||
}
|
||||
|
||||
return parsed;
|
||||
|
@ -1,91 +0,0 @@
|
||||
function textNode(text) {
|
||||
const node = new commonmark.Node("text", undefined);
|
||||
node.literal = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function linkNode(text, url) {
|
||||
const urlNode = new commonmark.Node("link", undefined);
|
||||
urlNode.destination = url;
|
||||
urlNode.appendChild(textNode(text));
|
||||
|
||||
return urlNode;
|
||||
}
|
||||
|
||||
function splitMatches(text, regexp) {
|
||||
// Regexp must be sticky.
|
||||
regexp = new RegExp(regexp, "gm");
|
||||
|
||||
let i = 0;
|
||||
const result = [];
|
||||
|
||||
let match = regexp.exec(text);
|
||||
while (match) {
|
||||
const matchText = match[0];
|
||||
|
||||
if (match.index > i) {
|
||||
result.push([text.substring(i, match.index), false]);
|
||||
}
|
||||
|
||||
result.push([matchText, true]);
|
||||
i = match.index + matchText.length;
|
||||
|
||||
match = regexp.exec(text);
|
||||
}
|
||||
|
||||
if (i < text.length) {
|
||||
result.push([text.substring(i, text.length), false]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
||||
|
||||
function splitURLs(textNodes) {
|
||||
const text = textNodes.map(n => n.literal).join("");
|
||||
const parts = splitMatches(text, urlRegexp);
|
||||
|
||||
return parts.map(part => {
|
||||
if (part[1]) {
|
||||
return linkNode(part[0], part[0]);
|
||||
} else {
|
||||
return textNode(part[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function transform(parsed) {
|
||||
const walker = parsed.walker();
|
||||
let event;
|
||||
|
||||
let nodes = [];
|
||||
while ((event = walker.next())) {
|
||||
const node = event.node;
|
||||
if (event.entering && node.type === "text") {
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length > 0) {
|
||||
splitURLs(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
|
||||
nodes.forEach(n => n.unlink());
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
splitURLs(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
nodes.forEach(n => n.unlink());
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
let g_emojis;
|
||||
|
||||
function get_emojis() {
|
||||
@ -10,8 +12,21 @@ function get_emojis() {
|
||||
});
|
||||
}
|
||||
|
||||
export function picker(callback, anchor) {
|
||||
get_emojis().then(function (json) {
|
||||
async function get_recent(author) {
|
||||
let recent = await tfrpc.rpc.query(`
|
||||
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||
FROM messages
|
||||
WHERE author = ? AND
|
||||
content ->> '$.type' = 'vote'
|
||||
ORDER BY timestamp DESC LIMIT 10
|
||||
`, [author]);
|
||||
return recent.map(x => x.value);
|
||||
}
|
||||
|
||||
export async function picker(callback, anchor, author) {
|
||||
let json = await get_emojis();
|
||||
let recent = await get_recent(author);
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.id = 'emoji_picker';
|
||||
div.style.color = '#000';
|
||||
@ -66,6 +81,40 @@ export function picker(callback, anchor) {
|
||||
}
|
||||
let search = input.value.toLowerCase();
|
||||
let any_at_all = false;
|
||||
if (recent) {
|
||||
let emoji_to_name = {};
|
||||
for (let row of Object.values(json)) {
|
||||
for (let entry of Object.entries(row)) {
|
||||
emoji_to_name[entry[1]] = entry[0];
|
||||
}
|
||||
}
|
||||
let header = document.createElement('div');
|
||||
header.appendChild(document.createTextNode('Recent'));
|
||||
list.appendChild(header);
|
||||
let any = false;
|
||||
for (let entry of recent) {
|
||||
if (
|
||||
search &&
|
||||
search.length &&
|
||||
(emoji_to_name[entry] || '').toLowerCase().indexOf(search) == -1
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let emoji = document.createElement('span');
|
||||
const k_size = '1.25em';
|
||||
emoji.style.display = 'inline-block';
|
||||
emoji.style.overflow = 'hidden';
|
||||
emoji.style.cursor = 'pointer';
|
||||
emoji.onclick = chosen;
|
||||
emoji.title = emoji_to_name[entry] || entry;
|
||||
emoji.appendChild(document.createTextNode(entry));
|
||||
list.appendChild(emoji);
|
||||
any = true;
|
||||
}
|
||||
if (!any) {
|
||||
list.removeChild(header);
|
||||
}
|
||||
}
|
||||
for (let row of Object.entries(json)) {
|
||||
let header = document.createElement('div');
|
||||
header.appendChild(document.createTextNode(row[0]));
|
||||
@ -110,5 +159,4 @@ export function picker(callback, anchor) {
|
||||
console.log('adding click');
|
||||
document.body.addEventListener('mousedown', cleanup);
|
||||
window.addEventListener('keydown', key_down);
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html style="color: #fff">
|
||||
<html>
|
||||
<head>
|
||||
<title>Tilde Friends</title>
|
||||
<base target="_top" />
|
||||
@ -10,14 +10,13 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #223a5e">
|
||||
<tf-app class="w3-deep-purple" />
|
||||
<body style="margin: 0; padding: 0">
|
||||
<tf-app/>
|
||||
<script>
|
||||
window.litDisableBundleWarning = true;
|
||||
</script>
|
||||
<script src="filesaver.min.js"></script>
|
||||
<script src="commonmark.min.js"></script>
|
||||
<script src="commonmark-linkify.js" type="module"></script>
|
||||
<script src="commonmark-hashtag.js" type="module"></script>
|
||||
<script src="script.js" type="module"></script>
|
||||
</body>
|
||||
|
@ -205,7 +205,7 @@ class TfElement extends LitElement {
|
||||
@change=${this._handle_whoami_changed}
|
||||
></tf-id-picker>
|
||||
<button
|
||||
class="w3-button w3-dark-grey w3-border"
|
||||
class="w3-button w3-theme-d1 w3-border"
|
||||
style="flex: 0 0 auto"
|
||||
@click=${this.create_identity}
|
||||
id="create_identity"
|
||||
@ -352,15 +352,15 @@ class TfElement extends LitElement {
|
||||
};
|
||||
|
||||
let tabs = html`
|
||||
<div class="w3-bar w3-black">
|
||||
<div class="w3-bar w3-theme-l1">
|
||||
${Object.entries(k_tabs).map(
|
||||
([k, v]) => html`
|
||||
<button
|
||||
title=${v}
|
||||
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
|
||||
class="w3-bar-item w3-padding-large w3-hover-theme tab ${self.tab ==
|
||||
v
|
||||
? 'w3-red'
|
||||
: 'w3-black'}"
|
||||
? 'w3-theme-l2'
|
||||
: 'w3-theme-l1'}"
|
||||
@click=${() => self.set_tab(v)}
|
||||
>
|
||||
${k}
|
||||
@ -375,11 +375,15 @@ class TfElement extends LitElement {
|
||||
: html`<div>Select or create an identity.</div>`
|
||||
: this.render_tab();
|
||||
return html`
|
||||
<div style="width: 100vw; min-height: 100vh; height: 100%" class="w3-theme-dark">
|
||||
<div style="padding: 8px">
|
||||
${this.render_id_picker()} ${tabs}
|
||||
${this.tags.map(
|
||||
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||
)}
|
||||
${contents}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ class TfComposeElement extends LitElement {
|
||||
branch: {type: String},
|
||||
apps: {type: Object},
|
||||
drafts: {type: Object},
|
||||
author: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
|
||||
this.branch = undefined;
|
||||
this.apps = undefined;
|
||||
this.drafts = {};
|
||||
this.author = undefined;
|
||||
}
|
||||
|
||||
process_text(text) {
|
||||
@ -284,13 +286,25 @@ class TfComposeElement extends LitElement {
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
let values = Object.entries(this.users).map((x) => ({
|
||||
key: x[1].name ?? x[0],
|
||||
value: x[0],
|
||||
}));
|
||||
if (this.author) {
|
||||
values = [].concat(
|
||||
[
|
||||
{
|
||||
key: this.users[this.author]?.name,
|
||||
value: this.author,
|
||||
},
|
||||
],
|
||||
values
|
||||
);
|
||||
}
|
||||
let tribute = new Tribute({
|
||||
collection: [
|
||||
{
|
||||
values: Object.entries(this.users).map((x) => ({
|
||||
key: x[1].name,
|
||||
value: x[0],
|
||||
})),
|
||||
values: values,
|
||||
selectTemplate: function (item) {
|
||||
return `[@${item.original.key}](${item.original.value})`;
|
||||
},
|
||||
@ -342,7 +356,7 @@ class TfComposeElement extends LitElement {
|
||||
return html` <div style="display: flex; flex-direction: row">
|
||||
<div style="align-self: center; margin: 0.5em">
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
title="Remove ${mention.name} mention"
|
||||
@click=${() => self.remove_mention(mention.link)}
|
||||
>
|
||||
@ -396,16 +410,16 @@ class TfComposeElement extends LitElement {
|
||||
if (this.apps) {
|
||||
return html`
|
||||
<div class="w3-card-4 w3-margin w3-padding">
|
||||
<select id="select" class="w3-select w3-dark-grey">
|
||||
<select id="select" class="w3-select w3-theme-d1">
|
||||
${Object.keys(self.apps).map(
|
||||
(app) => html`<option value=${app}>${app}</option>`
|
||||
)}
|
||||
</select>
|
||||
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
|
||||
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
|
||||
Attach
|
||||
</button>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (this.apps = null)}
|
||||
>
|
||||
Cancel
|
||||
@ -421,12 +435,12 @@ class TfComposeElement extends LitElement {
|
||||
self.apps = await tfrpc.rpc.apps();
|
||||
}
|
||||
if (!this.apps) {
|
||||
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>
|
||||
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
|
||||
Attach App
|
||||
</button>`;
|
||||
} else {
|
||||
return html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (this.apps = null)}
|
||||
>
|
||||
Discard App
|
||||
@ -448,15 +462,15 @@ class TfComposeElement extends LitElement {
|
||||
return html`
|
||||
<div class="w3-container w3-padding">
|
||||
<p>
|
||||
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||
<label for="cw">CW</label>
|
||||
</p>
|
||||
<input type="text" class="w3-input w3-border w3-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||
<label for="cw">CW</label>
|
||||
`;
|
||||
}
|
||||
@ -486,14 +500,14 @@ class TfComposeElement extends LitElement {
|
||||
<div style="display: flex; flex-direction: row; width: 100%">
|
||||
<label for="encrypt_to">🔐 To:</label>
|
||||
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||
</div>
|
||||
<ul>
|
||||
${draft.encrypt_to.map(
|
||||
(x) => html`
|
||||
<li>
|
||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||
<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
@ -512,7 +526,7 @@ class TfComposeElement extends LitElement {
|
||||
let draft = self.get_draft();
|
||||
let content_warning =
|
||||
draft.content_warning !== undefined
|
||||
? html`<div class="w3-panel w3-round-xlarge w3-blue">
|
||||
? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
|
||||
<p id="content_warning_preview">${draft.content_warning}</p>
|
||||
</div>`
|
||||
: undefined;
|
||||
@ -520,14 +534,14 @@ class TfComposeElement extends LitElement {
|
||||
draft.encrypt_to !== undefined
|
||||
? undefined
|
||||
: html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.set_encrypt([])}
|
||||
>
|
||||
🔐
|
||||
</button>`;
|
||||
let result = html`
|
||||
<div
|
||||
class="w3-card-4 w3-blue-grey w3-padding"
|
||||
class="w3-card-4 w3-theme-d4 w3-padding"
|
||||
style="box-sizing: border-box"
|
||||
>
|
||||
${this.render_encrypt()}
|
||||
@ -535,15 +549,14 @@ class TfComposeElement extends LitElement {
|
||||
<div style="flex: 1 0 50%">
|
||||
<p>
|
||||
<textarea
|
||||
class="w3-input w3-dark-grey w3-border"
|
||||
class="w3-input w3-theme-d1 w3-border"
|
||||
style="resize: vertical"
|
||||
placeholder="Write a post here."
|
||||
id="edit"
|
||||
@input=${this.input}
|
||||
@change=${this.change}
|
||||
@paste=${this.paste}
|
||||
>
|
||||
${draft.text}</textarea
|
||||
>${draft.text}</textarea
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
@ -552,22 +565,20 @@ ${draft.text}</textarea
|
||||
<div id="preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
${Object.values(draft.mentions || {}).map((x) =>
|
||||
self.render_mention(x)
|
||||
)}
|
||||
${Object.values(draft.mentions || {}).map((x) => self.render_mention(x))}
|
||||
${this.render_attach_app()} ${this.render_content_warning()}
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
id="submit"
|
||||
@click=${this.submit}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.attach}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.attach}>
|
||||
Attach
|
||||
</button>
|
||||
${this.render_attach_app_button()} ${encrypt}
|
||||
<button class="w3-button w3-dark-grey" @click=${this.discard}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.discard}>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
|
@ -34,7 +34,7 @@ class TfIdentityPickerElement extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<select
|
||||
class="w3-select w3-dark-grey w3-padding w3-border"
|
||||
class="w3-select w3-theme-d1 w3-padding w3-border"
|
||||
@change=${this.changed}
|
||||
style="max-width: 100%; overflow: hidden"
|
||||
>
|
||||
|
@ -125,7 +125,7 @@ class TfMessageElement extends LitElement {
|
||||
}
|
||||
|
||||
react(event) {
|
||||
emojis.picker((x) => this.vote(x));
|
||||
emojis.picker((x) => this.vote(x), null, this.whoami);
|
||||
}
|
||||
|
||||
show_image(link) {
|
||||
@ -240,7 +240,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
let self = this;
|
||||
return html`
|
||||
<fieldset
|
||||
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
|
||||
style="backdrop-filter: brightness(1.2); padding: 0.5em; border: 1px solid black"
|
||||
>
|
||||
<legend>Mentions</legend>
|
||||
${mentions.map((x) => self.render_mention(x))}
|
||||
@ -282,14 +282,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
if (this.message.child_messages?.length) {
|
||||
if (!this.expanded[this.message.id]) {
|
||||
return html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self.set_expanded(true)}
|
||||
>
|
||||
+ ${this.total_child_messages(this.message) + ' More'}
|
||||
</button>`;
|
||||
} else {
|
||||
return html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self.set_expanded(false)}
|
||||
>
|
||||
Collapse</button
|
||||
@ -337,14 +337,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
case 'raw':
|
||||
if (content?.type == 'post' || content?.type == 'blog') {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'md')}
|
||||
>
|
||||
Markdown
|
||||
</button>`;
|
||||
} else {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'message')}
|
||||
>
|
||||
Message
|
||||
@ -353,7 +353,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
break;
|
||||
case 'md':
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'message')}
|
||||
>
|
||||
Message
|
||||
@ -361,7 +361,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
break;
|
||||
case 'decrypted':
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'raw')}
|
||||
>
|
||||
Raw
|
||||
@ -370,14 +370,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
default:
|
||||
if (this.message.decrypted) {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'decrypted')}
|
||||
>
|
||||
Decrypted
|
||||
</button>`;
|
||||
} else {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'raw')}
|
||||
>
|
||||
Raw
|
||||
@ -390,7 +390,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
return html`
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||
style="backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||
>
|
||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||
<span style="padding-right: 8px"
|
||||
@ -406,7 +406,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
if (this.message?.type === 'contact_group') {
|
||||
return html` <div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
style="border: 1px solid black; backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
>
|
||||
${this.message.messages.map(
|
||||
(x) =>
|
||||
@ -422,7 +422,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
} else if (this.message.placeholder) {
|
||||
return html` <div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
style="border: 1px solid black; backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
>
|
||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||
(placeholder)
|
||||
@ -498,11 +498,12 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
branch=${this.message.id}
|
||||
.drafts=${this.drafts}
|
||||
@tf-discard=${this.discard_reply}
|
||||
author=${this.message.author}
|
||||
></tf-compose>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${this.show_reply}
|
||||
>
|
||||
Reply
|
||||
@ -533,7 +534,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
}
|
||||
let content_warning = html`
|
||||
<div
|
||||
class="w3-panel w3-round-xlarge w3-blue"
|
||||
class="w3-panel w3-round-xlarge w3-theme-l4"
|
||||
style="cursor: pointer"
|
||||
@click=${(x) => this.toggle_expanded(':cw')}
|
||||
>
|
||||
@ -554,8 +555,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
? html`<span style="align-self: center">🔓</span>`
|
||||
: undefined;
|
||||
let style_background = this.message?.decrypted
|
||||
? 'rgba(255, 0, 0, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.1)';
|
||||
? 'background-color: rgba(255, 0, 0, 0.2)'
|
||||
: 'backdrop-filter: brightness(1.2)';
|
||||
return html`
|
||||
<style>
|
||||
code {
|
||||
@ -573,7 +574,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
</style>
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
||||
style="border: 1px solid black; ${style_background}; margin-top: 8px; padding: 16px"
|
||||
>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
@ -588,7 +589,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
${payload} ${this.render_votes()}
|
||||
<p>
|
||||
${reply}
|
||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</p>
|
||||
@ -600,8 +601,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
? html`<span style="align-self: center">🔓</span>`
|
||||
: undefined;
|
||||
let style_background = this.message?.decrypted
|
||||
? 'rgba(255, 0, 0, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.1)';
|
||||
? 'background: rgba(255, 0, 0, 0.2)'
|
||||
: 'backdrop-filter: brightness(1.2)';
|
||||
return html`
|
||||
<style>
|
||||
code {
|
||||
@ -619,7 +620,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
</style>
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
||||
style="border: 1px solid black; ${style_background}; margin-top: 8px; padding: 16px"
|
||||
>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
@ -633,7 +634,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
</div>
|
||||
${content.text} ${this.render_votes()}
|
||||
<p>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</p>
|
||||
@ -685,11 +686,12 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
branch=${this.message.id}
|
||||
.drafts=${this.drafts}
|
||||
@tf-discard=${this.discard_reply}
|
||||
author=${this.message.author}
|
||||
></tf-compose>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${this.show_reply}
|
||||
>
|
||||
Reply
|
||||
@ -712,7 +714,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
</style>
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"
|
||||
style="border: 1px solid black; backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px"
|
||||
>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
@ -728,7 +730,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
${this.render_mentions()}
|
||||
<div>
|
||||
${reply}
|
||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</div>
|
||||
|
@ -215,49 +215,49 @@ class TfProfileElement extends LitElement {
|
||||
let server_follow;
|
||||
if (this.server_follows_me === true) {
|
||||
server_follow = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.server_follow_me(false)}
|
||||
>
|
||||
Server, Stop Following Me
|
||||
</button>`;
|
||||
} else if (this.server_follows_me === false) {
|
||||
server_follow = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.server_follow_me(true)}
|
||||
>
|
||||
Server, Follow Me
|
||||
</button>`;
|
||||
}
|
||||
edit = html`
|
||||
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
|
||||
Save Profile
|
||||
</button>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||
Discard
|
||||
</button>
|
||||
${server_follow}
|
||||
`;
|
||||
} else {
|
||||
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
|
||||
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
|
||||
Edit Profile
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
if (this.id !== this.whoami && this.following !== undefined) {
|
||||
follow = this.following
|
||||
? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
|
||||
? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
|
||||
Unfollow
|
||||
</button>`
|
||||
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
|
||||
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
|
||||
Follow
|
||||
</button>`;
|
||||
}
|
||||
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||
block = this.blocking
|
||||
? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
|
||||
? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
|
||||
Unblock
|
||||
</button>`
|
||||
: html`<button class="w3-button w3-dark-grey" @click=${this.block}>
|
||||
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
|
||||
Block
|
||||
</button>`;
|
||||
}
|
||||
@ -267,16 +267,16 @@ class TfProfileElement extends LitElement {
|
||||
<div class="w3-container">
|
||||
<div>
|
||||
<label for="name">Name:</label>
|
||||
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||
</div>
|
||||
<div><label for="description">Description:</label></div>
|
||||
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||
<div>
|
||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||
</div>
|
||||
<div>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
@ -1,17 +1,6 @@
|
||||
import {css} from './lit-all.min.js';
|
||||
|
||||
const tf = css`
|
||||
a:link {
|
||||
color: #bbf;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ddf;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: min(640px, 100%);
|
||||
@ -1692,4 +1681,29 @@ const w3 = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export let styles = [tf, w3];
|
||||
const w3_2016_snorkel_blue = css`
|
||||
.w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important}
|
||||
.w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important}
|
||||
.w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important}
|
||||
.w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important}
|
||||
.w3-theme-l1 {color:#fff !important; background-color:#0479cc !important}
|
||||
.w3-theme-d1 {color:#fff !important; background-color:#024575 !important}
|
||||
.w3-theme-d2 {color:#fff !important; background-color:#023e68 !important}
|
||||
.w3-theme-d3 {color:#fff !important; background-color:#02365b !important}
|
||||
.w3-theme-d4 {color:#fff !important; background-color:#022e4e !important}
|
||||
.w3-theme-d5 {color:#fff !important; background-color:#012641 !important}
|
||||
|
||||
.w3-theme-light {color:#000 !important; background-color:#e9f5ff !important}
|
||||
.w3-theme-dark {color:#fff !important; background-color:#012641 !important}
|
||||
.w3-theme-action {color:#fff !important; background-color:#012641 !important}
|
||||
|
||||
.w3-theme {color:#fff !important; background-color:#034f84 !important}
|
||||
.w3-text-theme {color:#034f84 !important}
|
||||
.w3-border-theme {border-color:#034f84 !important}
|
||||
|
||||
.w3-hover-theme:hover {color:#fff !important; background-color:#034f84 !important}
|
||||
.w3-hover-text-theme:hover {color:#034f84 !important}
|
||||
.w3-hover-border-theme:hover {border-color:#034f84 !important}
|
||||
`;
|
||||
|
||||
export let styles = [tf, w3, w3_2016_snorkel_blue];
|
||||
|
@ -61,7 +61,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
return html`
|
||||
<li>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||
>
|
||||
Connect
|
||||
@ -75,7 +75,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
return html`
|
||||
<li>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => tfrpc.rpc.connect(connection)}
|
||||
>
|
||||
Connect
|
||||
@ -94,7 +94,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
render_connection(connection) {
|
||||
return html`
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||
>
|
||||
Close
|
||||
@ -117,9 +117,9 @@ class TfTabConnectionsElement extends LitElement {
|
||||
return html`
|
||||
<div class="w3-container">
|
||||
<h2>New Connection</h2>
|
||||
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
||||
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() =>
|
||||
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||
>
|
||||
@ -143,13 +143,13 @@ class TfTabConnectionsElement extends LitElement {
|
||||
(x) => html`
|
||||
<li>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self.forget_stored_connection(x)}
|
||||
>
|
||||
Forget
|
||||
</button>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => tfrpc.rpc.connect(x)}
|
||||
>
|
||||
Connect
|
||||
|
@ -187,7 +187,7 @@ class TfTabNewsFeedElement extends LitElement {
|
||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||
more = html`
|
||||
<p>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.load_more}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
|
||||
Load More
|
||||
</button>
|
||||
</p>
|
||||
|
@ -119,7 +119,7 @@ class TfTabNewsElement extends LitElement {
|
||||
return html`
|
||||
<p class="w3-bar">
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-dark-grey"
|
||||
class="w3-bar-item w3-button w3-theme-d1"
|
||||
@click=${this.show_more}
|
||||
>
|
||||
${this.new_messages_text()}
|
||||
|
@ -110,14 +110,14 @@ class TfTabQueryElement extends LitElement {
|
||||
<textarea
|
||||
id="search"
|
||||
rows="8"
|
||||
class="w3-input w3-dark-grey"
|
||||
class="w3-input w3-theme-d1"
|
||||
style="flex: 1; resize: vertical"
|
||||
@keydown=${this.search_keydown}
|
||||
>
|
||||
${this.query}</textarea
|
||||
>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${(event) =>
|
||||
self.search(self.renderRoot.getElementById('search').value)}
|
||||
>
|
||||
|
@ -78,8 +78,8 @@ class TfTabSearchElement extends LitElement {
|
||||
let self = this;
|
||||
return html`
|
||||
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||
<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||
</div>
|
||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
||||
`;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as linkify from './commonmark-linkify.js';
|
||||
import * as hashtagify from './commonmark-hashtag.js';
|
||||
|
||||
function image(node, entering) {
|
||||
@ -67,7 +66,6 @@ export function markdown(md) {
|
||||
writer.image = image;
|
||||
let parsed = reader.parse(md || '');
|
||||
parsed = hashtagify.transform(parsed);
|
||||
parsed = linkify.transform(parsed);
|
||||
let walker = parsed.walker();
|
||||
let event, node;
|
||||
while ((event = walker.next())) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📝",
|
||||
"previous": "&DnfuAUGzzalSh9NgZXnzDc9Ru5aM0omfRJ4h27jYw4k=.sha256"
|
||||
"previous": "&hTgvoLzZ+6dlqYL7zRDAa91TzptTquAY8MaZ7Gn36Cc=.sha256"
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ export async function collection(
|
||||
let rows = [];
|
||||
await ssb.sqlAsync(
|
||||
`
|
||||
SELECT messages.id, author, content, timestamp
|
||||
SELECT messages.id, author, json(content) AS content, timestamp
|
||||
FROM messages
|
||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||
WHERE
|
||||
|
13
core/app.js
13
core/app.js
@ -1,4 +1,3 @@
|
||||
import * as auth from './auth.js';
|
||||
import * as core from './core.js';
|
||||
|
||||
let g_next_id = 1;
|
||||
@ -87,8 +86,7 @@ App.prototype.send = function (message) {
|
||||
function socket(request, response, client) {
|
||||
let process;
|
||||
let options = {};
|
||||
let credentials = auth.query(request.headers);
|
||||
let refresh = auth.makeRefresh(credentials);
|
||||
let credentials = httpd.auth_query(request.headers);
|
||||
|
||||
response.onClose = async function () {
|
||||
if (process && process.task) {
|
||||
@ -241,14 +239,7 @@ function socket(request, response, client) {
|
||||
}
|
||||
};
|
||||
|
||||
response.upgrade(
|
||||
100,
|
||||
refresh
|
||||
? {
|
||||
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
||||
}
|
||||
: {}
|
||||
);
|
||||
response.upgrade(100, {});
|
||||
}
|
||||
|
||||
export {socket, App};
|
||||
|
420
core/auth.js
420
core/auth.js
@ -1,420 +0,0 @@
|
||||
import * as core from './core.js';
|
||||
import * as form from './form.js';
|
||||
|
||||
let gDatabase = new Database('auth');
|
||||
|
||||
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Makes a Base64 value URL safe
|
||||
* @param {string} value
|
||||
* @returns TODOC
|
||||
*/
|
||||
function b64url(value) {
|
||||
value = value.replaceAll('+', '-').replaceAll('/', '_');
|
||||
let equals = value.indexOf('=');
|
||||
|
||||
if (equals !== -1) {
|
||||
return value.substring(0, equals);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {string} value
|
||||
* @returns
|
||||
*/
|
||||
function unb64url(value) {
|
||||
value = value.replaceAll('-', '+').replaceAll('_', '/');
|
||||
let remainder = value.length % 4;
|
||||
|
||||
if (remainder == 3) {
|
||||
return value + '=';
|
||||
} else if (remainder == 2) {
|
||||
return value + '==';
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON Web Token
|
||||
* @param {object} payload Object: {"name": "username"}
|
||||
* @returns the JWT
|
||||
*/
|
||||
function makeJwt(payload) {
|
||||
const ids = ssb.getIdentities(':auth');
|
||||
let id;
|
||||
|
||||
if (ids?.length) {
|
||||
id = ids[0];
|
||||
} else {
|
||||
id = ssb.createIdentity(':auth');
|
||||
}
|
||||
|
||||
const final_payload = b64url(
|
||||
base64Encode(
|
||||
JSON.stringify(
|
||||
Object.assign({}, payload, {
|
||||
exp: new Date().valueOf() + kRefreshInterval,
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
const jwt = [
|
||||
b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))),
|
||||
final_payload,
|
||||
b64url(ssb.hmacsha256sign(final_payload, ':auth', id)),
|
||||
].join('.');
|
||||
return jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a JWT ?
|
||||
* @param {*} session TODOC
|
||||
* @returns
|
||||
*/
|
||||
function readSession(session) {
|
||||
let jwt_parts = session?.split('.');
|
||||
|
||||
if (jwt_parts?.length === 3) {
|
||||
let [header, payload, signature] = jwt_parts;
|
||||
header = JSON.parse(utf8Decode(base64Decode(unb64url(header))));
|
||||
|
||||
if (header.typ === 'JWT' && header.alg === 'HS256') {
|
||||
signature = unb64url(signature);
|
||||
let id = ssb.getIdentities(':auth');
|
||||
|
||||
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
||||
const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
||||
const now = new Date().valueOf();
|
||||
|
||||
if (now < result.exp) {
|
||||
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
|
||||
return result;
|
||||
} else {
|
||||
print(`JWT expired by ${(now - result.exp) / 1000} seconds.`);
|
||||
}
|
||||
} else {
|
||||
print('JWT verification failed.');
|
||||
}
|
||||
} else {
|
||||
print('Invalid JWT header.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the provided password matches the hash
|
||||
* @param {string} password
|
||||
* @param {string} hash bcrypt hash
|
||||
* @returns true if the password matches the hash
|
||||
*/
|
||||
function verifyPassword(password, hash) {
|
||||
return bCrypt.hashpw(password, hash) === hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes a password
|
||||
* @param {string} password
|
||||
* @returns {string} TODOC
|
||||
*/
|
||||
function hashPassword(password) {
|
||||
let salt = bCrypt.gensalt(12);
|
||||
return bCrypt.hashpw(password, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an administrator on the instance
|
||||
* @returns TODOC
|
||||
*/
|
||||
function noAdministrator() {
|
||||
return (
|
||||
!core.globalSettings ||
|
||||
!core.globalSettings.permissions ||
|
||||
!Object.keys(core.globalSettings.permissions).some(function (name) {
|
||||
return (
|
||||
core.globalSettings.permissions[name].indexOf('administration') != -1
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a user an administrator
|
||||
* @param {string} name the user's name
|
||||
*/
|
||||
function makeAdministrator(name) {
|
||||
if (!core.globalSettings.permissions) {
|
||||
core.globalSettings.permissions = {};
|
||||
}
|
||||
if (!core.globalSettings.permissions[name]) {
|
||||
core.globalSettings.permissions[name] = [];
|
||||
}
|
||||
if (core.globalSettings.permissions[name].indexOf('administration') == -1) {
|
||||
core.globalSettings.permissions[name].push('administration');
|
||||
}
|
||||
|
||||
core.setGlobalSettings(core.globalSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} headers most likely an object
|
||||
* @returns
|
||||
*/
|
||||
function getCookies(headers) {
|
||||
let cookies = {};
|
||||
|
||||
if (headers.cookie) {
|
||||
let parts = headers.cookie.split(/,|;/);
|
||||
for (let i in parts) {
|
||||
let equals = parts[i].indexOf('=');
|
||||
let name = parts[i].substring(0, equals).trim();
|
||||
let value = parts[i].substring(equals + 1).trim();
|
||||
cookies[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a username
|
||||
* @param {string} name
|
||||
* @returns false | boolean[] ?
|
||||
*/
|
||||
function isNameValid(name) {
|
||||
// TODO(tasiaiso): convert this into a regex
|
||||
let c = name.charAt(0);
|
||||
return (
|
||||
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
|
||||
name
|
||||
.split()
|
||||
.map(
|
||||
(x) =>
|
||||
x >= ('a' && x <= 'z') ||
|
||||
x >= ('A' && x <= 'Z') ||
|
||||
x >= ('0' && x <= '9')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request handler ?
|
||||
* @param {*} request TODOC
|
||||
* @param {*} response
|
||||
* @returns
|
||||
*/
|
||||
function handler(request, response) {
|
||||
// TODO(tasiaiso): split this function
|
||||
let session = getCookies(request.headers).session;
|
||||
if (request.uri == '/login') {
|
||||
let formData = form.decodeForm(request.query);
|
||||
if (query(request.headers)?.permissions?.authenticated) {
|
||||
if (formData.return) {
|
||||
response.writeHead(303, {Location: formData.return});
|
||||
} else {
|
||||
response.writeHead(303, {
|
||||
Location:
|
||||
(request.client.tls ? 'https://' : 'http://') +
|
||||
request.headers.host +
|
||||
'/',
|
||||
'Content-Length': '0',
|
||||
});
|
||||
}
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionIsNew = false;
|
||||
let loginError;
|
||||
|
||||
if (request.method == 'POST' || formData.submit) {
|
||||
sessionIsNew = true;
|
||||
formData = form.decodeForm(utf8Decode(request.body), formData);
|
||||
if (formData.submit == 'Login') {
|
||||
let account = gDatabase.get('user:' + formData.name);
|
||||
account = account ? JSON.parse(account) : account;
|
||||
if (formData.register == '1') {
|
||||
if (
|
||||
!account &&
|
||||
isNameValid(formData.name) &&
|
||||
formData.password == formData.confirm
|
||||
) {
|
||||
let users = new Set();
|
||||
let users_original = gDatabase.get('users');
|
||||
try {
|
||||
users = new Set(JSON.parse(users_original));
|
||||
} catch {}
|
||||
if (!users.has(formData.name)) {
|
||||
users.add(formData.name);
|
||||
}
|
||||
users = JSON.stringify([...users].sort());
|
||||
if (users !== users_original) {
|
||||
gDatabase.set('users', users);
|
||||
}
|
||||
session = makeJwt({name: formData.name});
|
||||
account = {password: hashPassword(formData.password)};
|
||||
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
||||
if (noAdministrator()) {
|
||||
makeAdministrator(formData.name);
|
||||
}
|
||||
} else {
|
||||
loginError = 'Error registering account.';
|
||||
}
|
||||
} else if (formData.change == '1') {
|
||||
if (
|
||||
account &&
|
||||
isNameValid(formData.name) &&
|
||||
formData.new_password == formData.confirm &&
|
||||
verifyPassword(formData.password, account.password)
|
||||
) {
|
||||
session = makeJwt({name: formData.name});
|
||||
account = {password: hashPassword(formData.new_password)};
|
||||
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
||||
} else {
|
||||
loginError = 'Error changing password.';
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
account &&
|
||||
account.password &&
|
||||
verifyPassword(formData.password, account.password)
|
||||
) {
|
||||
session = makeJwt({name: formData.name});
|
||||
if (noAdministrator()) {
|
||||
makeAdministrator(formData.name);
|
||||
}
|
||||
} else {
|
||||
loginError = 'Invalid username or password.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Proceed as Guest
|
||||
session = makeJwt({name: 'guest'});
|
||||
}
|
||||
}
|
||||
|
||||
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
|
||||
let entry = readSession(session);
|
||||
if (entry && formData.return) {
|
||||
response.writeHead(303, {
|
||||
Location: formData.return,
|
||||
'Set-Cookie': cookie,
|
||||
});
|
||||
response.end();
|
||||
} else {
|
||||
File.readFile('core/auth.html')
|
||||
.then(function (data) {
|
||||
let html = utf8Decode(data);
|
||||
let auth_data = {
|
||||
session_is_new: sessionIsNew,
|
||||
name: entry?.name,
|
||||
error: loginError,
|
||||
code_of_conduct: core.globalSettings.code_of_conduct,
|
||||
have_administrator: !noAdministrator(),
|
||||
};
|
||||
html = utf8Encode(
|
||||
html.replace('$AUTH_DATA', JSON.stringify(auth_data))
|
||||
);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Set-Cookie': cookie,
|
||||
'Content-Length': html.length,
|
||||
});
|
||||
response.end(html);
|
||||
})
|
||||
.catch(function (error) {
|
||||
response.writeHead(404, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
Connection: 'close',
|
||||
});
|
||||
response.end('404 File not found');
|
||||
});
|
||||
}
|
||||
} else if (request.uri == '/login/logout') {
|
||||
response.writeHead(303, {
|
||||
'Set-Cookie': `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`,
|
||||
Location: '/login' + (request.query ? '?' + request.query : ''),
|
||||
});
|
||||
response.end();
|
||||
} else {
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
Connection: 'close',
|
||||
});
|
||||
response.end('Hello, ' + request.client.peerName + '.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user's permissions based on it's session ?
|
||||
* @param {*} session TODOC
|
||||
* @returns
|
||||
*/
|
||||
function getPermissions(session) {
|
||||
let permissions;
|
||||
let entry = readSession(session);
|
||||
if (entry) {
|
||||
permissions = getPermissionsForUser(entry.name);
|
||||
permissions.authenticated = entry.name !== 'guest';
|
||||
}
|
||||
return permissions || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's permissions ?
|
||||
* @param {string} userName TODOC
|
||||
* @returns
|
||||
*/
|
||||
function getPermissionsForUser(userName) {
|
||||
let permissions = {};
|
||||
if (
|
||||
core.globalSettings &&
|
||||
core.globalSettings.permissions &&
|
||||
core.globalSettings.permissions[userName]
|
||||
) {
|
||||
for (let i in core.globalSettings.permissions[userName]) {
|
||||
permissions[core.globalSettings.permissions[userName][i]] = true;
|
||||
}
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} headers
|
||||
* @returns
|
||||
*/
|
||||
function query(headers) {
|
||||
let session = getCookies(headers).session;
|
||||
let entry;
|
||||
let autologin = tildefriends.args.autologin;
|
||||
if ((entry = autologin ? {name: autologin} : readSession(session))) {
|
||||
return {
|
||||
session: entry,
|
||||
permissions: autologin
|
||||
? getPermissionsForUser(autologin)
|
||||
: getPermissions(session),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a JWT ?
|
||||
* @param {*} credentials TODOC
|
||||
* @returns
|
||||
*/
|
||||
function makeRefresh(credentials) {
|
||||
if (credentials?.session?.name) {
|
||||
return {
|
||||
token: makeJwt({name: credentials.session.name}),
|
||||
interval: kRefreshInterval,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {handler, query, makeRefresh};
|
@ -1,5 +1,4 @@
|
||||
import * as app from './app.js';
|
||||
import * as auth from './auth.js';
|
||||
import * as form from './form.js';
|
||||
import * as http from './http.js';
|
||||
|
||||
@ -967,7 +966,7 @@ async function useAppHandler(
|
||||
},
|
||||
respond: do_resolve,
|
||||
},
|
||||
credentials: auth.query(headers),
|
||||
credentials: httpd.auth_query(headers),
|
||||
packageOwner: packageOwner,
|
||||
packageName: packageName,
|
||||
}
|
||||
@ -1098,7 +1097,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||
let user = match[1];
|
||||
let appName = match[2];
|
||||
let credentials = auth.query(request.headers);
|
||||
let credentials = httpd.auth_query(request.headers);
|
||||
if (
|
||||
credentials &&
|
||||
credentials.session &&
|
||||
@ -1161,7 +1160,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||
let user = match[1];
|
||||
let appName = match[2];
|
||||
let credentials = auth.query(request.headers);
|
||||
let credentials = https.auth_query(request.headers);
|
||||
if (
|
||||
credentials &&
|
||||
credentials.session &&
|
||||
@ -1334,8 +1333,6 @@ loadSettings()
|
||||
if (tildefriends.https_port && gGlobalSettings.http_redirect) {
|
||||
httpd.set_http_redirect(gGlobalSettings.http_redirect);
|
||||
}
|
||||
httpd.all('/login', auth.handler);
|
||||
httpd.all('/login/logout', auth.handler);
|
||||
httpd.all('/app/socket', app.socket);
|
||||
httpd.all('', function default_http_handler(request, response) {
|
||||
let match;
|
||||
|
@ -15,22 +15,6 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #268bd2;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #6c71c4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #859900;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #2aa198;
|
||||
}
|
||||
|
||||
#logo {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.unprompted.tildefriends"
|
||||
android:versionCode="17"
|
||||
android:versionName="0.0.17">
|
||||
android:versionCode="18"
|
||||
android:versionName="0.0.18-wip">
|
||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
|
43
src/http.c
43
src/http.c
@ -1030,3 +1030,46 @@ void* tf_http_get_user_data(tf_http_t* http)
|
||||
{
|
||||
return http->user_data;
|
||||
}
|
||||
|
||||
const char* tf_http_get_cookie(const char* cookie_header, const char* name)
|
||||
{
|
||||
if (!cookie_header)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int name_start = 0;
|
||||
int equals = 0;
|
||||
for (int i = 0;; i++)
|
||||
{
|
||||
if (cookie_header[i] == '=')
|
||||
{
|
||||
equals = i;
|
||||
}
|
||||
else if (cookie_header[i] == ',' || cookie_header[i] == ';' || cookie_header[i] == '\0')
|
||||
{
|
||||
if (equals > name_start && strncmp(cookie_header + name_start, name, equals - name_start) == 0 && (int)strlen(name) == equals - name_start)
|
||||
{
|
||||
int length = i - equals - 1;
|
||||
char* result = tf_malloc(length + 1);
|
||||
memcpy(result, cookie_header + equals + 1, length);
|
||||
result[length] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cookie_header[i] == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
name_start = i + 1;
|
||||
while (cookie_header[name_start] == ' ')
|
||||
{
|
||||
name_start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
@ -196,6 +196,15 @@ void tf_http_request_unref(tf_http_request_t* request);
|
||||
*/
|
||||
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name);
|
||||
|
||||
/**
|
||||
** Get a cookie value from request headers.
|
||||
** @param cookie_header The value of the "Cookie" header of the form
|
||||
** "name1=value1; name2=value2".
|
||||
** @param name The cookie name.
|
||||
** @return The cookie value, if found, or NULL. Must be freed with tf_free().
|
||||
*/
|
||||
const char* tf_http_get_cookie(const char* cookie_header, const char* name);
|
||||
|
||||
/**
|
||||
** Send a websocket message.
|
||||
** @param request The HTTP request which was previously updated to a websocket
|
||||
|
671
src/httpd.js.c
671
src/httpd.js.c
@ -4,16 +4,24 @@
|
||||
#include "http.h"
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "ssb.h"
|
||||
#include "ssb.db.h"
|
||||
#include "task.h"
|
||||
#include "tlscontext.js.h"
|
||||
#include "trace.h"
|
||||
#include "util.js.h"
|
||||
|
||||
#include "ow-crypt.h"
|
||||
|
||||
#include "picohttpparser.h"
|
||||
|
||||
#include "sodium/crypto_sign.h"
|
||||
#include "sodium/utils.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <openssl/sha.h>
|
||||
|
||||
@ -23,7 +31,12 @@
|
||||
|
||||
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
|
||||
|
||||
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
static JSValue _authenticate_jwt(JSContext* context, const char* jwt);
|
||||
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
|
||||
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name);
|
||||
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
|
||||
|
||||
static JSClassID _httpd_class_id;
|
||||
static JSClassID _httpd_request_class_id;
|
||||
@ -311,6 +324,22 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
|
||||
headers[headers_count * 2 + 1] = key;
|
||||
headers_count++;
|
||||
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(tf_task_get(context));
|
||||
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
|
||||
JSValue jwt = _authenticate_jwt(context, session);
|
||||
tf_free((void*)session);
|
||||
JSValue name = JS_GetPropertyStr(context, jwt, "name");
|
||||
const char* name_string = JS_ToCString(context, name);
|
||||
const char* session_token = _make_session_jwt(ssb, name_string);
|
||||
const char* cookie = _make_set_session_cookie_header(request, session_token);
|
||||
tf_free((void*)session_token);
|
||||
JS_FreeCString(context, name_string);
|
||||
JS_FreeValue(context, name);
|
||||
JS_FreeValue(context, jwt);
|
||||
headers[headers_count * 2 + 0] = "Set-Cookie";
|
||||
headers[headers_count * 2 + 1] = cookie ? cookie : "";
|
||||
headers_count++;
|
||||
|
||||
bool send_version = !tf_http_request_get_header(request, "sec-websocket-version") || strcmp(tf_http_request_get_header(request, "sec-websocket-version"), "13") != 0;
|
||||
if (send_version)
|
||||
{
|
||||
@ -330,6 +359,8 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
|
||||
JS_FreeCString(context, headers[i * 2 + 1]);
|
||||
}
|
||||
|
||||
tf_free((void*)cookie);
|
||||
|
||||
request->on_message = _httpd_message_callback;
|
||||
request->on_close = _httpd_websocket_close_callback;
|
||||
request->context = context;
|
||||
@ -410,6 +441,58 @@ static JSValue _httpd_set_http_redirect(JSContext* context, JSValueConst this_va
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||
JSValue headers = argv[0];
|
||||
if (JS_IsUndefined(headers))
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
JSValue cookie = JS_GetPropertyStr(context, headers, "cookie");
|
||||
const char* cookie_string = JS_ToCString(context, cookie);
|
||||
const char* session = tf_http_get_cookie(cookie_string, "session");
|
||||
JSValue entry = _authenticate_jwt(context, session);
|
||||
tf_free((void*)session);
|
||||
JS_FreeCString(context, cookie_string);
|
||||
JS_FreeValue(context, cookie);
|
||||
|
||||
JSValue result = JS_UNDEFINED;
|
||||
if (!JS_IsUndefined(entry))
|
||||
{
|
||||
result = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, result, "session", entry);
|
||||
JSValue out_permissions = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, result, "permissions", out_permissions);
|
||||
|
||||
JSValue name = JS_GetPropertyStr(context, entry, "name");
|
||||
const char* name_string = JS_ToCString(context, name);
|
||||
|
||||
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
|
||||
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
|
||||
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
|
||||
JSValue user_permissions = JS_GetPropertyStr(context, permissions, name_string);
|
||||
int length = tf_util_get_length(context, user_permissions);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
|
||||
const char* permission_string = JS_ToCString(context, permission);
|
||||
JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
|
||||
JS_FreeCString(context, permission_string);
|
||||
JS_FreeValue(context, permission);
|
||||
}
|
||||
JS_FreeValue(context, user_permissions);
|
||||
JS_FreeValue(context, permissions);
|
||||
JS_FreeValue(context, settings_value);
|
||||
tf_free((void*)settings);
|
||||
JS_FreeCString(context, name_string);
|
||||
JS_FreeValue(context, name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void _httpd_finalizer(JSRuntime* runtime, JSValue value)
|
||||
{
|
||||
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id);
|
||||
@ -732,6 +815,590 @@ static void _httpd_endpoint_debug(tf_http_request_t* request)
|
||||
tf_free(response);
|
||||
}
|
||||
|
||||
const char** _form_data_decode(const char* data, int length)
|
||||
{
|
||||
int key_max = 1;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (data[i] == '&')
|
||||
{
|
||||
key_max++;
|
||||
}
|
||||
}
|
||||
|
||||
int write_length = length + 1;
|
||||
char** result = tf_malloc(sizeof(const char*) * (key_max + 1) * 2 + write_length);
|
||||
char* result_buffer = ((char*)result) + sizeof(const char*) * (key_max + 1) * 2;
|
||||
|
||||
char* write_pos = result_buffer;
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
while (i < length)
|
||||
{
|
||||
result[count++] = write_pos;
|
||||
while (i < length)
|
||||
{
|
||||
if (data[i] == '+')
|
||||
{
|
||||
*write_pos++ = ' ';
|
||||
i++;
|
||||
}
|
||||
else if (data[i] == '%' && i + 2 < length)
|
||||
{
|
||||
*write_pos++ = (char)strtoul((const char[]) { data[i + 1], data[i + 2], 0 }, NULL, 16);
|
||||
i += 3;
|
||||
}
|
||||
else if (data[i] == '=')
|
||||
{
|
||||
if (count % 2 == 0)
|
||||
{
|
||||
result[count++] = "";
|
||||
}
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
else if (data[i] == '&')
|
||||
{
|
||||
if (count % 2 != 0)
|
||||
{
|
||||
result[count++] = "";
|
||||
}
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
*write_pos++ = data[i++];
|
||||
}
|
||||
}
|
||||
*write_pos++ = '\0';
|
||||
}
|
||||
|
||||
result[count++] = NULL;
|
||||
result[count++] = NULL;
|
||||
|
||||
return (const char**)result;
|
||||
}
|
||||
|
||||
const char* _form_data_get(const char** form_data, const char* key)
|
||||
{
|
||||
for (int i = 0; form_data[i]; i += 2)
|
||||
{
|
||||
if (form_data[i] && strcmp(form_data[i], key) == 0)
|
||||
{
|
||||
return form_data[i + 1];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
typedef struct _login_request_t
|
||||
{
|
||||
tf_http_request_t* request;
|
||||
const char* session_cookie;
|
||||
JSValue jwt;
|
||||
const char* name;
|
||||
const char* error;
|
||||
const char* code_of_conduct;
|
||||
bool have_administrator;
|
||||
bool session_is_new;
|
||||
} login_request_t;
|
||||
|
||||
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie)
|
||||
{
|
||||
const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
|
||||
int length = session_cookie ? snprintf(NULL, 0, k_pattern, session_cookie, k_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
|
||||
char* cookie = length ? tf_malloc(length + 1) : NULL;
|
||||
if (cookie)
|
||||
{
|
||||
snprintf(cookie, length + 1, k_pattern, session_cookie, k_refresh_interval, request->is_tls ? "Secure; " : "");
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
|
||||
{
|
||||
login_request_t* login = user_data;
|
||||
tf_http_request_t* request = login->request;
|
||||
if (result >= 0)
|
||||
{
|
||||
const char* cookie = _make_set_session_cookie_header(request, login->session_cookie);
|
||||
const char* headers[] = {
|
||||
"Content-Type",
|
||||
"text/html; charset=utf-8",
|
||||
"Set-Cookie",
|
||||
cookie ? cookie : "",
|
||||
};
|
||||
const char* replace_me = "$AUTH_DATA";
|
||||
const char* auth = strstr(data, replace_me);
|
||||
if (auth)
|
||||
{
|
||||
JSContext* context = tf_task_get_context(task);
|
||||
JSValue object = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, object, "session_is_new", JS_NewBool(context, login->session_is_new));
|
||||
JS_SetPropertyStr(context, object, "name", login->name ? JS_NewString(context, login->name) : JS_UNDEFINED);
|
||||
JS_SetPropertyStr(context, object, "error", login->error ? JS_NewString(context, login->error) : JS_UNDEFINED);
|
||||
JS_SetPropertyStr(context, object, "code_of_conduct", login->code_of_conduct ? JS_NewString(context, login->code_of_conduct) : JS_UNDEFINED);
|
||||
JS_SetPropertyStr(context, object, "have_administrator", JS_NewBool(context, login->have_administrator));
|
||||
JSValue object_json = JS_JSONStringify(context, object, JS_NULL, JS_NULL);
|
||||
size_t json_length = 0;
|
||||
const char* json = JS_ToCStringLen(context, &json_length, object_json);
|
||||
|
||||
char* copy = tf_malloc(result + json_length);
|
||||
int replace_start = (auth - (const char*)data);
|
||||
int replace_end = (auth - (const char*)data) + (int)strlen(replace_me);
|
||||
memcpy(copy, data, replace_start);
|
||||
memcpy(copy + replace_start, json, json_length);
|
||||
memcpy(copy + replace_start + json_length, ((const char*)data) + replace_end, result - replace_end);
|
||||
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, copy, replace_start + json_length + (result - replace_end));
|
||||
tf_free(copy);
|
||||
|
||||
JS_FreeCString(context, json);
|
||||
JS_FreeValue(context, object_json);
|
||||
JS_FreeValue(context, object);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
|
||||
}
|
||||
tf_free((void*)cookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* k_payload = tf_http_status_text(404);
|
||||
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
|
||||
}
|
||||
tf_http_request_unref(request);
|
||||
tf_free((void*)login->name);
|
||||
tf_free((void*)login->code_of_conduct);
|
||||
tf_free((void*)login->session_cookie);
|
||||
tf_free(login);
|
||||
}
|
||||
|
||||
static bool _string_property_equals(JSContext* context, JSValue object, const char* name, const char* value)
|
||||
{
|
||||
JSValue object_value = JS_GetPropertyStr(context, object, name);
|
||||
const char* object_value_string = JS_ToCString(context, object_value);
|
||||
bool equals = object_value_string && strcmp(object_value_string, value) == 0;
|
||||
JS_FreeCString(context, object_value_string);
|
||||
JS_FreeValue(context, object_value);
|
||||
return equals;
|
||||
}
|
||||
|
||||
static void _public_key_visit(const char* identity, void* user_data)
|
||||
{
|
||||
snprintf(user_data, k_id_base64_len, "%s", identity);
|
||||
}
|
||||
|
||||
static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
|
||||
{
|
||||
if (!jwt)
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
int dot[2] = { 0 };
|
||||
int dot_count = 0;
|
||||
for (int i = 0; jwt[i]; i++)
|
||||
{
|
||||
if (jwt[i] == '.')
|
||||
{
|
||||
if (dot_count >= tf_countof(dot))
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
dot[dot_count++] = i;
|
||||
}
|
||||
}
|
||||
if (dot_count != 2)
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
uint8_t header[256] = { 0 };
|
||||
size_t actual_length = 0;
|
||||
if (sodium_base642bin(header, sizeof(header), jwt, dot[0], NULL, &actual_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0 || actual_length >= sizeof(header))
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
header[actual_length] = '\0';
|
||||
JSValue header_value = JS_ParseJSON(context, (const char*)header, actual_length, NULL);
|
||||
bool header_valid = _string_property_equals(context, header_value, "typ", "JWT") && _string_property_equals(context, header_value, "alg", "HS256");
|
||||
JS_FreeValue(context, header_value);
|
||||
if (!header_valid)
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||
char public_key_b64[k_id_base64_len] = { 0 };
|
||||
tf_ssb_db_identity_visit(ssb, ":auth", _public_key_visit, public_key_b64);
|
||||
|
||||
const char* payload = jwt + dot[0] + 1;
|
||||
size_t payload_length = dot[1] - dot[0] - 1;
|
||||
if (!tf_ssb_hmacsha256_verify(public_key_b64, payload, payload_length, jwt + dot[1] + 1, true))
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
uint8_t payload_bin[256];
|
||||
size_t actual_payload_length = 0;
|
||||
if (sodium_base642bin(payload_bin, sizeof(payload_bin), payload, payload_length, NULL, &actual_payload_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0 ||
|
||||
actual_payload_length >= sizeof(payload_bin))
|
||||
{
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
payload_bin[actual_payload_length] = '\0';
|
||||
JSValue parsed = JS_ParseJSON(context, (const char*)payload_bin, actual_payload_length, NULL);
|
||||
JSValue exp = JS_GetPropertyStr(context, parsed, "exp");
|
||||
int64_t exp_value = 0;
|
||||
JS_ToInt64(context, &exp_value, exp);
|
||||
if (time(NULL) >= exp_value)
|
||||
{
|
||||
JS_FreeValue(context, parsed);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
|
||||
{
|
||||
bool result = false;
|
||||
JSValue user = JS_GetPropertyStr(context, session, "user");
|
||||
const char* user_string = JS_ToCString(context, user);
|
||||
result = user_string && strcmp(user_string, "guest") != 0;
|
||||
JS_FreeCString(context, user_string);
|
||||
JS_FreeValue(context, user);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool _is_name_valid(const char* name)
|
||||
{
|
||||
if (!name || !((*name >= 'a' && *name <= 'z') || (*name >= 'A' && *name <= 'Z')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const char* p = name; *p; p++)
|
||||
{
|
||||
bool in_range = (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9');
|
||||
if (!in_range)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void _visit_auth_identity(const char* identity, void* user_data)
|
||||
{
|
||||
if (!*(char*)user_data)
|
||||
{
|
||||
snprintf((char*)user_data, k_id_base64_len, "%s", identity);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _get_auth_private_key(tf_ssb_t* ssb, uint8_t* out_private_key)
|
||||
{
|
||||
char id[k_id_base64_len] = { 0 };
|
||||
tf_ssb_db_identity_visit(ssb, ":auth", _visit_auth_identity, id);
|
||||
if (*id)
|
||||
{
|
||||
return tf_ssb_db_identity_get_private_key(ssb, ":auth", id, out_private_key, crypto_sign_SECRETKEYBYTES);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tf_ssb_db_identity_create(ssb, ":auth", out_private_key + crypto_sign_PUBLICKEYBYTES, out_private_key);
|
||||
}
|
||||
}
|
||||
|
||||
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
|
||||
{
|
||||
if (!name || !*name)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
|
||||
if (!_get_auth_private_key(ssb, private_key))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uv_timespec64_t now = { 0 };
|
||||
uv_clock_gettime(UV_CLOCK_REALTIME, &now);
|
||||
|
||||
JSContext* context = tf_ssb_get_context(ssb);
|
||||
|
||||
const char* header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
||||
char header_base64[256];
|
||||
sodium_bin2base64(header_base64, sizeof(header_base64), (uint8_t*)header_json, strlen(header_json), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
|
||||
|
||||
JSValue payload = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, payload, "name", JS_NewString(context, name));
|
||||
JS_SetPropertyStr(context, payload, "exp", JS_NewInt64(context, now.tv_sec * 1000 + now.tv_nsec / 1000000LL + k_refresh_interval));
|
||||
JSValue payload_json = JS_JSONStringify(context, payload, JS_NULL, JS_NULL);
|
||||
size_t payload_length = 0;
|
||||
const char* payload_string = JS_ToCStringLen(context, &payload_length, payload_json);
|
||||
char payload_base64[256];
|
||||
sodium_bin2base64(payload_base64, sizeof(payload_base64), (uint8_t*)payload_string, payload_length, sodium_base64_VARIANT_URLSAFE_NO_PADDING);
|
||||
|
||||
char* result = NULL;
|
||||
uint8_t signature[crypto_sign_BYTES];
|
||||
unsigned long long signature_length = 0;
|
||||
char signature_base64[256] = { 0 };
|
||||
|
||||
if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 0)
|
||||
{
|
||||
sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
|
||||
size_t size = strlen(header_base64) + 1 + strlen(payload_base64) + 1 + strlen(signature_base64) + 1;
|
||||
result = tf_malloc(size);
|
||||
snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
|
||||
}
|
||||
|
||||
JS_FreeCString(context, payload_string);
|
||||
JS_FreeValue(context, payload_json);
|
||||
JS_FreeValue(context, payload);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool _verify_password(const char* password, const char* hash)
|
||||
{
|
||||
char buffer[7 + 22 + 31 + 1];
|
||||
const char* out_hash = crypt_rn(password, hash, buffer, sizeof(buffer));
|
||||
return out_hash && strcmp(hash, out_hash) == 0;
|
||||
}
|
||||
|
||||
static void _httpd_endpoint_login(tf_http_request_t* request)
|
||||
{
|
||||
tf_task_t* task = request->user_data;
|
||||
JSContext* context = tf_task_get_context(task);
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
|
||||
const char** form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0);
|
||||
const char* account_name_copy = NULL;
|
||||
JSValue jwt = _authenticate_jwt(context, session);
|
||||
|
||||
if (_session_is_authenticated_as_user(context, jwt))
|
||||
{
|
||||
const char* return_url = _form_data_get(form_data, "return");
|
||||
char url[1024];
|
||||
if (!return_url)
|
||||
{
|
||||
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
|
||||
return_url = url;
|
||||
}
|
||||
const char* headers[] = {
|
||||
"Location",
|
||||
return_url,
|
||||
};
|
||||
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
|
||||
goto done;
|
||||
}
|
||||
|
||||
const char* send_session = tf_strdup(session);
|
||||
bool session_is_new = false;
|
||||
const char* login_error = NULL;
|
||||
bool may_become_first_admin = false;
|
||||
if (strcmp(request->method, "POST") == 0)
|
||||
{
|
||||
session_is_new = true;
|
||||
const char** post_form_data = _form_data_decode(request->body, request->content_length);
|
||||
const char* submit = _form_data_get(post_form_data, "submit");
|
||||
if (submit && strcmp(submit, "Login") == 0)
|
||||
{
|
||||
const char* account_name = _form_data_get(post_form_data, "name");
|
||||
account_name_copy = tf_strdup(account_name);
|
||||
const char* password = _form_data_get(post_form_data, "password");
|
||||
const char* new_password = _form_data_get(post_form_data, "new_password");
|
||||
const char* confirm = _form_data_get(post_form_data, "confirm");
|
||||
const char* change = _form_data_get(post_form_data, "change");
|
||||
const char* form_register = _form_data_get(post_form_data, "register");
|
||||
char account_passwd[256] = { 0 };
|
||||
bool have_account = tf_ssb_db_get_account_password_hash(ssb, _form_data_get(post_form_data, "name"), account_passwd, sizeof(account_passwd));
|
||||
|
||||
if (form_register && strcmp(form_register, "1") == 0)
|
||||
{
|
||||
if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0 && tf_ssb_db_register_account(ssb, account_name, password))
|
||||
{
|
||||
tf_free((void*)send_session);
|
||||
send_session = _make_session_jwt(ssb, account_name);
|
||||
may_become_first_admin = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
login_error = "Error registering account.";
|
||||
}
|
||||
}
|
||||
else if (change && strcmp(change, "1") == 0)
|
||||
{
|
||||
if (have_account && _is_name_valid(account_name) && new_password && confirm && strcmp(new_password, confirm) == 0 && _verify_password(password, account_passwd) &&
|
||||
tf_ssb_db_set_account_password(ssb, account_name, new_password))
|
||||
{
|
||||
tf_free((void*)send_session);
|
||||
send_session = _make_session_jwt(ssb, account_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
login_error = "Error changing password.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (have_account && *account_passwd && _verify_password(password, account_passwd))
|
||||
{
|
||||
tf_free((void*)send_session);
|
||||
send_session = _make_session_jwt(ssb, account_name);
|
||||
may_become_first_admin = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
login_error = "Invalid username or password.";
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_free((void*)send_session);
|
||||
send_session = _make_session_jwt(ssb, "guest");
|
||||
}
|
||||
tf_free(post_form_data);
|
||||
}
|
||||
|
||||
if (session_is_new && _form_data_get(form_data, "return") && !login_error)
|
||||
{
|
||||
const char* return_url = _form_data_get(form_data, "return");
|
||||
char url[1024];
|
||||
if (!return_url)
|
||||
{
|
||||
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
|
||||
return_url = url;
|
||||
}
|
||||
const char* cookie = _make_set_session_cookie_header(request, send_session);
|
||||
const char* headers[] = {
|
||||
"Location",
|
||||
return_url,
|
||||
"Set-Cookie",
|
||||
cookie ? cookie : "",
|
||||
};
|
||||
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
|
||||
tf_free((void*)cookie);
|
||||
tf_free((void*)send_session);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_http_request_ref(request);
|
||||
|
||||
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
|
||||
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
|
||||
JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
|
||||
const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
|
||||
|
||||
bool have_administrator = false;
|
||||
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
|
||||
|
||||
JSPropertyEnum* ptab = NULL;
|
||||
uint32_t plen = 0;
|
||||
JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
|
||||
for (int i = 0; i < (int)plen; i++)
|
||||
{
|
||||
JSPropertyDescriptor desc = { 0 };
|
||||
if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
|
||||
{
|
||||
int permission_length = tf_util_get_length(context, desc.value);
|
||||
for (int i = 0; i < permission_length; i++)
|
||||
{
|
||||
JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
|
||||
const char* permission = JS_ToCString(context, entry);
|
||||
if (permission && strcmp(permission, "administration") == 0)
|
||||
{
|
||||
have_administrator = true;
|
||||
}
|
||||
JS_FreeCString(context, permission);
|
||||
JS_FreeValue(context, entry);
|
||||
}
|
||||
JS_FreeValue(context, desc.setter);
|
||||
JS_FreeValue(context, desc.getter);
|
||||
JS_FreeValue(context, desc.value);
|
||||
}
|
||||
}
|
||||
for (uint32_t i = 0; i < plen; ++i)
|
||||
{
|
||||
JS_FreeAtom(context, ptab[i].atom);
|
||||
}
|
||||
js_free(context, ptab);
|
||||
|
||||
if (!have_administrator && may_become_first_admin)
|
||||
{
|
||||
if (JS_IsUndefined(permissions))
|
||||
{
|
||||
permissions = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, settings_value, "permissions", permissions);
|
||||
}
|
||||
JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
|
||||
if (JS_IsUndefined(user))
|
||||
{
|
||||
user = JS_NewArray(context);
|
||||
JS_SetPropertyStr(context, permissions, account_name_copy, user);
|
||||
}
|
||||
JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
|
||||
|
||||
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
|
||||
const char* settings_string = JS_ToCString(context, settings_json);
|
||||
tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
|
||||
JS_FreeCString(context, settings_string);
|
||||
JS_FreeValue(context, settings_json);
|
||||
}
|
||||
JS_FreeValue(context, permissions);
|
||||
|
||||
login_request_t* login = tf_malloc(sizeof(login_request_t));
|
||||
*login = (login_request_t) {
|
||||
.request = request,
|
||||
.name = account_name_copy,
|
||||
.jwt = jwt,
|
||||
.error = login_error,
|
||||
.session_cookie = send_session,
|
||||
.session_is_new = session_is_new,
|
||||
.code_of_conduct = tf_strdup(code_of_conduct),
|
||||
.have_administrator = have_administrator,
|
||||
};
|
||||
|
||||
JS_FreeCString(context, code_of_conduct);
|
||||
JS_FreeValue(context, code_of_conduct_value);
|
||||
JS_FreeValue(context, settings_value);
|
||||
tf_free((void*)settings);
|
||||
tf_file_read(request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
|
||||
jwt = JS_UNDEFINED;
|
||||
account_name_copy = NULL;
|
||||
}
|
||||
|
||||
done:
|
||||
tf_free((void*)session);
|
||||
tf_free(form_data);
|
||||
tf_free((void*)account_name_copy);
|
||||
JS_FreeValue(context, jwt);
|
||||
}
|
||||
|
||||
static void _httpd_endpoint_logout(tf_http_request_t* request)
|
||||
{
|
||||
const char* k_set_cookie = request->is_tls ? "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly"
|
||||
: "session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
|
||||
const char* k_location_format = "/login%s%s";
|
||||
int length = snprintf(NULL, 0, k_location_format, request->query ? "?" : "", request->query);
|
||||
char* location = alloca(length + 1);
|
||||
snprintf(location, length + 1, k_location_format, request->query ? "?" : "", request->query ? request->query : "");
|
||||
const char* headers[] = {
|
||||
"Set-Cookie",
|
||||
k_set_cookie,
|
||||
"Location",
|
||||
location,
|
||||
};
|
||||
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
|
||||
}
|
||||
|
||||
void tf_httpd_register(JSContext* context)
|
||||
{
|
||||
JS_NewClassID(&_httpd_class_id);
|
||||
@ -775,10 +1442,14 @@ void tf_httpd_register(JSContext* context)
|
||||
tf_http_add_handler(http, "/mem", _httpd_endpoint_mem, NULL, task);
|
||||
tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, NULL, task);
|
||||
|
||||
tf_http_add_handler(http, "/login/logout", _httpd_endpoint_logout, NULL, task);
|
||||
tf_http_add_handler(http, "/login", _httpd_endpoint_login, NULL, task);
|
||||
|
||||
JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context));
|
||||
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2));
|
||||
JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_endpoint_start, "start", 2));
|
||||
JS_SetPropertyStr(context, httpd, "set_http_redirect", JS_NewCFunction(context, _httpd_set_http_redirect, "set_http_redirect", 1));
|
||||
JS_SetPropertyStr(context, httpd, "auth_query", JS_NewCFunction(context, _httpd_auth_query, "auth_query", 1));
|
||||
JS_SetPropertyStr(context, global, "httpd", httpd);
|
||||
JS_FreeValue(context, global);
|
||||
}
|
||||
|
@ -516,7 +516,7 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
|
||||
|
||||
if (args.count == 1)
|
||||
{
|
||||
_tf_run_task(&args, 0);
|
||||
result = _tf_run_task(&args, 0);
|
||||
}
|
||||
if (args.count > 1)
|
||||
{
|
||||
|
27
src/ssb.c
27
src/ssb.c
@ -3910,3 +3910,30 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
|
||||
uv_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0);
|
||||
uv_unref((uv_handle_t*)&timer->timer);
|
||||
}
|
||||
|
||||
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature, bool signature_is_urlb64)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
const char* public_key_start = public_key && *public_key == '@' ? public_key + 1 : public_key;
|
||||
const char* public_key_end = public_key_start ? strstr(public_key_start, ".ed25519") : NULL;
|
||||
if (public_key_start && !public_key_end)
|
||||
{
|
||||
public_key_end = public_key_start + strlen(public_key_start);
|
||||
}
|
||||
|
||||
uint8_t bin_public_key[crypto_sign_PUBLICKEYBYTES] = { 0 };
|
||||
if (tf_base64_decode(public_key_start, public_key_end - public_key_start, bin_public_key, sizeof(bin_public_key)) > 0)
|
||||
{
|
||||
uint8_t bin_signature[crypto_sign_BYTES] = { 0 };
|
||||
if (sodium_base642bin(bin_signature, sizeof(bin_signature), signature, strlen(signature), NULL, NULL, NULL,
|
||||
signature_is_urlb64 ? sodium_base64_VARIANT_URLSAFE_NO_PADDING : sodium_base64_VARIANT_ORIGINAL) == 0)
|
||||
{
|
||||
if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0)
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
157
src/ssb.db.c
157
src/ssb.db.c
@ -6,6 +6,7 @@
|
||||
#include "trace.h"
|
||||
#include "util.js.h"
|
||||
|
||||
#include "ow-crypt.h"
|
||||
#include "sodium/crypto_hash_sha256.h"
|
||||
#include "sodium/crypto_sign.h"
|
||||
#include "sqlite3.h"
|
||||
@ -1059,9 +1060,15 @@ bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_pub
|
||||
char private[512];
|
||||
tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private));
|
||||
if (tf_ssb_db_identity_add(ssb, user, public, private))
|
||||
{
|
||||
if (out_public_key)
|
||||
{
|
||||
tf_ssb_id_str_to_bin(out_public_key, public);
|
||||
}
|
||||
if (out_private_key)
|
||||
{
|
||||
tf_ssb_id_str_to_bin(out_private_key, private);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1173,8 +1180,16 @@ bool tf_ssb_db_identity_get_private_key(tf_ssb_t* ssb, const char* user, const c
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("Bind failed: %s.\n", sqlite3_errmsg(db));
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("Prepare failed: %s.\n", sqlite3_errmsg(db));
|
||||
}
|
||||
tf_ssb_release_db_reader(ssb, db);
|
||||
return success;
|
||||
}
|
||||
@ -1578,3 +1593,145 @@ void tf_ssb_db_forget_stored_connection(tf_ssb_t* ssb, const char* address, int
|
||||
}
|
||||
tf_ssb_release_db_writer(ssb, db);
|
||||
}
|
||||
|
||||
bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char* out_password, size_t password_size)
|
||||
{
|
||||
bool result = false;
|
||||
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db, "SELECT value ->> '$.password' FROM properties WHERE id = 'auth' AND key = 'user:' || ?", -1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_step(statement) == SQLITE_ROW)
|
||||
{
|
||||
snprintf(out_password, password_size, "%s", (const char*)sqlite3_column_text(statement, 0));
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
tf_ssb_release_db_reader(ssb, db);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char* password)
|
||||
{
|
||||
JSContext* context = tf_ssb_get_context(ssb);
|
||||
bool result = false;
|
||||
static const int k_salt_length = 12;
|
||||
|
||||
char buffer[16];
|
||||
size_t bytes = uv_random(tf_ssb_get_loop(ssb), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
|
||||
char output[7 + 22 + 1];
|
||||
char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output));
|
||||
char hash_output[7 + 22 + 31 + 1];
|
||||
char* hash = crypt_rn(password, salt, hash_output, sizeof(hash_output));
|
||||
|
||||
JSValue user_entry = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, user_entry, "password", JS_NewString(context, hash));
|
||||
JSValue user_json = JS_JSONStringify(context, user_entry, JS_NULL, JS_NULL);
|
||||
size_t user_length = 0;
|
||||
const char* user_string = JS_ToCStringLen(context, &user_length, user_json);
|
||||
|
||||
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, user_string, user_length, NULL) == SQLITE_OK)
|
||||
{
|
||||
result = sqlite3_step(statement) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
tf_ssb_release_db_writer(ssb, db);
|
||||
|
||||
JS_FreeCString(context, user_string);
|
||||
JS_FreeValue(context, user_json);
|
||||
JS_FreeValue(context, user_entry);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* password)
|
||||
{
|
||||
bool result = false;
|
||||
JSContext* context = tf_ssb_get_context(ssb);
|
||||
JSValue users_array = JS_UNDEFINED;
|
||||
|
||||
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_step(statement) == SQLITE_ROW)
|
||||
{
|
||||
users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL);
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
if (JS_IsUndefined(users_array))
|
||||
{
|
||||
users_array = JS_NewArray(context);
|
||||
}
|
||||
int length = tf_util_get_length(context, users_array);
|
||||
JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name));
|
||||
|
||||
JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL);
|
||||
JS_FreeValue(context, users_array);
|
||||
size_t value_length = 0;
|
||||
const char* value = JS_ToCStringLen(context, &value_length, json);
|
||||
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
|
||||
{
|
||||
result = sqlite3_step(statement) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
JS_FreeCString(context, value);
|
||||
JS_FreeValue(context, json);
|
||||
tf_ssb_release_db_writer(ssb, db);
|
||||
|
||||
result = result && tf_ssb_db_set_account_password(ssb, name, password);
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* key)
|
||||
{
|
||||
char* result = NULL;
|
||||
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_step(statement) == SQLITE_ROW)
|
||||
{
|
||||
size_t length = sqlite3_column_bytes(statement, 0);
|
||||
result = tf_malloc(length + 1);
|
||||
memcpy(result, sqlite3_column_text(statement, 0), length);
|
||||
result[length] = '\0';
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
tf_ssb_release_db_reader(ssb, db);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
|
||||
{
|
||||
bool result = false;
|
||||
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?, ?, ?)", -1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
|
||||
sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
|
||||
{
|
||||
result = sqlite3_step(statement) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
tf_ssb_release_db_writer(ssb, db);
|
||||
return result;
|
||||
}
|
||||
|
49
src/ssb.db.h
49
src/ssb.db.h
@ -172,7 +172,7 @@ int tf_ssb_db_identity_get_count_for_user(tf_ssb_t* ssb, const char* user);
|
||||
** @param ssb The SSB instance.
|
||||
** @param user The user's username.
|
||||
** @param[out] out_public_key A buffer populated with the new public key.
|
||||
** @param[out] out_private_key A buffer populated with the new privatee key.
|
||||
** @param[out] out_private_key A buffer populated with the new private key.
|
||||
** @return True if the identity was created.
|
||||
*/
|
||||
bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_public_key, uint8_t* out_private_key);
|
||||
@ -311,6 +311,53 @@ tf_ssb_db_stored_connection_t* tf_ssb_db_get_stored_connections(tf_ssb_t* ssb, i
|
||||
*/
|
||||
void tf_ssb_db_forget_stored_connection(tf_ssb_t* ssb, const char* address, int port, const char* pubkey);
|
||||
|
||||
/**
|
||||
** Retrieve a user's hashed password from the database.
|
||||
** @param ssb The SSB instance.
|
||||
** @param name The username.
|
||||
** @param[out] out_password Populated with the password.
|
||||
** @param password_size The size of the out_password buffer.
|
||||
** @return true if the password hash was successfully retrieved.
|
||||
*/
|
||||
bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char* out_password, size_t password_size);
|
||||
|
||||
/**
|
||||
** Insert or update a user's hashed password in the database.
|
||||
** @param ssb The SSB instance.
|
||||
** @param name The username.
|
||||
** @param password The raw password.
|
||||
** @return true if the hash of the password was successfully stored.
|
||||
*/
|
||||
bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char* password);
|
||||
|
||||
/**
|
||||
** Add a user account to the database.
|
||||
** @param ssb The SSB instance.
|
||||
** @param name The username to add.
|
||||
** @param password The user's raw password.
|
||||
** @return true If the user was added successfully.
|
||||
*/
|
||||
bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* password);
|
||||
|
||||
/**
|
||||
** Get an entry from the properties table.
|
||||
** @param ssb The SSB instance.
|
||||
** @param id The user.
|
||||
** @param key The property key.
|
||||
** @return The property value or null. Free with tf_free().
|
||||
*/
|
||||
const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* key);
|
||||
|
||||
/**
|
||||
** Store an entry in the properties table.
|
||||
** @param ssb The SSB instance.
|
||||
** @param id The user.
|
||||
** @param key The property key.
|
||||
** @param value The property value.
|
||||
** @return true if the property was stored successfully.
|
||||
*/
|
||||
bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
|
||||
|
||||
/**
|
||||
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
|
||||
** @param user_data User data registered with the authorizer.
|
||||
|
11
src/ssb.h
11
src/ssb.h
@ -957,4 +957,15 @@ void tf_ssb_set_room_name(tf_ssb_t* ssb, const char* room_name);
|
||||
*/
|
||||
void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data);
|
||||
|
||||
/**
|
||||
** Verify a signature.
|
||||
** @param public_key The public key for which the message was signed.
|
||||
** @param payload The signed payload.
|
||||
** @param payload_length The length of the signed payload in bytes.
|
||||
** @param signature The signature.
|
||||
** @param signature_is_urlb64 True if the signature is in URL base64 format, otherwise standard base64.
|
||||
** @return true If the message was successfully verified.
|
||||
*/
|
||||
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature, bool signature_is_urlb64);
|
||||
|
||||
/** @} */
|
||||
|
74
src/ssb.js.c
74
src/ssb.js.c
@ -1281,78 +1281,6 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_hmacsha256_sign(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
|
||||
|
||||
size_t payload_length = 0;
|
||||
const char* payload = JS_ToCStringLen(context, &payload_length, argv[0]);
|
||||
const char* user = JS_ToCString(context, argv[1]);
|
||||
const char* public_key = JS_ToCString(context, argv[2]);
|
||||
|
||||
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
|
||||
if (tf_ssb_db_identity_get_private_key(ssb, user, public_key, private_key, sizeof(private_key)))
|
||||
{
|
||||
uint8_t signature[crypto_sign_BYTES];
|
||||
unsigned long long siglen;
|
||||
if (crypto_sign_detached(signature, &siglen, (const uint8_t*)payload, payload_length, private_key) == 0)
|
||||
{
|
||||
char signature_base64[crypto_sign_BYTES * 2];
|
||||
tf_base64_encode(signature, sizeof(signature), signature_base64, sizeof(signature_base64));
|
||||
result = JS_NewString(context, signature_base64);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = JS_ThrowInternalError(context, "Private key not found.");
|
||||
}
|
||||
|
||||
JS_FreeCString(context, public_key);
|
||||
JS_FreeCString(context, user);
|
||||
JS_FreeCString(context, payload);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_hmacsha256_verify(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
|
||||
size_t public_key_length = 0;
|
||||
const char* public_key = JS_ToCStringLen(context, &public_key_length, argv[0]);
|
||||
size_t payload_length = 0;
|
||||
const char* payload = JS_ToCStringLen(context, &payload_length, argv[1]);
|
||||
size_t signature_length = 0;
|
||||
const char* signature = JS_ToCStringLen(context, &signature_length, argv[2]);
|
||||
|
||||
const char* public_key_start = public_key && *public_key == '@' ? public_key + 1 : public_key;
|
||||
const char* public_key_end = public_key_start ? strstr(public_key_start, ".ed25519") : NULL;
|
||||
if (public_key_start && !public_key_end)
|
||||
{
|
||||
public_key_end = public_key_start + strlen(public_key_start);
|
||||
}
|
||||
|
||||
uint8_t bin_public_key[crypto_sign_PUBLICKEYBYTES] = { 0 };
|
||||
if (tf_base64_decode(public_key_start, public_key_end - public_key_start, bin_public_key, sizeof(bin_public_key)) > 0)
|
||||
{
|
||||
uint8_t bin_signature[crypto_sign_BYTES] = { 0 };
|
||||
if (tf_base64_decode(signature, signature_length, bin_signature, sizeof(bin_signature)) > 0)
|
||||
{
|
||||
if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0)
|
||||
{
|
||||
result = JS_TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JS_FreeCString(context, signature);
|
||||
JS_FreeCString(context, payload);
|
||||
JS_FreeCString(context, public_key);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
@ -1750,8 +1678,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
||||
JS_SetPropertyStr(context, object, "setServerFollowingMe", JS_NewCFunction(context, _tf_ssb_set_server_following_me, "setServerFollowingMe", 3));
|
||||
JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
|
||||
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
|
||||
JS_SetPropertyStr(context, object, "hmacsha256sign", JS_NewCFunction(context, _tf_ssb_hmacsha256_sign, "hmacsha256sign", 3));
|
||||
JS_SetPropertyStr(context, object, "hmacsha256verify", JS_NewCFunction(context, _tf_ssb_hmacsha256_verify, "hmacsha256verify", 3));
|
||||
JS_SetPropertyStr(context, object, "privateMessageEncrypt", JS_NewCFunction(context, _tf_ssb_private_message_encrypt, "privateMessageEncrypt", 4));
|
||||
JS_SetPropertyStr(context, object, "privateMessageDecrypt", JS_NewCFunction(context, _tf_ssb_private_message_decrypt, "privateMessageDecrypt", 3));
|
||||
/* Write. */
|
||||
|
@ -113,8 +113,8 @@ typedef struct _tf_task_t
|
||||
bool _trusted;
|
||||
bool _one_proc;
|
||||
bool _killed;
|
||||
int32_t _exitCode;
|
||||
char _scriptName[256];
|
||||
int _global_exception_count;
|
||||
|
||||
JSRuntime* _runtime;
|
||||
JSContext* _context;
|
||||
@ -421,11 +421,11 @@ int tf_task_execute(tf_task_t* task, const char* fileName)
|
||||
if (source)
|
||||
{
|
||||
JSValue result = JS_Eval(task->_context, source, strlen(source), fileName, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_ASYNC);
|
||||
if (tf_util_report_error(task->_context, result))
|
||||
if (tf_util_report_error(task->_context, result) || task->_global_exception_count)
|
||||
{
|
||||
tf_printf("Reported an error.\n");
|
||||
}
|
||||
if (!JS_IsError(task->_context, result) && !JS_IsException(result))
|
||||
else
|
||||
{
|
||||
executed = true;
|
||||
}
|
||||
@ -1457,6 +1457,8 @@ static void _tf_task_promise_rejection_tracker(JSContext* context, JSValueConst
|
||||
if (!is_handled)
|
||||
{
|
||||
tf_util_report_error(context, reason);
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
task->_global_exception_count++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,7 +443,7 @@ JSValue tf_taskstub_kill(tf_taskstub_t* stub)
|
||||
JSValue result = JS_UNDEFINED;
|
||||
if (!tf_task_get_one_proc(stub->_owner))
|
||||
{
|
||||
uv_process_kill(&stub->_process, SIGTERM);
|
||||
uv_process_kill(&stub->_process, SIGKILL);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
57
src/tests.c
57
src/tests.c
@ -58,6 +58,20 @@ static void _test_nop(const tf_test_options_t* options)
|
||||
assert(WEXITSTATUS(result) == 0);
|
||||
}
|
||||
|
||||
static void _test_exception(const tf_test_options_t* options)
|
||||
{
|
||||
_write_file("out/test.js", "throw new Error('oops');");
|
||||
|
||||
char command[256];
|
||||
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
|
||||
tf_printf("%s\n", command);
|
||||
int result = system(command);
|
||||
tf_printf("result = %d\n", result);
|
||||
(void)result;
|
||||
assert(WIFEXITED(result));
|
||||
assert(WEXITSTATUS(result) != 0);
|
||||
}
|
||||
|
||||
#if !defined(__HAIKU__)
|
||||
static void _test_sandbox(const tf_test_options_t* options)
|
||||
{
|
||||
@ -372,7 +386,7 @@ static void _test_import(const tf_test_options_t* options)
|
||||
result = system(command);
|
||||
tf_printf("returned %d\n", WEXITSTATUS(result));
|
||||
assert(WIFEXITED(result));
|
||||
assert(WEXITSTATUS(result) == 0);
|
||||
assert(WEXITSTATUS(result) != 0);
|
||||
|
||||
unlink("out/test.js");
|
||||
unlink("out/required.js");
|
||||
@ -606,36 +620,7 @@ static void _test_file(const tf_test_options_t* options)
|
||||
"});\n");
|
||||
|
||||
char command[256];
|
||||
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
|
||||
tf_printf("%s\n", command);
|
||||
int result = system(command);
|
||||
tf_printf("returned %d\n", WEXITSTATUS(result));
|
||||
assert(WIFEXITED(result));
|
||||
assert(WEXITSTATUS(result) == 0);
|
||||
|
||||
unlink("out/test.js");
|
||||
}
|
||||
|
||||
static void _test_sign(const tf_test_options_t* options)
|
||||
{
|
||||
_write_file("out/test.js",
|
||||
"'use strict';\n"
|
||||
"let id = ssb.createIdentity('test');\n"
|
||||
"print(id);\n"
|
||||
"let sig = ssb.hmacsha256sign('hello', 'test', id);\n"
|
||||
"print(sig);\n"
|
||||
"if (!ssb.hmacsha256verify(id, 'hello', sig)) {\n"
|
||||
" exit(1);\n"
|
||||
"}\n"
|
||||
"if (ssb.hmacsha256verify(id, 'world', sig)) {\n"
|
||||
" exit(1);\n"
|
||||
"}\n"
|
||||
"if (ssb.hmacsha256verify(id, 'hello1', sig)) {\n"
|
||||
" exit(1);\n"
|
||||
"}\n");
|
||||
|
||||
char command[256];
|
||||
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
|
||||
snprintf(command, sizeof(command), "%s run --db-path=out/test.db -s out/test.js" TEST_ARGS, options->exe_path);
|
||||
tf_printf("%s\n", command);
|
||||
int result = system(command);
|
||||
tf_printf("returned %d\n", WEXITSTATUS(result));
|
||||
@ -701,6 +686,14 @@ static void _test_http_async(uv_async_t* async)
|
||||
static void _test_http_thread(void* data)
|
||||
{
|
||||
test_http_t* test = data;
|
||||
const char* value = tf_http_get_cookie("a=foo; b=bar", "a");
|
||||
assert(strcmp(value, "foo") == 0);
|
||||
tf_free((void*)value);
|
||||
value = tf_http_get_cookie("a=foo; b=bar", "b");
|
||||
assert(strcmp(value, "bar") == 0);
|
||||
tf_free((void*)value);
|
||||
assert(tf_http_get_cookie("a=foo; b=bar", "c") == NULL);
|
||||
|
||||
int r = system("curl -v http://localhost:23456/404");
|
||||
assert(WEXITSTATUS(r) == 0);
|
||||
tf_printf("curl returned %d\n", WEXITSTATUS(r));
|
||||
@ -883,6 +876,7 @@ void tf_tests(const tf_test_options_t* options)
|
||||
_tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false);
|
||||
_tf_test_run(options, "ssb_following", tf_ssb_test_following, false);
|
||||
_tf_test_run(options, "nop", _test_nop, false);
|
||||
_tf_test_run(options, "exception", _test_exception, false);
|
||||
#if !defined(__HAIKU__)
|
||||
_tf_test_run(options, "sandbox", _test_sandbox, false);
|
||||
#endif
|
||||
@ -900,7 +894,6 @@ void tf_tests(const tf_test_options_t* options)
|
||||
_tf_test_run(options, "float", _test_float, false);
|
||||
_tf_test_run(options, "socket", _test_socket, false);
|
||||
_tf_test_run(options, "file", _test_file, false);
|
||||
_tf_test_run(options, "sign", _test_sign, false);
|
||||
_tf_test_run(options, "b64", _test_b64, false);
|
||||
_tf_test_run(options, "rooms", tf_ssb_test_rooms, false);
|
||||
_tf_test_run(options, "bench", tf_ssb_test_bench, false);
|
||||
|
113
src/util.js.c
113
src/util.js.c
@ -244,67 +244,6 @@ bool tf_util_report_error(JSContext* context, JSValue value)
|
||||
return is_error;
|
||||
}
|
||||
|
||||
static JSValue _util_parseHttpRequest(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
const char* method = NULL;
|
||||
size_t method_length = 0;
|
||||
const char* path = NULL;
|
||||
size_t path_length = 0;
|
||||
int minor_version = 0;
|
||||
struct phr_header headers[100];
|
||||
size_t header_count = sizeof(headers) / sizeof(*headers);
|
||||
int previous_length = 0;
|
||||
JS_ToInt32(context, &previous_length, argv[1]);
|
||||
|
||||
JSValue buffer = JS_UNDEFINED;
|
||||
size_t length;
|
||||
uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[0]);
|
||||
if (!array)
|
||||
{
|
||||
size_t offset;
|
||||
size_t element_size;
|
||||
buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &length, &element_size);
|
||||
if (!JS_IsException(buffer))
|
||||
{
|
||||
array = tf_util_try_get_array_buffer(context, &length, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if (array)
|
||||
{
|
||||
int parse_result = phr_parse_request((const char*)array, length, &method, &method_length, &path, &path_length, &minor_version, headers, &header_count, previous_length);
|
||||
if (parse_result > 0)
|
||||
{
|
||||
result = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, result, "bytes_parsed", JS_NewInt32(context, parse_result));
|
||||
JS_SetPropertyStr(context, result, "minor_version", JS_NewInt32(context, minor_version));
|
||||
JS_SetPropertyStr(context, result, "method", JS_NewStringLen(context, method, method_length));
|
||||
JS_SetPropertyStr(context, result, "path", JS_NewStringLen(context, path, path_length));
|
||||
JSValue header_object = JS_NewObject(context);
|
||||
for (int i = 0; i < (int)header_count; i++)
|
||||
{
|
||||
char name[256];
|
||||
snprintf(name, sizeof(name), "%.*s", (int)headers[i].name_len, headers[i].name);
|
||||
JS_SetPropertyStr(context, header_object, name, JS_NewStringLen(context, headers[i].value, headers[i].value_len));
|
||||
}
|
||||
JS_SetPropertyStr(context, result, "headers", header_object);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = JS_NewInt32(context, parse_result);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = JS_ThrowTypeError(context, "Could not convert argument to array.");
|
||||
}
|
||||
|
||||
JS_FreeValue(context, buffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
@ -365,16 +304,6 @@ static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue _util_sha1_digest(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
size_t length = 0;
|
||||
const char* value = JS_ToCStringLen(context, &length, argv[0]);
|
||||
unsigned char digest[SHA_DIGEST_LENGTH] = { 0 };
|
||||
SHA1((const unsigned char*)value, length, digest);
|
||||
JS_FreeCString(context, value);
|
||||
return JS_NewArrayBufferCopy(context, digest, sizeof(digest));
|
||||
}
|
||||
|
||||
JSValue tf_util_new_uint8_array(JSContext* context, const uint8_t* data, size_t size)
|
||||
{
|
||||
JSValue array_buffer = JS_NewArrayBufferCopy(context, data, size);
|
||||
@ -387,45 +316,6 @@ JSValue tf_util_new_uint8_array(JSContext* context, const uint8_t* data, size_t
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue _util_mask_bytes(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue result = JS_UNDEFINED;
|
||||
uint32_t mask = 0;
|
||||
JS_ToUint32(context, &mask, argv[1]);
|
||||
uint64_t double_mask = ((uint64_t)mask << 32) | mask;
|
||||
|
||||
size_t offset = 0;
|
||||
size_t length = 0;
|
||||
size_t element_size = 0;
|
||||
JSValue buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &length, &element_size);
|
||||
if (!JS_IsException(buffer))
|
||||
{
|
||||
size_t size = 0;
|
||||
const uint8_t* array = tf_util_try_get_array_buffer(context, &size, buffer);
|
||||
if (array)
|
||||
{
|
||||
uint8_t* copy = tf_malloc(size);
|
||||
size_t i = 0;
|
||||
for (; i + sizeof(double_mask) < size; i += sizeof(double_mask))
|
||||
{
|
||||
((uint64_t*)copy)[i / sizeof(double_mask)] = ((const uint64_t*)array)[i / sizeof(double_mask)] ^ double_mask;
|
||||
}
|
||||
for (; i + sizeof(mask) < size; i += sizeof(mask))
|
||||
{
|
||||
((uint32_t*)copy)[i / sizeof(mask)] = ((const uint32_t*)array)[i / sizeof(mask)] ^ mask;
|
||||
}
|
||||
for (; i < size; i++)
|
||||
{
|
||||
copy[i] = array[i] ^ ((mask >> (8 * (i % 4))) & 0xff);
|
||||
}
|
||||
result = tf_util_new_uint8_array(context, copy, size);
|
||||
tf_free(copy);
|
||||
}
|
||||
}
|
||||
JS_FreeValue(context, buffer);
|
||||
return result;
|
||||
}
|
||||
|
||||
void tf_util_register(JSContext* context)
|
||||
{
|
||||
JSValue global = JS_GetGlobalObject(context);
|
||||
@ -436,10 +326,7 @@ void tf_util_register(JSContext* context)
|
||||
JS_SetPropertyStr(context, global, "bip39Words", JS_NewCFunction(context, _util_bip39_words, "bip39Words", 1));
|
||||
JS_SetPropertyStr(context, global, "bip39Bytes", JS_NewCFunction(context, _util_bip39_bytes, "bip39Bytes", 1));
|
||||
JS_SetPropertyStr(context, global, "print", JS_NewCFunction(context, _util_print, "print", 1));
|
||||
JS_SetPropertyStr(context, global, "parseHttpRequest", JS_NewCFunction(context, _util_parseHttpRequest, "parseHttpRequest", 2));
|
||||
JS_SetPropertyStr(context, global, "parseHttpResponse", JS_NewCFunction(context, _util_parseHttpResponse, "parseHttpResponse", 2));
|
||||
JS_SetPropertyStr(context, global, "sha1Digest", JS_NewCFunction(context, _util_sha1_digest, "sha1Digest", 1));
|
||||
JS_SetPropertyStr(context, global, "maskBytes", JS_NewCFunction(context, _util_mask_bytes, "maskBytes", 2));
|
||||
JS_FreeValue(context, global);
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
#define VERSION_NUMBER "0.0.17"
|
||||
#define VERSION_NAME "Please enjoy responsibly."
|
||||
#define VERSION_NUMBER "0.0.18-wip"
|
||||
#define VERSION_NAME "Celebrating totality for upwards of 3m1.4s."
|
||||
|
@ -28,7 +28,7 @@ try:
|
||||
driver.get('http://localhost:8888')
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
@ -39,7 +39,13 @@ try:
|
||||
driver.switch_to.default_content()
|
||||
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
|
||||
# StaleElementReferenceException
|
||||
while True:
|
||||
try:
|
||||
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
|
||||
driver.switch_to.alert.accept()
|
||||
@ -71,7 +77,13 @@ try:
|
||||
driver.switch_to.default_content()
|
||||
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
|
||||
# StaleElementReferenceException
|
||||
while True:
|
||||
try:
|
||||
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
|
||||
break
|
||||
except:
|
||||
pass
|
||||
# NoSuchShadowRootException
|
||||
while True:
|
||||
try:
|
||||
@ -89,15 +101,15 @@ try:
|
||||
driver.switch_to.default_content()
|
||||
driver.find_element(By.ID, 'allow').click()
|
||||
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
|
||||
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click()
|
||||
|
||||
@ -109,7 +121,7 @@ try:
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout guest').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
|
||||
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
|
||||
@ -120,7 +132,7 @@ try:
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
|
||||
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('wrong_test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
@ -141,20 +153,20 @@ try:
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
|
||||
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'change_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'new_password').send_keys('new_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('new_password')
|
||||
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
|
||||
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
|
||||
|
Reference in New Issue
Block a user