16 Commits

Author SHA1 Message Date
b9a73106ed Better CSS for the ssb app to fill the iframe? 2024-04-10 18:43:48 -04:00
c674cca482 Move some DB things out of httpd. 2024-04-04 21:00:59 -04:00
81d1228b92 Experimenting with w3.css themes. 2024-04-04 20:35:09 -04:00
6ae61d5b81 Fix wiki vs. JSONB. 2024-04-05 00:11:55 +01:00
9cb872eec2 Remove JS functions: hmacsha256sign, hmac2ha256verify, parseHttpRequest, sha1Digest, and maskBytes. These are no longer needed with httpd and auth in C 2024-04-03 21:14:52 -04:00
68e8c010b7 Show recently used emojis in the emoji picker. #16 2024-04-04 02:12:43 +01:00
9671413906 Make it easier to @mention the person to whom you are replying. 2024-04-04 00:50:59 +01:00
4c8d24c319 Consolidate markdown linkification, and add support for authors, blobs, and messages. 2024-04-04 00:18:39 +01:00
e50144bd34 Validate exit codes more thoroughly. C'mon, Cory. 2024-04-02 20:32:47 -04:00
9f3171e3f1 Remove auth.js. #7 2024-04-02 20:11:36 -04:00
cc92748747 Move sending refresh tokens out of JS. 2024-04-02 12:42:31 -04:00
0a0b0c1adb Make sure we don't leak the session string when reassigning it. 2024-04-02 12:20:59 -04:00
92a74026a6 Format the new auth code. 2024-04-01 12:53:47 -04:00
3fa1c6c420 Tidied up getting an auth key slightly. 2024-04-01 12:53:00 -04:00
b04eccdbda Move the auth handler out of JS. #7 2024-03-31 16:15:50 -04:00
9ce30dee70 Start working on 0.0.18. 2024-03-27 19:08:10 -04:00
40 changed files with 1381 additions and 1055 deletions

View File

@ -3,9 +3,9 @@
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 17 VERSION_CODE := 18
VERSION_NUMBER := 0.0.17 VERSION_NUMBER := 0.0.18-wip
VERSION_NAME := Please enjoy responsibly. VERSION_NAME := Celebrating totality for upwards of 3m1.4s.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450200.zip 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 LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&Xs1X5TzLCk6KVr+5IDc80JAHYxJyoD10cXKBUYpFqWQ=.sha256" "previous": "&sR39JhvUCRlOv7hdEWV81RWWkyoPGLg6u4w+BfqUE9s=.sha256"
} }

View File

@ -1,90 +1,94 @@
function textNode(text) { function textNode(text) {
const node = new commonmark.Node("text", undefined); const node = new commonmark.Node('text', undefined);
node.literal = text; node.literal = text;
return node; return node;
} }
function linkNode(text, link) { function linkNode(text, link) {
const linkNode = new commonmark.Node("link", undefined); const linkNode = new commonmark.Node('link', undefined);
linkNode.destination = `#q=${encodeURIComponent(link)}`; if (link.startsWith('#')) {
linkNode.appendChild(textNode(text)); linkNode.destination = `#q=${encodeURIComponent(link)}`;
return linkNode; } else {
linkNode.destination = link;
}
linkNode.appendChild(textNode(text));
return linkNode;
} }
function splitMatches(text, regexp) { function splitMatches(text, regexp) {
// Regexp must be sticky. // Regexp must be sticky.
regexp = new RegExp(regexp, "gm"); regexp = new RegExp(regexp, 'gm');
let i = 0; let i = 0;
const result = []; const result = [];
let match = regexp.exec(text); let match = regexp.exec(text);
while (match) { while (match) {
const matchText = match[0]; const matchText = match[0];
if (match.index > i) { if (match.index > i) {
result.push([text.substring(i, match.index), false]); result.push([text.substring(i, match.index), false]);
} }
result.push([matchText, true]); result.push([matchText, true]);
i = match.index + matchText.length; i = match.index + matchText.length;
match = regexp.exec(text); match = regexp.exec(text);
} }
if (i < text.length) { if (i < text.length) {
result.push([text.substring(i, text.length), false]); result.push([text.substring(i, text.length), false]);
} }
return result; 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) { function split(textNodes) {
const text = textNodes.map(n => n.literal).join(""); const text = textNodes.map((n) => n.literal).join('');
const parts = splitMatches(text, regex); const parts = splitMatches(text, regex);
return parts.map(part => { return parts.map((part) => {
if (part[1]) { if (part[1]) {
return linkNode(part[0], part[0]); return linkNode(part[0], part[0]);
} else { } else {
return textNode(part[0]); return textNode(part[0]);
} }
}); });
} }
export function transform(parsed) { export function transform(parsed) {
const walker = parsed.walker(); const walker = parsed.walker();
let event; let event;
let nodes = []; let nodes = [];
while ((event = walker.next())) { while ((event = walker.next())) {
const node = event.node; const node = event.node;
if (event.entering && node.type === "text") { if (event.entering && node.type === 'text') {
nodes.push(node); nodes.push(node);
} else { } else {
if (nodes.length > 0) { if (nodes.length > 0) {
split(nodes) split(nodes)
.reverse() .reverse()
.forEach(newNode => { .forEach((newNode) => {
nodes[0].insertAfter(newNode); nodes[0].insertAfter(newNode);
}); });
nodes.forEach(n => n.unlink()); nodes.forEach((n) => n.unlink());
nodes = []; nodes = [];
} }
} }
} }
if (nodes.length > 0) { if (nodes.length > 0) {
split(nodes) split(nodes)
.reverse() .reverse()
.forEach(newNode => { .forEach((newNode) => {
nodes[0].insertAfter(newNode); nodes[0].insertAfter(newNode);
}); });
nodes.forEach(n => n.unlink()); nodes.forEach((n) => n.unlink());
} }
return parsed; return parsed;
} }

View File

@ -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;
}

View File

@ -1,3 +1,5 @@
import * as tfrpc from '/static/tfrpc.js';
let g_emojis; let g_emojis;
function get_emojis() { function get_emojis() {
@ -10,105 +12,151 @@ function get_emojis() {
}); });
} }
export function picker(callback, anchor) { async function get_recent(author) {
get_emojis().then(function (json) { let recent = await tfrpc.rpc.query(`
let div = document.createElement('div'); SELECT DISTINCT content ->> '$.vote.expression' AS value
div.id = 'emoji_picker'; FROM messages
div.style.color = '#000'; WHERE author = ? AND
div.style.background = '#fff'; content ->> '$.type' = 'vote'
div.style.border = '1px solid #000'; ORDER BY timestamp DESC LIMIT 10
div.style.display = 'block'; `, [author]);
div.style.position = 'absolute'; return recent.map(x => x.value);
div.style.minWidth = 'min(16em, 90vw)'; }
div.style.width = 'min(16em, 90vw)';
div.style.maxWidth = 'min(16em, 90vw)';
div.style.maxHeight = '16em';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
let input = document.createElement('input');
input.type = 'text';
input.style.display = 'block';
input.style.boxSizing = 'border-box';
input.style.width = '100%';
input.style.margin = '0';
input.style.position = 'relative';
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
div.addEventListener('mousedown', function (event) {
event.stopPropagation();
});
function cleanup() { export async function picker(callback, anchor, author) {
console.log('emoji cleanup'); let json = await get_emojis();
div.parentElement.removeChild(div); let recent = await get_recent(author);
window.removeEventListener('keydown', key_down);
console.log('removing click');
document.body.removeEventListener('mousedown', cleanup);
}
function key_down(event) { let div = document.createElement('div');
if (event.key == 'Escape') { div.id = 'emoji_picker';
cleanup(); div.style.color = '#000';
} div.style.background = '#fff';
} div.style.border = '1px solid #000';
div.style.display = 'block';
div.style.position = 'absolute';
div.style.minWidth = 'min(16em, 90vw)';
div.style.width = 'min(16em, 90vw)';
div.style.maxWidth = 'min(16em, 90vw)';
div.style.maxHeight = '16em';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
let input = document.createElement('input');
input.type = 'text';
input.style.display = 'block';
input.style.boxSizing = 'border-box';
input.style.width = '100%';
input.style.margin = '0';
input.style.position = 'relative';
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
div.addEventListener('mousedown', function (event) {
event.stopPropagation();
});
function chosen(event) { function cleanup() {
console.log(event.srcElement.innerText); console.log('emoji cleanup');
callback(event.srcElement.innerText); div.parentElement.removeChild(div);
window.removeEventListener('keydown', key_down);
console.log('removing click');
document.body.removeEventListener('mousedown', cleanup);
}
function key_down(event) {
if (event.key == 'Escape') {
cleanup(); cleanup();
} }
}
function refresh() { function chosen(event) {
while (list.firstChild) { console.log(event.srcElement.innerText);
list.removeChild(list.firstChild); callback(event.srcElement.innerText);
} cleanup();
let search = input.value.toLowerCase(); }
let any_at_all = false;
for (let row of Object.entries(json)) { function refresh() {
let header = document.createElement('div'); while (list.firstChild) {
header.appendChild(document.createTextNode(row[0])); list.removeChild(list.firstChild);
list.appendChild(header); }
let any = false; let search = input.value.toLowerCase();
for (let entry of Object.entries(row[1])) { let any_at_all = false;
if ( if (recent) {
search && let emoji_to_name = {};
search.length && for (let row of Object.values(json)) {
entry[0].toLowerCase().indexOf(search) == -1 for (let entry of Object.entries(row)) {
) { emoji_to_name[entry[1]] = entry[0];
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 = entry[0];
emoji.appendChild(document.createTextNode(entry[1]));
list.appendChild(emoji);
any = true;
any_at_all = true;
}
if (!any) {
list.removeChild(header);
} }
} }
if (!any_at_all) { let header = document.createElement('div');
list.appendChild(document.createTextNode('No matches found.')); 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);
} }
} }
refresh(); for (let row of Object.entries(json)) {
input.oninput = refresh; let header = document.createElement('div');
document.body.appendChild(div); header.appendChild(document.createTextNode(row[0]));
div.style.position = 'fixed'; list.appendChild(header);
div.style.top = '50%'; let any = false;
div.style.left = '50%'; for (let entry of Object.entries(row[1])) {
div.style.transform = 'translate(-50%, -50%)'; if (
input.focus(); search &&
console.log('adding click'); search.length &&
document.body.addEventListener('mousedown', cleanup); entry[0].toLowerCase().indexOf(search) == -1
window.addEventListener('keydown', key_down); ) {
}); 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 = entry[0];
emoji.appendChild(document.createTextNode(entry[1]));
list.appendChild(emoji);
any = true;
any_at_all = true;
}
if (!any) {
list.removeChild(header);
}
}
if (!any_at_all) {
list.appendChild(document.createTextNode('No matches found.'));
}
}
refresh();
input.oninput = refresh;
document.body.appendChild(div);
div.style.position = 'fixed';
div.style.top = '50%';
div.style.left = '50%';
div.style.transform = 'translate(-50%, -50%)';
input.focus();
console.log('adding click');
document.body.addEventListener('mousedown', cleanup);
window.addEventListener('keydown', key_down);
} }

View File

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html style="color: #fff"> <html>
<head> <head>
<title>Tilde Friends</title> <title>Tilde Friends</title>
<base target="_top" /> <base target="_top" />
@ -10,14 +10,13 @@
} }
</style> </style>
</head> </head>
<body style="background-color: #223a5e"> <body style="margin: 0; padding: 0">
<tf-app class="w3-deep-purple" /> <tf-app/>
<script> <script>
window.litDisableBundleWarning = true; window.litDisableBundleWarning = true;
</script> </script>
<script src="filesaver.min.js"></script> <script src="filesaver.min.js"></script>
<script src="commonmark.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="commonmark-hashtag.js" type="module"></script>
<script src="script.js" type="module"></script> <script src="script.js" type="module"></script>
</body> </body>

View File

@ -205,7 +205,7 @@ class TfElement extends LitElement {
@change=${this._handle_whoami_changed} @change=${this._handle_whoami_changed}
></tf-id-picker> ></tf-id-picker>
<button <button
class="w3-button w3-dark-grey w3-border" class="w3-button w3-theme-d1 w3-border"
style="flex: 0 0 auto" style="flex: 0 0 auto"
@click=${this.create_identity} @click=${this.create_identity}
id="create_identity" id="create_identity"
@ -352,15 +352,15 @@ class TfElement extends LitElement {
}; };
let tabs = html` let tabs = html`
<div class="w3-bar w3-black"> <div class="w3-bar w3-theme-l1">
${Object.entries(k_tabs).map( ${Object.entries(k_tabs).map(
([k, v]) => html` ([k, v]) => html`
<button <button
title=${v} 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 v
? 'w3-red' ? 'w3-theme-l2'
: 'w3-black'}" : 'w3-theme-l1'}"
@click=${() => self.set_tab(v)} @click=${() => self.set_tab(v)}
> >
${k} ${k}
@ -375,11 +375,15 @@ class TfElement extends LitElement {
: html`<div>Select or create an identity.</div>` : html`<div>Select or create an identity.</div>`
: this.render_tab(); : this.render_tab();
return html` return html`
${this.render_id_picker()} ${tabs} <div style="width: 100vw; min-height: 100vh; height: 100%" class="w3-theme-dark">
${this.tags.map( <div style="padding: 8px">
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>` ${this.render_id_picker()} ${tabs}
)} ${this.tags.map(
${contents} (x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents}
</div>
</div>
`; `;
} }
} }

View File

@ -13,6 +13,7 @@ class TfComposeElement extends LitElement {
branch: {type: String}, branch: {type: String},
apps: {type: Object}, apps: {type: Object},
drafts: {type: Object}, drafts: {type: Object},
author: {type: String},
}; };
} }
@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
this.branch = undefined; this.branch = undefined;
this.apps = undefined; this.apps = undefined;
this.drafts = {}; this.drafts = {};
this.author = undefined;
} }
process_text(text) { process_text(text) {
@ -284,13 +286,25 @@ class TfComposeElement extends LitElement {
} }
firstUpdated() { 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({ let tribute = new Tribute({
collection: [ collection: [
{ {
values: Object.entries(this.users).map((x) => ({ values: values,
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) { selectTemplate: function (item) {
return `[@${item.original.key}](${item.original.value})`; return `[@${item.original.key}](${item.original.value})`;
}, },
@ -342,7 +356,7 @@ class TfComposeElement extends LitElement {
return html` <div style="display: flex; flex-direction: row"> return html` <div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em"> <div style="align-self: center; margin: 0.5em">
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
title="Remove ${mention.name} mention" title="Remove ${mention.name} mention"
@click=${() => self.remove_mention(mention.link)} @click=${() => self.remove_mention(mention.link)}
> >
@ -396,16 +410,16 @@ class TfComposeElement extends LitElement {
if (this.apps) { if (this.apps) {
return html` return html`
<div class="w3-card-4 w3-margin w3-padding"> <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( ${Object.keys(self.apps).map(
(app) => html`<option value=${app}>${app}</option>` (app) => html`<option value=${app}>${app}</option>`
)} )}
</select> </select>
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}> <button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
Attach Attach
</button> </button>
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (this.apps = null)} @click=${() => (this.apps = null)}
> >
Cancel Cancel
@ -421,12 +435,12 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps(); self.apps = await tfrpc.rpc.apps();
} }
if (!this.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 Attach App
</button>`; </button>`;
} else { } else {
return html`<button return html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (this.apps = null)} @click=${() => (this.apps = null)}
> >
Discard App Discard App
@ -448,15 +462,15 @@ class TfComposeElement extends LitElement {
return html` return html`
<div class="w3-container w3-padding"> <div class="w3-container w3-padding">
<p> <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> <label for="cw">CW</label>
</p> </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> </div>
`; `;
} else { } else {
return html` 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> <label for="cw">CW</label>
`; `;
} }
@ -486,14 +500,14 @@ class TfComposeElement extends LitElement {
<div style="display: flex; flex-direction: row; width: 100%"> <div style="display: flex; flex-direction: row; width: 100%">
<label for="encrypt_to">🔐 To:</label> <label for="encrypt_to">🔐 To:</label>
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input> <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> </div>
<ul> <ul>
${draft.encrypt_to.map( ${draft.encrypt_to.map(
(x) => html` (x) => html`
<li> <li>
<tf-user id=${x} .users=${this.users}></tf-user> <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>` </li>`
)} )}
</ul> </ul>
@ -512,7 +526,7 @@ class TfComposeElement extends LitElement {
let draft = self.get_draft(); let draft = self.get_draft();
let content_warning = let content_warning =
draft.content_warning !== undefined 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> <p id="content_warning_preview">${draft.content_warning}</p>
</div>` </div>`
: undefined; : undefined;
@ -520,14 +534,14 @@ class TfComposeElement extends LitElement {
draft.encrypt_to !== undefined draft.encrypt_to !== undefined
? undefined ? undefined
: html`<button : html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => this.set_encrypt([])} @click=${() => this.set_encrypt([])}
> >
🔐 🔐
</button>`; </button>`;
let result = html` let result = html`
<div <div
class="w3-card-4 w3-blue-grey w3-padding" class="w3-card-4 w3-theme-d4 w3-padding"
style="box-sizing: border-box" style="box-sizing: border-box"
> >
${this.render_encrypt()} ${this.render_encrypt()}
@ -535,15 +549,14 @@ class TfComposeElement extends LitElement {
<div style="flex: 1 0 50%"> <div style="flex: 1 0 50%">
<p> <p>
<textarea <textarea
class="w3-input w3-dark-grey w3-border" class="w3-input w3-theme-d1 w3-border"
style="resize: vertical" style="resize: vertical"
placeholder="Write a post here." placeholder="Write a post here."
id="edit" id="edit"
@input=${this.input} @input=${this.input}
@change=${this.change} @change=${this.change}
@paste=${this.paste} @paste=${this.paste}
> >${draft.text}</textarea
${draft.text}</textarea
> >
</p> </p>
</div> </div>
@ -552,22 +565,20 @@ ${draft.text}</textarea
<div id="preview"></div> <div id="preview"></div>
</div> </div>
</div> </div>
${Object.values(draft.mentions || {}).map((x) => ${Object.values(draft.mentions || {}).map((x) => self.render_mention(x))}
self.render_mention(x)
)}
${this.render_attach_app()} ${this.render_content_warning()} ${this.render_attach_app()} ${this.render_content_warning()}
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
id="submit" id="submit"
@click=${this.submit} @click=${this.submit}
> >
Submit Submit
</button> </button>
<button class="w3-button w3-dark-grey" @click=${this.attach}> <button class="w3-button w3-theme-d1" @click=${this.attach}>
Attach Attach
</button> </button>
${this.render_attach_app_button()} ${encrypt} ${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 Discard
</button> </button>
</div> </div>

View File

@ -34,7 +34,7 @@ class TfIdentityPickerElement extends LitElement {
render() { render() {
return html` return html`
<select <select
class="w3-select w3-dark-grey w3-padding w3-border" class="w3-select w3-theme-d1 w3-padding w3-border"
@change=${this.changed} @change=${this.changed}
style="max-width: 100%; overflow: hidden" style="max-width: 100%; overflow: hidden"
> >

View File

@ -125,7 +125,7 @@ class TfMessageElement extends LitElement {
} }
react(event) { react(event) {
emojis.picker((x) => this.vote(x)); emojis.picker((x) => this.vote(x), null, this.whoami);
} }
show_image(link) { show_image(link) {
@ -240,7 +240,7 @@ ${JSON.stringify(mention, null, 2)}</pre
let self = this; let self = this;
return html` return html`
<fieldset <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> <legend>Mentions</legend>
${mentions.map((x) => self.render_mention(x))} ${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.message.child_messages?.length) {
if (!this.expanded[this.message.id]) { if (!this.expanded[this.message.id]) {
return html`<button return html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => self.set_expanded(true)} @click=${() => self.set_expanded(true)}
> >
+ ${this.total_child_messages(this.message) + ' More'} + ${this.total_child_messages(this.message) + ' More'}
</button>`; </button>`;
} else { } else {
return html`<button return html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => self.set_expanded(false)} @click=${() => self.set_expanded(false)}
> >
Collapse</button Collapse</button
@ -337,14 +337,14 @@ ${JSON.stringify(mention, null, 2)}</pre
case 'raw': case 'raw':
if (content?.type == 'post' || content?.type == 'blog') { if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'md')} @click=${() => (self.format = 'md')}
> >
Markdown Markdown
</button>`; </button>`;
} else { } else {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')} @click=${() => (self.format = 'message')}
> >
Message Message
@ -353,7 +353,7 @@ ${JSON.stringify(mention, null, 2)}</pre
break; break;
case 'md': case 'md':
raw_button = html`<button raw_button = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')} @click=${() => (self.format = 'message')}
> >
Message Message
@ -361,7 +361,7 @@ ${JSON.stringify(mention, null, 2)}</pre
break; break;
case 'decrypted': case 'decrypted':
raw_button = html`<button raw_button = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')} @click=${() => (self.format = 'raw')}
> >
Raw Raw
@ -370,14 +370,14 @@ ${JSON.stringify(mention, null, 2)}</pre
default: default:
if (this.message.decrypted) { if (this.message.decrypted) {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'decrypted')} @click=${() => (self.format = 'decrypted')}
> >
Decrypted Decrypted
</button>`; </button>`;
} else { } else {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')} @click=${() => (self.format = 'raw')}
> >
Raw Raw
@ -390,7 +390,7 @@ ${JSON.stringify(mention, null, 2)}</pre
return html` return html`
<div <div
class="w3-card-4" 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> <tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px" <span style="padding-right: 8px"
@ -406,7 +406,7 @@ ${JSON.stringify(mention, null, 2)}</pre
if (this.message?.type === 'contact_group') { if (this.message?.type === 'contact_group') {
return html` <div return html` <div
class="w3-card-4" 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( ${this.message.messages.map(
(x) => (x) =>
@ -422,7 +422,7 @@ ${JSON.stringify(mention, null, 2)}</pre
} else if (this.message.placeholder) { } else if (this.message.placeholder) {
return html` <div return html` <div
class="w3-card-4" 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> <a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
(placeholder) (placeholder)
@ -498,11 +498,12 @@ ${JSON.stringify(mention, null, 2)}</pre
branch=${this.message.id} branch=${this.message.id}
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-discard=${this.discard_reply} @tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose> ></tf-compose>
` `
: html` : html`
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${this.show_reply} @click=${this.show_reply}
> >
Reply Reply
@ -533,7 +534,7 @@ ${JSON.stringify(content, null, 2)}</pre
} }
let content_warning = html` let content_warning = html`
<div <div
class="w3-panel w3-round-xlarge w3-blue" class="w3-panel w3-round-xlarge w3-theme-l4"
style="cursor: pointer" style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')} @click=${(x) => this.toggle_expanded(':cw')}
> >
@ -554,8 +555,8 @@ ${JSON.stringify(content, null, 2)}</pre
? html`<span style="align-self: center">🔓</span>` ? html`<span style="align-self: center">🔓</span>`
: undefined; : undefined;
let style_background = this.message?.decrypted let style_background = this.message?.decrypted
? 'rgba(255, 0, 0, 0.2)' ? 'background-color: rgba(255, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.1)'; : 'backdrop-filter: brightness(1.2)';
return html` return html`
<style> <style>
code { code {
@ -573,7 +574,7 @@ ${JSON.stringify(content, null, 2)}</pre
</style> </style>
<div <div
class="w3-card-4" 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"> <div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <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()} ${payload} ${this.render_votes()}
<p> <p>
${reply} ${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}> <button class="w3-button w3-theme-d1" @click=${this.react}>
React React
</button> </button>
</p> </p>
@ -600,8 +601,8 @@ ${JSON.stringify(content, null, 2)}</pre
? html`<span style="align-self: center">🔓</span>` ? html`<span style="align-self: center">🔓</span>`
: undefined; : undefined;
let style_background = this.message?.decrypted let style_background = this.message?.decrypted
? 'rgba(255, 0, 0, 0.2)' ? 'background: rgba(255, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.1)'; : 'backdrop-filter: brightness(1.2)';
return html` return html`
<style> <style>
code { code {
@ -619,7 +620,7 @@ ${JSON.stringify(content, null, 2)}</pre
</style> </style>
<div <div
class="w3-card-4" 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"> <div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
@ -633,7 +634,7 @@ ${JSON.stringify(content, null, 2)}</pre
</div> </div>
${content.text} ${this.render_votes()} ${content.text} ${this.render_votes()}
<p> <p>
<button class="w3-button w3-dark-grey" @click=${this.react}> <button class="w3-button w3-theme-d1" @click=${this.react}>
React React
</button> </button>
</p> </p>
@ -685,11 +686,12 @@ ${JSON.stringify(content, null, 2)}</pre
branch=${this.message.id} branch=${this.message.id}
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-discard=${this.discard_reply} @tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose> ></tf-compose>
` `
: html` : html`
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${this.show_reply} @click=${this.show_reply}
> >
Reply Reply
@ -712,7 +714,7 @@ ${JSON.stringify(content, null, 2)}</pre
</style> </style>
<div <div
class="w3-card-4" 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"> <div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
@ -728,7 +730,7 @@ ${JSON.stringify(content, null, 2)}</pre
${this.render_mentions()} ${this.render_mentions()}
<div> <div>
${reply} ${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}> <button class="w3-button w3-theme-d1" @click=${this.react}>
React React
</button> </button>
</div> </div>

View File

@ -215,49 +215,49 @@ class TfProfileElement extends LitElement {
let server_follow; let server_follow;
if (this.server_follows_me === true) { if (this.server_follows_me === true) {
server_follow = html`<button server_follow = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(false)} @click=${() => this.server_follow_me(false)}
> >
Server, Stop Following Me Server, Stop Following Me
</button>`; </button>`;
} else if (this.server_follows_me === false) { } else if (this.server_follows_me === false) {
server_follow = html`<button server_follow = html`<button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(true)} @click=${() => this.server_follow_me(true)}
> >
Server, Follow Me Server, Follow Me
</button>`; </button>`;
} }
edit = html` 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 Save Profile
</button> </button>
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}> <button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard Discard
</button> </button>
${server_follow} ${server_follow}
`; `;
} else { } 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 Edit Profile
</button>`; </button>`;
} }
} }
if (this.id !== this.whoami && this.following !== undefined) { if (this.id !== this.whoami && this.following !== undefined) {
follow = this.following 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 Unfollow
</button>` </button>`
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}> : html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
Follow Follow
</button>`; </button>`;
} }
if (this.id !== this.whoami && this.blocking !== undefined) { if (this.id !== this.whoami && this.blocking !== undefined) {
block = this.blocking 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 Unblock
</button>` </button>`
: html`<button class="w3-button w3-dark-grey" @click=${this.block}> : html`<button class="w3-button w3-theme-d1" @click=${this.block}>
Block Block
</button>`; </button>`;
} }
@ -267,16 +267,16 @@ class TfProfileElement extends LitElement {
<div class="w3-container"> <div class="w3-container">
<div> <div>
<label for="name">Name:</label> <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>
<div><label for="description">Description:</label></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> <div>
<label for="public_web_hosting">Public Web Hosting:</label> <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>
<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> </div>
</div>` </div>`

View File

@ -1,17 +1,6 @@
import {css} from './lit-all.min.js'; import {css} from './lit-all.min.js';
const tf = css` const tf = css`
a:link {
color: #bbf;
}
a:visited {
color: #ddd;
}
a:hover {
color: #ddf;
}
img { img {
max-width: min(640px, 100%); 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];

View File

@ -61,7 +61,7 @@ class TfTabConnectionsElement extends LitElement {
return html` return html`
<li> <li>
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
> >
Connect Connect
@ -75,7 +75,7 @@ class TfTabConnectionsElement extends LitElement {
return html` return html`
<li> <li>
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.connect(connection)} @click=${() => tfrpc.rpc.connect(connection)}
> >
Connect Connect
@ -94,7 +94,7 @@ class TfTabConnectionsElement extends LitElement {
render_connection(connection) { render_connection(connection) {
return html` return html`
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)} @click=${() => tfrpc.rpc.closeConnection(connection.id)}
> >
Close Close
@ -117,9 +117,9 @@ class TfTabConnectionsElement extends LitElement {
return html` return html`
<div class="w3-container"> <div class="w3-container">
<h2>New Connection</h2> <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 <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => @click=${() =>
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
> >
@ -143,13 +143,13 @@ class TfTabConnectionsElement extends LitElement {
(x) => html` (x) => html`
<li> <li>
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => self.forget_stored_connection(x)} @click=${() => self.forget_stored_connection(x)}
> >
Forget Forget
</button> </button>
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.connect(x)} @click=${() => tfrpc.rpc.connect(x)}
> >
Connect Connect

View File

@ -187,7 +187,7 @@ class TfTabNewsFeedElement extends LitElement {
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) { if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
more = html` more = html`
<p> <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 Load More
</button> </button>
</p> </p>

View File

@ -119,7 +119,7 @@ class TfTabNewsElement extends LitElement {
return html` return html`
<p class="w3-bar"> <p class="w3-bar">
<button <button
class="w3-bar-item w3-button w3-dark-grey" class="w3-bar-item w3-button w3-theme-d1"
@click=${this.show_more} @click=${this.show_more}
> >
${this.new_messages_text()} ${this.new_messages_text()}

View File

@ -110,14 +110,14 @@ class TfTabQueryElement extends LitElement {
<textarea <textarea
id="search" id="search"
rows="8" rows="8"
class="w3-input w3-dark-grey" class="w3-input w3-theme-d1"
style="flex: 1; resize: vertical" style="flex: 1; resize: vertical"
@keydown=${this.search_keydown} @keydown=${this.search_keydown}
> >
${this.query}</textarea ${this.query}</textarea
> >
<button <button
class="w3-button w3-dark-grey" class="w3-button w3-theme-d1"
@click=${(event) => @click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)} self.search(self.renderRoot.getElementById('search').value)}
> >

View File

@ -78,8 +78,8 @@ class TfTabSearchElement extends LitElement {
let self = this; let self = this;
return html` return html`
<div style="display: flex; flex-direction: row; gap: 4px"> <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> <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-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button> <button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div> </div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news> <tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
`; `;

View File

@ -1,4 +1,3 @@
import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.js'; import * as hashtagify from './commonmark-hashtag.js';
function image(node, entering) { function image(node, entering) {
@ -67,7 +66,6 @@ export function markdown(md) {
writer.image = image; writer.image = image;
let parsed = reader.parse(md || ''); let parsed = reader.parse(md || '');
parsed = hashtagify.transform(parsed); parsed = hashtagify.transform(parsed);
parsed = linkify.transform(parsed);
let walker = parsed.walker(); let walker = parsed.walker();
let event, node; let event, node;
while ((event = walker.next())) { while ((event = walker.next())) {

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "📝", "emoji": "📝",
"previous": "&DnfuAUGzzalSh9NgZXnzDc9Ru5aM0omfRJ4h27jYw4k=.sha256" "previous": "&hTgvoLzZ+6dlqYL7zRDAa91TzptTquAY8MaZ7Gn36Cc=.sha256"
} }

View File

@ -96,7 +96,7 @@ export async function collection(
let rows = []; let rows = [];
await ssb.sqlAsync( await ssb.sqlAsync(
` `
SELECT messages.id, author, content, timestamp SELECT messages.id, author, json(content) AS content, timestamp
FROM messages FROM messages
JOIN json_each(?1) AS id ON messages.author = id.value JOIN json_each(?1) AS id ON messages.author = id.value
WHERE WHERE

View File

@ -1,4 +1,3 @@
import * as auth from './auth.js';
import * as core from './core.js'; import * as core from './core.js';
let g_next_id = 1; let g_next_id = 1;
@ -87,8 +86,7 @@ App.prototype.send = function (message) {
function socket(request, response, client) { function socket(request, response, client) {
let process; let process;
let options = {}; let options = {};
let credentials = auth.query(request.headers); let credentials = httpd.auth_query(request.headers);
let refresh = auth.makeRefresh(credentials);
response.onClose = async function () { response.onClose = async function () {
if (process && process.task) { if (process && process.task) {
@ -241,14 +239,7 @@ function socket(request, response, client) {
} }
}; };
response.upgrade( response.upgrade(100, {});
100,
refresh
? {
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
}
: {}
);
} }
export {socket, App}; export {socket, App};

View File

@ -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};

View File

@ -1,5 +1,4 @@
import * as app from './app.js'; import * as app from './app.js';
import * as auth from './auth.js';
import * as form from './form.js'; import * as form from './form.js';
import * as http from './http.js'; import * as http from './http.js';
@ -967,7 +966,7 @@ async function useAppHandler(
}, },
respond: do_resolve, respond: do_resolve,
}, },
credentials: auth.query(headers), credentials: httpd.auth_query(headers),
packageOwner: packageOwner, packageOwner: packageOwner,
packageName: packageName, packageName: packageName,
} }
@ -1098,7 +1097,7 @@ async function blobHandler(request, response, blobId, uri) {
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let user = match[1]; let user = match[1];
let appName = match[2]; let appName = match[2];
let credentials = auth.query(request.headers); let credentials = httpd.auth_query(request.headers);
if ( if (
credentials && credentials &&
credentials.session && credentials.session &&
@ -1161,7 +1160,7 @@ async function blobHandler(request, response, blobId, uri) {
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let user = match[1]; let user = match[1];
let appName = match[2]; let appName = match[2];
let credentials = auth.query(request.headers); let credentials = https.auth_query(request.headers);
if ( if (
credentials && credentials &&
credentials.session && credentials.session &&
@ -1334,8 +1333,6 @@ loadSettings()
if (tildefriends.https_port && gGlobalSettings.http_redirect) { if (tildefriends.https_port && gGlobalSettings.http_redirect) {
httpd.set_http_redirect(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('/app/socket', app.socket);
httpd.all('', function default_http_handler(request, response) { httpd.all('', function default_http_handler(request, response) {
let match; let match;

View File

@ -15,22 +15,6 @@ body {
margin: 0; margin: 0;
} }
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
#logo { #logo {
vertical-align: middle; vertical-align: middle;
} }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends" package="com.unprompted.tildefriends"
android:versionCode="17" android:versionCode="18"
android:versionName="0.0.17"> android:versionName="0.0.18-wip">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/> <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<application <application

View File

@ -1030,3 +1030,46 @@ void* tf_http_get_user_data(tf_http_t* http)
{ {
return http->user_data; 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;
}

View File

@ -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); 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. ** Send a websocket message.
** @param request The HTTP request which was previously updated to a websocket ** @param request The HTTP request which was previously updated to a websocket

View File

@ -4,16 +4,24 @@
#include "http.h" #include "http.h"
#include "log.h" #include "log.h"
#include "mem.h" #include "mem.h"
#include "ssb.h"
#include "ssb.db.h"
#include "task.h" #include "task.h"
#include "tlscontext.js.h" #include "tlscontext.js.h"
#include "trace.h" #include "trace.h"
#include "util.js.h" #include "util.js.h"
#include "ow-crypt.h"
#include "picohttpparser.h" #include "picohttpparser.h"
#include "sodium/crypto_sign.h"
#include "sodium/utils.h"
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
#include <openssl/sha.h> #include <openssl/sha.h>
@ -23,7 +31,12 @@
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a)))) #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 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_class_id;
static JSClassID _httpd_request_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[headers_count * 2 + 1] = key;
headers_count++; 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; 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) if (send_version)
{ {
@ -330,6 +359,8 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
JS_FreeCString(context, headers[i * 2 + 1]); JS_FreeCString(context, headers[i * 2 + 1]);
} }
tf_free((void*)cookie);
request->on_message = _httpd_message_callback; request->on_message = _httpd_message_callback;
request->on_close = _httpd_websocket_close_callback; request->on_close = _httpd_websocket_close_callback;
request->context = context; request->context = context;
@ -410,6 +441,58 @@ static JSValue _httpd_set_http_redirect(JSContext* context, JSValueConst this_va
return JS_UNDEFINED; 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) static void _httpd_finalizer(JSRuntime* runtime, JSValue value)
{ {
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id); 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); 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) void tf_httpd_register(JSContext* context)
{ {
JS_NewClassID(&_httpd_class_id); 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, "/mem", _httpd_endpoint_mem, NULL, task);
tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, 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, "handlers", JS_NewObject(context));
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2)); 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, "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, "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_SetPropertyStr(context, global, "httpd", httpd);
JS_FreeValue(context, global); JS_FreeValue(context, global);
} }

View File

@ -516,7 +516,7 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
if (args.count == 1) if (args.count == 1)
{ {
_tf_run_task(&args, 0); result = _tf_run_task(&args, 0);
} }
if (args.count > 1) if (args.count > 1)
{ {

View File

@ -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_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0);
uv_unref((uv_handle_t*)&timer->timer); 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;
}

View File

@ -6,6 +6,7 @@
#include "trace.h" #include "trace.h"
#include "util.js.h" #include "util.js.h"
#include "ow-crypt.h"
#include "sodium/crypto_hash_sha256.h" #include "sodium/crypto_hash_sha256.h"
#include "sodium/crypto_sign.h" #include "sodium/crypto_sign.h"
#include "sqlite3.h" #include "sqlite3.h"
@ -1060,8 +1061,14 @@ bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_pub
tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private)); tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private));
if (tf_ssb_db_identity_add(ssb, user, public, private)) if (tf_ssb_db_identity_add(ssb, user, public, private))
{ {
tf_ssb_id_str_to_bin(out_public_key, public); if (out_public_key)
tf_ssb_id_str_to_bin(out_private_key, private); {
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; 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); sqlite3_finalize(statement);
} }
else
{
tf_printf("Prepare failed: %s.\n", sqlite3_errmsg(db));
}
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
return success; 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); 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;
}

View File

@ -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 ssb The SSB instance.
** @param user The user's username. ** @param user The user's username.
** @param[out] out_public_key A buffer populated with the new public key. ** @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. ** @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); 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); 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. ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
** @param user_data User data registered with the authorizer. ** @param user_data User data registered with the authorizer.

View File

@ -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); 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);
/** @} */ /** @} */

View File

@ -1281,78 +1281,6 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th
return result; 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) static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED; 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, "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, "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, "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, "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)); JS_SetPropertyStr(context, object, "privateMessageDecrypt", JS_NewCFunction(context, _tf_ssb_private_message_decrypt, "privateMessageDecrypt", 3));
/* Write. */ /* Write. */

View File

@ -113,8 +113,8 @@ typedef struct _tf_task_t
bool _trusted; bool _trusted;
bool _one_proc; bool _one_proc;
bool _killed; bool _killed;
int32_t _exitCode;
char _scriptName[256]; char _scriptName[256];
int _global_exception_count;
JSRuntime* _runtime; JSRuntime* _runtime;
JSContext* _context; JSContext* _context;
@ -421,11 +421,11 @@ int tf_task_execute(tf_task_t* task, const char* fileName)
if (source) if (source)
{ {
JSValue result = JS_Eval(task->_context, source, strlen(source), fileName, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_ASYNC); 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"); tf_printf("Reported an error.\n");
} }
if (!JS_IsError(task->_context, result) && !JS_IsException(result)) else
{ {
executed = true; executed = true;
} }
@ -1457,6 +1457,8 @@ static void _tf_task_promise_rejection_tracker(JSContext* context, JSValueConst
if (!is_handled) if (!is_handled)
{ {
tf_util_report_error(context, reason); tf_util_report_error(context, reason);
tf_task_t* task = tf_task_get(context);
task->_global_exception_count++;
} }
} }

View File

@ -443,7 +443,7 @@ JSValue tf_taskstub_kill(tf_taskstub_t* stub)
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
if (!tf_task_get_one_proc(stub->_owner)) if (!tf_task_get_one_proc(stub->_owner))
{ {
uv_process_kill(&stub->_process, SIGTERM); uv_process_kill(&stub->_process, SIGKILL);
} }
else else
{ {

View File

@ -58,6 +58,20 @@ static void _test_nop(const tf_test_options_t* options)
assert(WEXITSTATUS(result) == 0); 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__) #if !defined(__HAIKU__)
static void _test_sandbox(const tf_test_options_t* options) 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); result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WIFEXITED(result)); assert(WIFEXITED(result));
assert(WEXITSTATUS(result) == 0); assert(WEXITSTATUS(result) != 0);
unlink("out/test.js"); unlink("out/test.js");
unlink("out/required.js"); unlink("out/required.js");
@ -606,36 +620,7 @@ static void _test_file(const tf_test_options_t* options)
"});\n"); "});\n");
char command[256]; 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));
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);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); 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) static void _test_http_thread(void* data)
{ {
test_http_t* test = 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"); int r = system("curl -v http://localhost:23456/404");
assert(WEXITSTATUS(r) == 0); assert(WEXITSTATUS(r) == 0);
tf_printf("curl returned %d\n", WEXITSTATUS(r)); 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_id", tf_ssb_test_id_conversion, false);
_tf_test_run(options, "ssb_following", tf_ssb_test_following, false); _tf_test_run(options, "ssb_following", tf_ssb_test_following, false);
_tf_test_run(options, "nop", _test_nop, false); _tf_test_run(options, "nop", _test_nop, false);
_tf_test_run(options, "exception", _test_exception, false);
#if !defined(__HAIKU__) #if !defined(__HAIKU__)
_tf_test_run(options, "sandbox", _test_sandbox, false); _tf_test_run(options, "sandbox", _test_sandbox, false);
#endif #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, "float", _test_float, false);
_tf_test_run(options, "socket", _test_socket, false); _tf_test_run(options, "socket", _test_socket, false);
_tf_test_run(options, "file", _test_file, 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, "b64", _test_b64, false);
_tf_test_run(options, "rooms", tf_ssb_test_rooms, false); _tf_test_run(options, "rooms", tf_ssb_test_rooms, false);
_tf_test_run(options, "bench", tf_ssb_test_bench, false); _tf_test_run(options, "bench", tf_ssb_test_bench, false);

View File

@ -244,67 +244,6 @@ bool tf_util_report_error(JSContext* context, JSValue value)
return is_error; 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) static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
@ -365,16 +304,6 @@ static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val
return result; 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 tf_util_new_uint8_array(JSContext* context, const uint8_t* data, size_t size)
{ {
JSValue array_buffer = JS_NewArrayBufferCopy(context, data, 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; 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) void tf_util_register(JSContext* context)
{ {
JSValue global = JS_GetGlobalObject(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, "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, "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, "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, "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); JS_FreeValue(context, global);
} }

View File

@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.17" #define VERSION_NUMBER "0.0.18-wip"
#define VERSION_NAME "Please enjoy responsibly." #define VERSION_NAME "Celebrating totality for upwards of 3m1.4s."

View File

@ -28,7 +28,7 @@ try:
driver.get('http://localhost:8888') 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-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, '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, '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, 'confirm').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, 'loginButton').click()
@ -39,7 +39,13 @@ try:
driver.switch_to.default_content() driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) # 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() wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
driver.switch_to.alert.accept() driver.switch_to.alert.accept()
@ -71,7 +77,13 @@ try:
driver.switch_to.default_content() driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) # StaleElementReferenceException
while True:
try:
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
break
except:
pass
# NoSuchShadowRootException # NoSuchShadowRootException
while True: while True:
try: try:
@ -89,15 +101,15 @@ try:
driver.switch_to.default_content() driver.switch_to.default_content()
driver.find_element(By.ID, 'allow').click() 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, '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, '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, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) 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, 'guest_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').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-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, '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, '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, '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, '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, '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, '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, '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, 'confirm').send_keys('wrong_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, '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, '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, '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, '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, '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, 'confirm').send_keys('new_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, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) 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, '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, '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, '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, '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, '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, 'password').send_keys('new_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, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))