29 Commits

Author SHA1 Message Date
7997a739ab ssb.createIdentity without hitting the database from the main thread. 2024-06-12 20:47:48 -04:00
248b258413 Make database.getAll() not block the main thread on database access. 2024-06-12 20:29:39 -04:00
0423ed7fb4 Login without hitting the DB from the main thread. 2024-06-12 20:12:35 -04:00
c29378c2f8 Yes, curl, follow redirects. 2024-06-10 21:19:06 -04:00
163fbd85e7 Fix docs. 2024-06-10 20:23:11 -04:00
58bb86ebe1 Make http.auth_query async and get its DB work off the main thread. 2024-06-10 20:22:28 -04:00
c5140ee8e8 Move DB work for ssb.getIdentities() and ssb.getAllIdentities() off the main thread. 2024-06-10 17:18:29 -04:00
6270fd8118 We don't need to go to the DB to get our public key. 2024-06-10 16:56:21 -04:00
3fff706848 Get the code of conduct and JWT signing key without hitting the database from the main thread. 2024-06-10 16:37:12 -04:00
c259defab5 Move database.get and database.set off the main thread. 2024-06-10 15:30:14 -04:00
e5fee5c306 Buildfix. 2024-06-10 12:01:49 -04:00
9d35b4bdfb Resuming work to move all DB access off the main thread. 2024-06-10 11:45:20 -04:00
9497d7cf64 Fix some shutdown hangs/leaks. 2024-06-06 20:31:24 -04:00
c7d3e602cb Fix &-mentions while I'm at it. 2024-06-06 20:14:00 -04:00
0076eb4ed4 Fix autocomplete again/more. #65 2024-06-06 20:05:24 -04:00
6070bde413 Avoid a null dereference. 2024-06-06 19:57:36 -04:00
c7a6d426f0 Fix autocomplete on Chrome, because contenteditable and shadowRoots are tricksy, and this module for @mentions is aging. #65 2024-06-06 19:52:37 -04:00
f66cf0f802 Unused. 2024-06-06 19:11:48 -04:00
e4b6c81024 No need to show your identity in the navigation bar if you have a name. 2024-06-06 18:51:40 -04:00
44d784cd04 OpenSSL 3.3.1. 2024-06-05 12:51:26 -04:00
0394201113 Merge pull request 'buld(nix): Misc Nix-related improvements' (#68) from tasiaiso/tildefriends:tasiaiso-nix-misc into main
Reviewed-on: #68
2024-06-04 20:16:15 -04:00
e270c16516 lit 3.1.4. 2024-06-04 20:10:27 -04:00
4c10538632 buld(nix): Misc Nix-related improvements
- Nixpkgs 23.11 is deprecated, use 24.05 instead
- update flake.lock
- add glibc as a build dependency
- add doxygen and graphviz as development dependencies for `make format`
2024-06-04 15:22:18 +02:00
71329c5532 format+prettier 2024-06-03 12:36:34 -04:00
feb4bf9e87 Limit message sends in a continued attempt to fix intermittent runaway memory usage. #64 2024-06-02 12:38:12 -04:00
5d5567e94c Reworking the emoji picker to use w3-modal, in a step toward doing the same for the currently broken @autocomplete. 2024-05-30 12:40:21 -04:00
684e6fb9cb Merge pull request 'nix: update version to 0.0.19' (#66) from tasiaiso/tildefriends:tasiaiso-nix-update into main
Reviewed-on: #66
2024-05-30 12:12:45 -04:00
ee21fa6d03 nix: update version to 0.0.19 2024-05-30 11:34:57 +02:00
7a2974e54f Working on 0.0.20. 2024-05-29 20:17:33 -04:00
43 changed files with 903 additions and 506 deletions

View File

@ -2,6 +2,7 @@ node_modules
src src
deps deps
.clang-format .clang-format
flake.lock
# Minified files # Minified files
**/*.min.css **/*.min.css

View File

@ -3,9 +3,9 @@
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 19 VERSION_CODE := 20
VERSION_NUMBER := 0.0.19 VERSION_NUMBER := 0.0.20-wip
VERSION_NAME := Don't let your loyalty become a burden. VERSION_NAME := One word all lowercase four words all uppercase.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460000.zip SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460000.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&YhfwSB0+2jmPcHlxOXN73/81H5VEyQ1MaTJmWZbwqHU=.sha256" "previous": "&z0N6jlqflRd4+grj16K/IdllNVLQrPLbr7aKVs/21mE=.sha256"
} }

View File

@ -1,4 +1,6 @@
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {html, render} from './lit-all.min.js';
import {styles} from './tf-styles.js';
let g_emojis; let g_emojis;
@ -36,11 +38,6 @@ export async function picker(callback, anchor, author) {
div.style.background = '#fff'; div.style.background = '#fff';
div.style.border = '1px solid #000'; div.style.border = '1px solid #000';
div.style.display = 'block'; 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.overflow = 'scroll';
div.style.fontWeight = 'bold'; div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large'; div.style.fontSize = 'xx-large';
@ -58,14 +55,6 @@ export async function picker(callback, anchor, author) {
event.stopPropagation(); event.stopPropagation();
}); });
function cleanup() {
console.log('emoji cleanup');
div.parentElement.removeChild(div);
window.removeEventListener('keydown', key_down);
console.log('removing click');
document.body.removeEventListener('mousedown', cleanup);
}
function key_down(event) { function key_down(event) {
if (event.key == 'Escape') { if (event.key == 'Escape') {
cleanup(); cleanup();
@ -153,13 +142,23 @@ export async function picker(callback, anchor, author) {
} }
refresh(); refresh();
input.oninput = refresh; input.oninput = refresh;
document.body.appendChild(div); let modal = html`
div.style.position = 'fixed'; <style>
div.style.top = '50%'; ${styles}
div.style.left = '50%'; </style>
div.style.transform = 'translate(-50%, -50%)'; <div class="w3-modal" style="display: block">
<div class="w3-modal-content w3-card-4">${div}</div>
</div>
`;
let parent = document.createElement('div');
document.body.appendChild(parent);
function cleanup() {
parent.parentElement.removeChild(parent);
window.removeEventListener('keydown', key_down);
document.body.removeEventListener('mousedown', cleanup);
}
render(modal, parent);
input.focus(); input.focus();
console.log('adding click');
document.body.addEventListener('mousedown', cleanup); document.body.addEventListener('mousedown', cleanup);
window.addEventListener('keydown', key_down); window.addEventListener('keydown', key_down);
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -253,9 +253,9 @@ class TfComposeElement extends LitElement {
try { try {
let rows = await tfrpc.rpc.query( let rows = await tfrpc.rpc.query(
` `
SELECT json(messages.content) FROM messages_fts(?) SELECT json(messages.content) AS content FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
WHERE messages.content LIKE ? WHERE json(messages.content) LIKE ?
ORDER BY timestamp DESC LIMIT 10 ORDER BY timestamp DESC LIMIT 10
`, `,
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`] ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
@ -291,6 +291,7 @@ class TfComposeElement extends LitElement {
); );
} }
let tribute = new Tribute({ let tribute = new Tribute({
iframe: this.shadowRoot,
collection: [ collection: [
{ {
values: values, values: values,
@ -325,6 +326,7 @@ class TfComposeElement extends LitElement {
let encrypt = this.renderRoot.getElementById('encrypt_to'); let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) { if (encrypt) {
let tribute = new Tribute({ let tribute = new Tribute({
iframe: this.shadowRoot,
values: Object.entries(this.users).map((x) => ({ values: Object.entries(this.users).map((x) => ({
key: x[1].name, key: x[1].name,
value: x[0], value: x[0],

View File

@ -482,16 +482,7 @@ class TributeRange {
} }
getDocument() { getDocument() {
let iframe; return document;
if (this.tribute.current.collection) {
iframe = this.tribute.current.collection.iframe;
}
if (!iframe) {
return document
}
return iframe.contentWindow.document
} }
positionMenuAtCaret(scrollTo) { positionMenuAtCaret(scrollTo) {
@ -653,8 +644,8 @@ class TributeRange {
} }
getWindowSelection() { getWindowSelection() {
if (this.tribute.collection.iframe) { if (this.tribute.collection[0].iframe?.getSelection) {
return this.tribute.collection.iframe.contentWindow.getSelection() return this.tribute.collection[0].iframe.getSelection()
} }
return window.getSelection() return window.getSelection()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -83,10 +83,10 @@ App.prototype.send = function (message) {
* @param {*} response * @param {*} response
* @param {*} client * @param {*} client
*/ */
function socket(request, response, client) { async function socket(request, response, client) {
let process; let process;
let options = {}; let options = {};
let credentials = httpd.auth_query(request.headers); let credentials = await httpd.auth_query(request.headers);
response.onClose = async function () { response.onClose = async function () {
if (process && process.task) { if (process && process.task) {
@ -222,7 +222,7 @@ function socket(request, response, client) {
} else if (message.action == 'setActiveIdentity') { } else if (message.action == 'setActiveIdentity') {
process.setActiveIdentity(message.identity); process.setActiveIdentity(message.identity);
} else if (message.action == 'createIdentity') { } else if (message.action == 'createIdentity') {
process.createIdentity(); await process.createIdentity();
} else if (message.message == 'tfrpc') { } else if (message.message == 'tfrpc') {
if (message.id && g_calls[message.id]) { if (message.id && g_calls[message.id]) {
if (message.error !== undefined) { if (message.error !== undefined) {

View File

@ -170,16 +170,12 @@ class TfNavigationElement extends LitElement {
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
@click=${self.toggle_id_dropdown} @click=${self.toggle_id_dropdown}
> >
${self.names[this.identity]}${self.names[this.identity] === ${self.names[this.identity]}
this.identity
? ''
: html` - ${this.identity}`}
</button> </button>
<div <div
id="id_dropdown" id="id_dropdown"
class="w3-dropdown-content w3-bar-block w3-card-4" class="w3-dropdown-content w3-bar-block w3-card-4"
style="max-width: 100%" style="max-width: 100%; right: 0"
> >
<button <button
class="w3-bar-item w3-button w3-border" class="w3-bar-item w3-button w3-border"

View File

@ -206,7 +206,7 @@ function getUser(caller, process) {
* @param {*} process * @param {*} process
* @returns * @returns
*/ */
function getApps(user, process) { async function getApps(user, process) {
if ( if (
process.credentials && process.credentials &&
process.credentials.session && process.credentials.session &&
@ -221,10 +221,12 @@ function getApps(user, process) {
if (user) { if (user) {
let db = new Database(user); let db = new Database(user);
try { try {
let names = JSON.parse(db.get('apps')); let names = JSON.parse(await db.get('apps'));
return Object.fromEntries( let result = {};
names.map((name) => [name, db.get('path:' + name)]) for (let name of names) {
); result[name] = await db.get('path:' + name);
}
return result;
} catch {} } catch {}
} }
return {}; return {};
@ -320,9 +322,9 @@ async function getProcessBlob(blobId, key, options) {
} }
}, },
user: getUser(process, process), user: getUser(process, process),
users: function () { users: async function () {
try { try {
return JSON.parse(new Database('auth').get('users')); return JSON.parse(await new Database('auth').get('users'));
} catch { } catch {
return []; return [];
} }
@ -470,7 +472,7 @@ async function getProcessBlob(blobId, key, options) {
process.credentials.session.name && process.credentials.session.name &&
process.credentials.session.name !== 'guest' process.credentials.session.name !== 'guest'
) { ) {
let id = ssb.createIdentity(process.credentials.session.name); let id = await ssb.createIdentity(process.credentials.session.name);
await process.sendIdentities(); await process.sendIdentities();
broadcastAppEventToUser( broadcastAppEventToUser(
process?.credentials?.session?.name, process?.credentials?.session?.name,
@ -509,25 +511,20 @@ async function getProcessBlob(blobId, key, options) {
setGlobalSettings(gGlobalSettings); setGlobalSettings(gGlobalSettings);
print('Done.'); print('Done.');
}; };
imports.core.deleteUser = function (user) { imports.core.deleteUser = async function (user) {
return Promise.resolve( await imports.core.permissionTest('delete_user')
imports.core.permissionTest('delete_user')
).then(function () {
let db = new Database('auth'); let db = new Database('auth');
db.remove('user:' + user); db.remove('user:' + user);
let users = new Set(); let users = new Set();
let users_original = db.get('users'); let users_original = await db.get('users');
try { try {
users = new Set(JSON.parse(users_original)); users = new Set(JSON.parse(users_original));
} catch {} } catch {}
users.delete(user); users.delete(user);
users = JSON.stringify([...users].sort()); users = JSON.stringify([...users].sort());
if (users !== users_original) { if (users !== users_original) {
db.set('users', users); await db.set('users', users);
} }
});
}; };
} }
if (options.api) { if (options.api) {
@ -806,33 +803,15 @@ async function getProcessBlob(blobId, key, options) {
* @param {*} settings * @param {*} settings
* @returns * @returns
*/ */
function setGlobalSettings(settings) { async function setGlobalSettings(settings) {
gGlobalSettings = settings; gGlobalSettings = settings;
try { try {
return new Database('core').set('settings', JSON.stringify(settings)); return await new Database('core').set('settings', JSON.stringify(settings));
} catch (error) { } catch (error) {
print('Error storing settings:', error); print('Error storing settings:', error);
} }
} }
/**
* TODOC
* @param {*} data
* @param {*} bytes
* @returns
*/
function startsWithBytes(data, bytes) {
if (data.byteLength >= bytes.length) {
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
for (let i = 0; i < bytes.length; i++) {
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
return;
}
}
return true;
}
}
/** /**
* TODOC * TODOC
* @param {*} response * @param {*} response
@ -929,7 +908,7 @@ async function useAppHandler(
}, },
respond: do_resolve, respond: do_resolve,
}, },
credentials: httpd.auth_query(headers), credentials: await httpd.auth_query(headers),
packageOwner: packageOwner, packageOwner: packageOwner,
packageName: packageName, packageName: packageName,
} }
@ -1060,7 +1039,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 = httpd.auth_query(request.headers); let credentials = await httpd.auth_query(request.headers);
if ( if (
credentials && credentials &&
credentials.session && credentials.session &&
@ -1070,7 +1049,7 @@ async function blobHandler(request, response, blobId, uri) {
let database = new Database(user); let database = new Database(user);
let app_object = JSON.parse(utf8Decode(request.body)); let app_object = JSON.parse(utf8Decode(request.body));
let previous_id = database.get('path:' + appName); let previous_id = await database.get('path:' + appName);
if (previous_id) { if (previous_id) {
try { try {
let previous_object = JSON.parse( let previous_object = JSON.parse(
@ -1091,7 +1070,7 @@ async function blobHandler(request, response, blobId, uri) {
let newBlobId = await ssb.blobStore(JSON.stringify(app_object)); let newBlobId = await ssb.blobStore(JSON.stringify(app_object));
let apps = new Set(); let apps = new Set();
let apps_original = database.get('apps'); let apps_original = await database.get('apps');
try { try {
apps = new Set(JSON.parse(apps_original)); apps = new Set(JSON.parse(apps_original));
} catch {} } catch {}
@ -1100,9 +1079,9 @@ async function blobHandler(request, response, blobId, uri) {
} }
apps = JSON.stringify([...apps].sort()); apps = JSON.stringify([...apps].sort());
if (apps != apps_original) { if (apps != apps_original) {
database.set('apps', apps); await database.set('apps', apps);
} }
database.set('path:' + appName, newBlobId); await database.set('path:' + appName, newBlobId);
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}); response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('/' + newBlobId); response.end('/' + newBlobId);
} else { } else {
@ -1123,7 +1102,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 = httpd.auth_query(request.headers); let credentials = await httpd.auth_query(request.headers);
if ( if (
credentials && credentials &&
credentials.session && credentials.session &&
@ -1133,10 +1112,10 @@ async function blobHandler(request, response, blobId, uri) {
let database = new Database(user); let database = new Database(user);
let apps = new Set(); let apps = new Set();
try { try {
apps = new Set(JSON.parse(database.get('apps'))); apps = new Set(JSON.parse(await database.get('apps')));
} catch {} } catch {}
if (apps.delete(appName)) { if (apps.delete(appName)) {
database.set('apps', JSON.stringify([...apps].sort())); await database.set('apps', JSON.stringify([...apps].sort()));
} }
database.remove('path:' + appName); database.remove('path:' + appName);
} else { } else {
@ -1163,8 +1142,8 @@ async function blobHandler(request, response, blobId, uri) {
} }
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id))); let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
id = app_object.files[uri.substring(1)]; id = app_object?.files[uri.substring(1)];
if (!id && app_object.files['handler.js']) { if (!id && app_object?.files['handler.js']) {
let answer; let answer;
try { try {
answer = await useAppHandler( answer = await useAppHandler(
@ -1248,7 +1227,7 @@ ssb.addEventListener('connections', function () {
async function loadSettings() { async function loadSettings() {
let data = {}; let data = {};
try { try {
let settings = new Database('core').get('settings'); let settings = await new Database('core').get('settings');
if (settings) { if (settings) {
data = JSON.parse(settings); data = JSON.parse(settings);
} }

View File

@ -15,9 +15,6 @@
# - Build again, this time it should work. # - Build again, this time it should work.
# - Check the release notes, if there's a new dependency or a change to `GNUMakefile`, this file might need to be changed too. # - Check the release notes, if there's a new dependency or a change to `GNUMakefile`, this file might need to be changed too.
# For more details, contact tasiaiso @ https://tilde.club/~tasiaiso/ # For more details, contact tasiaiso @ https://tilde.club/~tasiaiso/
#
# WARNING: currently it is pinned to `47838d5e482cb4aac40190fa0414f08b8cf94d40`. I couldn't get v0.0.18 to work for some reason.
# I'll change this in the next release - tasiaiso
{ {
pkgs ? import <nixpkgs> {}, pkgs ? import <nixpkgs> {},
lib ? import <nixpkgs/lib>, lib ? import <nixpkgs/lib>,
@ -30,19 +27,20 @@ pkgs.stdenv.mkDerivation rec {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";
owner = "cory"; owner = "cory";
repo = "tildefriends"; repo = "tildefriends";
# rev = "v${version}"; rev = "v${version}";
rev = "47838d5e482cb4aac40190fa0414f08b8cf94d40"; hash = "sha256-ttqL2wz06Jvn2f6kKIAGpF0nSSle+g4nSlj4jL0D+Fk=";
hash = "sha256-mb5KYvWPIqgV64FOaXKHm2ownBJiiSRtdH8+YWiXwvE="; # 47838d5e482cb4aac40190fa0414f08b8cf94d40
fetchSubmodules = true; fetchSubmodules = true;
}; };
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
glibc
gnumake gnumake
openssl openssl
which which
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
glibc
openssl openssl
which which
]; ];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
flake.lock generated
View File

@ -20,16 +20,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1715395895, "lastModified": 1717281328,
"narHash": "sha256-DreMqi6+qa21ffLQqhMQL2XRUkAGt3N7iVB5FhJKie4=", "narHash": "sha256-evZPzpf59oNcDUXxh2GHcxHkTEG4fjae2ytWP85jXRo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "71bae31b7dbc335528ca7e96f479ec93462323ff", "rev": "b3b2b28c1daa04fe2ae47c21bb76fd226eac4ca1",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-23.11", "ref": "nixos-24.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@ -2,7 +2,7 @@
description = "Tilde Friends is a platform for making, running, and sharing web applications."; description = "Tilde Friends is a platform for making, running, and sharing web applications.";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
@ -31,6 +31,8 @@
openssl openssl
llvmPackages_17.clang-unwrapped llvmPackages_17.clang-unwrapped
unzip unzip
doxygen
graphviz
]; ];
}; };
}); });

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="19" android:versionCode="20"
android:versionName="0.0.19"> android:versionName="0.0.20-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

@ -4,6 +4,7 @@
#include "mem.h" #include "mem.h"
#include "ssb.h" #include "ssb.h"
#include "task.h" #include "task.h"
#include "util.js.h"
#include "sqlite3.h" #include "sqlite3.h"
@ -91,57 +92,157 @@ static void _database_finalizer(JSRuntime* runtime, JSValue value)
--_database_count; --_database_count;
} }
static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) typedef struct _database_get_t
{ {
JSValue entry = JS_UNDEFINED; const char* id;
database_t* database = JS_GetOpaque(this_val, _database_class_id); const char* key;
if (database) size_t key_length;
{ char* out_value;
tf_ssb_t* ssb = tf_task_get_ssb(database->task); size_t out_length;
JSValue promise[2];
} database_get_t;
static void _database_get_work(tf_ssb_t* ssb, void* user_data)
{
database_get_t* work = user_data;
sqlite3_stmt* statement; sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
{ {
size_t length; if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
const char* keyString = JS_ToCStringLen(context, &length, argv[0]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW) sqlite3_step(statement) == SQLITE_ROW)
{ {
entry = JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)); size_t length = sqlite3_column_bytes(statement, 0);
char* data = tf_malloc(length + 1);
memcpy(data, sqlite3_column_text(statement, 0), length);
data[length] = '\0';
work->out_value = data;
work->out_length = length;
} }
JS_FreeCString(context, keyString);
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
}
static void _database_get_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_get_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->out_value)
{
result = JS_NewStringLen(context, work->out_value, work->out_length);
} }
return entry; JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->out_value);
tf_free(work);
}
static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
size_t length;
const char* key = JS_ToCStringLen(context, &length, argv[0]);
database_get_t* work = tf_malloc(sizeof(database_get_t) + strlen(database->id) + 1 + length + 1);
*work = (database_get_t) {
.id = (const char*)(work + 1),
.key = (const char*)(work + 1) + strlen(database->id) + 1,
.key_length = length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, length + 1);
JS_FreeCString(context, key);
tf_ssb_run_work(ssb, _database_get_work, _database_get_after_work, work);
result = JS_NewPromiseCapability(context, work->promise);
}
return result;
}
typedef struct _database_set_t
{
const char* id;
const char* key;
size_t key_length;
const char* value;
size_t value_length;
bool result;
JSValue promise[2];
} database_set_t;
static void _database_set_work(tf_ssb_t* ssb, void* user_data)
{
database_set_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK)
{
work->result = true;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _database_set_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_set_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->result ? JS_TRUE : JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
} }
static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id); database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database) if (database)
{ {
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task); tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK) size_t key_length = 0;
{ const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
size_t keyLength; size_t value_length = 0;
const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]); const char* value = JS_ToCStringLen(context, &value_length, argv[1]);
size_t valueLength;
const char* valueString = JS_ToCStringLen(context, &valueLength, argv[1]); database_set_t* work = tf_malloc(sizeof(database_set_t) + strlen(database->id) + 1 + key_length + 1 + value_length + 1);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK && *work = (database_set_t) {
sqlite3_bind_text(statement, 3, valueString, valueLength, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK) .id = (const char*)(work + 1),
{ .key = (const char*)(work + 1) + strlen(database->id) + 1,
.value = (const char*)(work + 1) + strlen(database->id) + 1 + key_length + 1,
.key_length = key_length,
.value_length = value_length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, key_length + 1);
memcpy((char*)work->value, value, value_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _database_set_work, _database_set_after_work, work);
JS_FreeCString(context, key);
JS_FreeCString(context, value);
} }
JS_FreeCString(context, keyString); return result;
JS_FreeCString(context, valueString);
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
return JS_UNDEFINED;
} }
static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
@ -219,31 +320,90 @@ static JSValue _database_remove(JSContext* context, JSValueConst this_val, int a
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) typedef struct _database_get_all_t
{ {
JSValue array = JS_UNDEFINED; const char* id;
database_t* database = JS_GetOpaque(this_val, _database_class_id); const char* key;
if (database) size_t key_length;
{ char** out_values;
size_t* out_lengths;
int out_values_length;
JSValue promise[2];
} database_get_all_t;
static void _database_get_all_work(tf_ssb_t* ssb, void* user_data)
{
database_get_all_t* work = user_data;
sqlite3_stmt* statement; sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ?1", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, "SELECT key FROM properties WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
{ {
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK) if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK)
{ {
array = JS_NewArray(context);
uint32_t index = 0;
while (sqlite3_step(statement) == SQLITE_ROW) while (sqlite3_step(statement) == SQLITE_ROW)
{ {
JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0))); work->out_values = tf_resize_vec(work->out_values, sizeof(char*) * (work->out_values_length + 1));
work->out_lengths = tf_resize_vec(work->out_lengths, sizeof(size_t) * (work->out_values_length + 1));
size_t length = sqlite3_column_bytes(statement, 0);
char* data = tf_malloc(length + 1);
memcpy(data, sqlite3_column_text(statement, 0), length);
data[length] = '\0';
work->out_values[work->out_values_length] = data;
work->out_lengths[work->out_values_length] = length;
work->out_values_length++;
} }
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
}
static void _database_get_all_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_get_all_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
;
for (int i = 0; i < work->out_values_length; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewStringLen(context, work->out_values[i], work->out_lengths[i]));
tf_free((void*)work->out_values[i]);
} }
return array; JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->out_values);
tf_free(work->out_lengths);
tf_free(work);
}
static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
size_t length;
const char* key = JS_ToCStringLen(context, &length, argv[0]);
database_get_all_t* work = tf_malloc(sizeof(database_get_all_t) + strlen(database->id) + 1 + length + 1);
*work = (database_get_all_t) {
.id = (const char*)(work + 1),
.key = (const char*)(work + 1) + strlen(database->id) + 1,
.key_length = length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, length + 1);
JS_FreeCString(context, key);
tf_ssb_run_work(ssb, _database_get_all_work, _database_get_all_after_work, work);
result = JS_NewPromiseCapability(context, work->promise);
}
return result;
} }
static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)

View File

@ -37,7 +37,7 @@
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000; const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
static JSValue _authenticate_jwt(JSContext* context, const char* jwt); static JSValue _authenticate_jwt(tf_ssb_t* ssb, 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_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 const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
@ -330,7 +330,7 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
tf_ssb_t* ssb = tf_task_get_ssb(tf_task_get(context)); 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"); const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
JSValue jwt = _authenticate_jwt(context, session); JSValue jwt = _authenticate_jwt(ssb, context, session);
tf_free((void*)session); tf_free((void*)session);
JSValue name = !JS_IsUndefined(jwt) ? JS_GetPropertyStr(context, jwt, "name") : JS_UNDEFINED; JSValue name = !JS_IsUndefined(jwt) ? JS_GetPropertyStr(context, jwt, "name") : JS_UNDEFINED;
const char* name_string = !JS_IsUndefined(name) ? JS_ToCString(context, name) : NULL; const char* name_string = !JS_IsUndefined(name) ? JS_ToCString(context, name) : NULL;
@ -446,6 +446,55 @@ static JSValue _httpd_set_http_redirect(JSContext* context, JSValueConst this_va
return JS_UNDEFINED; return JS_UNDEFINED;
} }
typedef struct _auth_query_work_t
{
const char* settings;
JSValue entry;
JSValue result;
JSValue promise[2];
} auth_query_work_t;
static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
{
auth_query_work_t* work = user_data;
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
auth_query_work_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue name = JS_GetPropertyStr(context, work->entry, "name");
const char* name_string = JS_ToCString(context, name);
JSValue settings_value = work->settings ? JS_ParseJSON(context, work->settings, strlen(work->settings), NULL) : JS_UNDEFINED;
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, work->result, "permissions", out_permissions);
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
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);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &work->result);
JS_FreeValue(context, work->result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, user_permissions);
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
tf_free((void*)work->settings);
JS_FreeCString(context, name_string);
JS_FreeValue(context, name);
tf_free(work);
}
static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
tf_task_t* task = tf_task_get(context); tf_task_t* task = tf_task_get(context);
@ -459,7 +508,7 @@ static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int
JSValue cookie = JS_GetPropertyStr(context, headers, "cookie"); JSValue cookie = JS_GetPropertyStr(context, headers, "cookie");
const char* cookie_string = JS_ToCString(context, cookie); const char* cookie_string = JS_ToCString(context, cookie);
const char* session = tf_http_get_cookie(cookie_string, "session"); const char* session = tf_http_get_cookie(cookie_string, "session");
JSValue entry = _authenticate_jwt(context, session); JSValue entry = _authenticate_jwt(ssb, context, session);
tf_free((void*)session); tf_free((void*)session);
JS_FreeCString(context, cookie_string); JS_FreeCString(context, cookie_string);
JS_FreeValue(context, cookie); JS_FreeValue(context, cookie);
@ -467,33 +516,16 @@ static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
if (!JS_IsUndefined(entry)) if (!JS_IsUndefined(entry))
{ {
result = JS_NewObject(context); JSValue value = JS_NewObject(context);
JS_SetPropertyStr(context, result, "session", entry); JS_SetPropertyStr(context, value, "session", entry);
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, result, "permissions", out_permissions);
JSValue name = JS_GetPropertyStr(context, entry, "name"); auth_query_work_t* work = tf_malloc(sizeof(auth_query_work_t));
const char* name_string = JS_ToCString(context, name); *work = (auth_query_work_t) {
.entry = entry,
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings"); .result = value,
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED; };
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED; result = JS_NewPromiseCapability(context, work->promise);
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED; tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
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; return result;
} }
@ -1062,13 +1094,17 @@ const char* _form_data_get(const char** form_data, const char* key)
typedef struct _login_request_t typedef struct _login_request_t
{ {
tf_http_request_t* request; tf_http_request_t* request;
const char* session_cookie;
JSValue jwt;
const char* name; const char* name;
const char* error; const char* error;
const char* settings;
const char* code_of_conduct; const char* code_of_conduct;
bool have_administrator; bool have_administrator;
bool session_is_new; bool session_is_new;
char location_header[1024];
const char* set_cookie_header;
int pending;
} login_request_t; } login_request_t;
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie) static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie)
@ -1083,18 +1119,29 @@ static const char* _make_set_session_cookie_header(tf_http_request_t* request, c
return cookie; return cookie;
} }
static void _login_release(login_request_t* login)
{
int ref_count = --login->pending;
if (ref_count == 0)
{
tf_free((void*)login->name);
tf_free((void*)login->code_of_conduct);
tf_free((void*)login->set_cookie_header);
tf_free(login);
}
}
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data) 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; login_request_t* login = user_data;
tf_http_request_t* request = login->request; tf_http_request_t* request = login->request;
if (result >= 0) if (result >= 0)
{ {
const char* cookie = _make_set_session_cookie_header(request, login->session_cookie);
const char* headers[] = { const char* headers[] = {
"Content-Type", "Content-Type",
"text/html; charset=utf-8", "text/html; charset=utf-8",
"Set-Cookie", "Set-Cookie",
cookie ? cookie : "", login->set_cookie_header ? login->set_cookie_header : "",
}; };
const char* replace_me = "$AUTH_DATA"; const char* replace_me = "$AUTH_DATA";
const char* auth = strstr(data, replace_me); const char* auth = strstr(data, replace_me);
@ -1128,7 +1175,6 @@ static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char
{ {
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result); tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
} }
tf_free((void*)cookie);
} }
else else
{ {
@ -1136,10 +1182,7 @@ static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload)); tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
} }
tf_http_request_unref(request); tf_http_request_unref(request);
tf_free((void*)login->name); _login_release(login);
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) static bool _string_property_equals(JSContext* context, JSValue object, const char* name, const char* value)
@ -1152,12 +1195,7 @@ static bool _string_property_equals(JSContext* context, JSValue object, const ch
return equals; return equals;
} }
static void _public_key_visit(const char* identity, void* user_data) static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt)
{
snprintf(user_data, k_id_base64_len, "%s", identity);
}
static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
{ {
if (!jwt) if (!jwt)
{ {
@ -1198,10 +1236,8 @@ static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
return JS_UNDEFINED; 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 }; char public_key_b64[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":admin", _public_key_visit, public_key_b64); tf_ssb_whoami(ssb, public_key_b64, sizeof(public_key_b64));
const char* payload = jwt + dot[0] + 1; const char* payload = jwt + dot[0] + 1;
size_t payload_length = dot[1] - dot[0] - 1; size_t payload_length = dot[1] - dot[0] - 1;
@ -1260,25 +1296,6 @@ static bool _is_name_valid(const char* name)
return true; 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, ":admin", _visit_auth_identity, id);
if (*id)
{
return tf_ssb_db_identity_get_private_key(ssb, ":admin", id, out_private_key, crypto_sign_SECRETKEYBYTES);
}
return false;
}
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name) static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
{ {
if (!name || !*name) if (!name || !*name)
@ -1309,8 +1326,8 @@ static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
char signature_base64[256] = { 0 }; char signature_base64[256] = { 0 };
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
if (_get_auth_private_key(ssb, private_key)) tf_ssb_get_private_key(ssb, private_key, sizeof(private_key));
{
if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 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); sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
@ -1319,7 +1336,6 @@ static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64); snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
} }
sodium_memzero(private_key, sizeof(private_key)); sodium_memzero(private_key, sizeof(private_key));
}
JS_FreeCString(context, payload_string); JS_FreeCString(context, payload_string);
JS_FreeValue(context, payload_json); JS_FreeValue(context, payload_json);
@ -1335,21 +1351,6 @@ static bool _verify_password(const char* password, const char* hash)
return out_hash && strcmp(hash, out_hash) == 0; return out_hash && strcmp(hash, out_hash) == 0;
} }
static const char* _get_code_of_conduct(tf_ssb_t* ssb)
{
JSContext* context = tf_ssb_get_context(ssb);
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);
const char* result = tf_strdup(code_of_conduct);
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
return result;
}
static bool _make_administrator_if_first(tf_ssb_t* ssb, const char* account_name_copy, bool may_become_first_admin) static bool _make_administrator_if_first(tf_ssb_t* ssb, const char* account_name_copy, bool may_become_first_admin)
{ {
JSContext* context = tf_ssb_get_context(ssb); JSContext* context = tf_ssb_get_context(ssb);
@ -1423,30 +1424,32 @@ static bool _make_administrator_if_first(tf_ssb_t* ssb, const char* account_name
return have_administrator; return have_administrator;
} }
static void _httpd_endpoint_login(tf_http_request_t* request) static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
{ {
tf_task_t* task = request->user_data; login_request_t* login = user_data;
JSContext* context = tf_task_get_context(task); tf_http_request_t* request = login->request;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session"); 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** form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0);
const char* account_name_copy = NULL; const char* account_name_copy = NULL;
JSValue jwt = _authenticate_jwt(context, session); JSValue jwt = _authenticate_jwt(ssb, context, session);
if (_session_is_authenticated_as_user(context, jwt)) if (_session_is_authenticated_as_user(context, jwt))
{ {
const char* return_url = _form_data_get(form_data, "return"); const char* return_url = _form_data_get(form_data, "return");
char url[1024]; if (return_url)
if (!return_url)
{ {
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host")); snprintf(login->location_header, sizeof(login->location_header), "%s", return_url);
return_url = url; }
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
} }
const char* headers[] = {
"Location",
return_url,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
goto done; goto done;
} }
@ -1525,42 +1528,46 @@ static void _httpd_endpoint_login(tf_http_request_t* request)
if (session_is_new && _form_data_get(form_data, "return") && !login_error) if (session_is_new && _form_data_get(form_data, "return") && !login_error)
{ {
const char* return_url = _form_data_get(form_data, "return"); const char* return_url = _form_data_get(form_data, "return");
char url[1024]; if (return_url)
if (!return_url)
{ {
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host")); snprintf(login->location_header, sizeof(login->location_header), "%s", return_url);
return_url = url;
} }
const char* cookie = _make_set_session_cookie_header(request, send_session); else
const char* headers[] = { {
"Location", snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
return_url, }
"Set-Cookie", login->set_cookie_header = _make_set_session_cookie_header(request, send_session);
cookie ? cookie : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
tf_free((void*)cookie);
tf_free((void*)send_session); tf_free((void*)send_session);
} }
else else
{ {
tf_http_request_ref(request); tf_http_request_ref(request);
login_request_t* login = tf_malloc(sizeof(login_request_t)); login->name = account_name_copy;
const char* code_of_conduct = _get_code_of_conduct(ssb); login->error = login_error;
*login = (login_request_t) { login->set_cookie_header = _make_set_session_cookie_header(request, send_session);
.request = request, tf_free((void*)send_session);
.name = account_name_copy, login->session_is_new = session_is_new;
.jwt = jwt, login->have_administrator = have_administrator;
.error = login_error, login->settings = tf_ssb_db_get_property(ssb, "core", "settings");
.session_cookie = send_session,
.session_is_new = session_is_new, if (login->settings)
.code_of_conduct = code_of_conduct, {
.have_administrator = have_administrator, JSValue settings_value = JS_ParseJSON(context, login->settings, strlen(login->settings), NULL);
}; 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);
const char* result = tf_strdup(code_of_conduct);
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)login->settings);
login->settings = NULL;
login->code_of_conduct = result;
}
login->pending++;
tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
tf_file_read(request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
jwt = JS_UNDEFINED;
account_name_copy = NULL; account_name_copy = NULL;
} }
@ -1569,6 +1576,44 @@ done:
tf_free(form_data); tf_free(form_data);
tf_free((void*)account_name_copy); tf_free((void*)account_name_copy);
JS_FreeValue(context, jwt); JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
login_request_t* login = user_data;
if (login->pending == 1)
{
tf_http_request_t* request = login->request;
if (*login->location_header)
{
const char* headers[] = {
"Location",
login->location_header,
"Set-Cookie",
login->set_cookie_header ? login->set_cookie_header : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
tf_http_request_unref(request);
}
_login_release(login);
}
static void _httpd_endpoint_login(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_http_request_ref(request);
tf_ssb_t* ssb = tf_task_get_ssb(task);
login_request_t* login = tf_malloc(sizeof(login_request_t));
*login = (login_request_t) {
.request = request,
};
login->pending++;
tf_ssb_run_work(ssb, _httpd_endpoint_login_work, _httpd_endpoint_login_after_work, login);
} }
static void _httpd_endpoint_logout(tf_http_request_t* request) static void _httpd_endpoint_logout(tf_http_request_t* request)

View File

@ -381,6 +381,7 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
tf_ssb_import(tf_task_get_ssb(task), "core", "apps"); tf_ssb_import(tf_task_get_ssb(task), "core", "apps");
} }
} }
tf_ssb_set_main_thread(tf_task_get_ssb(task), true);
if (tf_task_execute(task, args->script)) if (tf_task_execute(task, args->script))
{ {
tf_task_run(task); tf_task_run(task);

View File

@ -344,6 +344,7 @@ typedef struct _tf_ssb_connection_t
int ref_count; int ref_count;
int read_back_pressure; int read_back_pressure;
int active_write_count;
} tf_ssb_connection_t; } tf_ssb_connection_t;
static JSClassID _connection_class_id; static JSClassID _connection_class_id;
@ -460,9 +461,10 @@ static void _tf_ssb_connection_on_tcp_alloc(uv_handle_t* handle, size_t suggeste
static void _tf_ssb_connection_on_write(uv_write_t* req, int status) static void _tf_ssb_connection_on_write(uv_write_t* req, int status)
{ {
tf_ssb_connection_t* connection = req->data;
tf_ssb_connection_adjust_write_count(connection, -1);
if (status) if (status)
{ {
tf_ssb_connection_t* connection = req->data;
char buffer[256]; char buffer[256];
snprintf(buffer, sizeof(buffer), "write failed asynchronously: %s", uv_strerror(status)); snprintf(buffer, sizeof(buffer), "write failed asynchronously: %s", uv_strerror(status));
_tf_ssb_connection_close(connection, buffer); _tf_ssb_connection_close(connection, buffer);
@ -477,9 +479,11 @@ static void _tf_ssb_write(tf_ssb_connection_t* connection, void* data, size_t si
uv_write_t* write = tf_malloc(sizeof(uv_write_t) + size); uv_write_t* write = tf_malloc(sizeof(uv_write_t) + size);
*write = (uv_write_t) { .data = connection }; *write = (uv_write_t) { .data = connection };
memcpy(write + 1, data, size); memcpy(write + 1, data, size);
tf_ssb_connection_adjust_write_count(connection, 1);
int result = uv_write(write, (uv_stream_t*)&connection->tcp, &(uv_buf_t) { .base = (char*)(write + 1), .len = size }, 1, _tf_ssb_connection_on_write); int result = uv_write(write, (uv_stream_t*)&connection->tcp, &(uv_buf_t) { .base = (char*)(write + 1), .len = size }, 1, _tf_ssb_connection_on_write);
if (result) if (result)
{ {
tf_ssb_connection_adjust_write_count(connection, -1);
_tf_ssb_connection_close(connection, "write failed"); _tf_ssb_connection_close(connection, "write failed");
tf_free(write); tf_free(write);
} }
@ -606,6 +610,19 @@ static void _tf_ssb_connection_box_stream_send(tf_ssb_connection_t* connection,
} }
} }
static void _tf_ssb_connection_dispatch_scheduled(tf_ssb_connection_t* connection)
{
while ((connection->active_write_count == 0 || connection->closing) && connection->scheduled_count && connection->scheduled)
{
tf_ssb_connection_scheduled_t scheduled = connection->scheduled[0];
memmove(connection->scheduled, connection->scheduled + 1, sizeof(tf_ssb_connection_scheduled_t) * (connection->scheduled_count - 1));
connection->scheduled_count--;
tf_trace_begin(connection->ssb->trace, "scheduled callback");
scheduled.callback(connection, scheduled.user_data);
tf_trace_end(connection->ssb->trace);
}
}
void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, tf_ssb_scheduled_callback_t* callback, void* user_data) void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, tf_ssb_scheduled_callback_t* callback, void* user_data)
{ {
connection->scheduled = tf_resize_vec(connection->scheduled, sizeof(tf_ssb_connection_scheduled_t) * (connection->scheduled_count + 1)); connection->scheduled = tf_resize_vec(connection->scheduled, sizeof(tf_ssb_connection_scheduled_t) * (connection->scheduled_count + 1));
@ -613,7 +630,7 @@ void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, tf_ssb_sch
.callback = callback, .callback = callback,
.user_data = user_data, .user_data = user_data,
}; };
uv_async_send(&connection->async); _tf_ssb_connection_dispatch_scheduled(connection);
} }
static int _request_compare(const void* a, const void* b) static int _request_compare(const void* a, const void* b)
@ -1815,24 +1832,6 @@ JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* pr
return root; return root;
} }
static void _tf_ssb_connection_dispatch_scheduled(tf_ssb_connection_t* connection)
{
const int k_scheduled_batch_count = 8;
for (int i = 0; i < k_scheduled_batch_count && connection->scheduled_count && connection->scheduled; i++)
{
tf_ssb_connection_scheduled_t scheduled = connection->scheduled[0];
memmove(connection->scheduled, connection->scheduled + 1, sizeof(tf_ssb_connection_scheduled_t) * (connection->scheduled_count - 1));
connection->scheduled_count--;
tf_trace_begin(connection->ssb->trace, "scheduled callback");
scheduled.callback(connection, scheduled.user_data);
tf_trace_end(connection->ssb->trace);
}
if (connection->scheduled_count)
{
uv_async_send(&connection->async);
}
}
static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const char* reason) static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const char* reason)
{ {
tf_ssb_t* ssb = connection->ssb; tf_ssb_t* ssb = connection->ssb;
@ -1841,10 +1840,7 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
{ {
connection->destroy_reason = reason; connection->destroy_reason = reason;
} }
while (connection->scheduled_count)
{
_tf_ssb_connection_dispatch_scheduled(connection); _tf_ssb_connection_dispatch_scheduled(connection);
}
tf_free(connection->scheduled); tf_free(connection->scheduled);
connection->scheduled = NULL; connection->scheduled = NULL;
while (connection->requests) while (connection->requests)
@ -2613,7 +2609,6 @@ static void _tf_ssb_connection_process_message_async(uv_async_t* async)
{ {
uv_async_send(&connection->async); uv_async_send(&connection->async);
} }
_tf_ssb_connection_dispatch_scheduled(connection);
} }
tf_ssb_connection_t* tf_ssb_connection_create(tf_ssb_t* ssb, const char* host, const struct sockaddr_in* addr, const uint8_t* public_key) tf_ssb_connection_t* tf_ssb_connection_create(tf_ssb_t* ssb, const char* host, const struct sockaddr_in* addr, const uint8_t* public_key)
@ -4108,3 +4103,9 @@ void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection,
_tf_ssb_connection_destroy(connection, "backpressure released"); _tf_ssb_connection_destroy(connection, "backpressure released");
} }
} }
void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta)
{
connection->active_write_count += delta;
_tf_ssb_connection_dispatch_scheduled(connection);
}

View File

@ -635,6 +635,44 @@ bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_
return result; return result;
} }
typedef struct _blob_get_async_t
{
tf_ssb_t* ssb;
char id[k_blob_id_len];
tf_ssb_db_blob_get_callback_t* callback;
void* user_data;
bool out_found;
uint8_t* out_data;
size_t out_size;
} blob_get_async_t;
static void _tf_ssb_db_blob_get_async_work(tf_ssb_t* ssb, void* user_data)
{
blob_get_async_t* async = user_data;
async->out_found = tf_ssb_db_blob_get(ssb, async->id, &async->out_data, &async->out_size);
}
static void _tf_ssb_db_blob_get_async_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
blob_get_async_t* async = user_data;
async->callback(async->out_found, async->out_data, async->out_size, async->user_data);
tf_free(async->out_data);
tf_free(async);
}
void tf_ssb_db_blob_get_async(tf_ssb_t* ssb, const char* id, tf_ssb_db_blob_get_callback_t* callback, void* user_data)
{
blob_get_async_t* async = tf_malloc(sizeof(blob_get_async_t));
*async = (blob_get_async_t) {
.ssb = ssb,
.callback = callback,
.user_data = user_data,
};
snprintf(async->id, sizeof(async->id), "%s", id);
tf_ssb_run_work(ssb, _tf_ssb_db_blob_get_async_work, _tf_ssb_db_blob_get_async_after_work, async);
}
typedef struct _blob_store_work_t typedef struct _blob_store_work_t
{ {
const uint8_t* blob; const uint8_t* blob;
@ -648,7 +686,10 @@ typedef struct _blob_store_work_t
static void _tf_ssb_db_blob_store_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_db_blob_store_work(tf_ssb_t* ssb, void* user_data)
{ {
blob_store_work_t* blob_work = user_data; blob_store_work_t* blob_work = user_data;
if (!tf_ssb_is_shutting_down(ssb))
{
tf_ssb_db_blob_store(ssb, blob_work->blob, blob_work->size, blob_work->id, sizeof(blob_work->id), &blob_work->is_new); tf_ssb_db_blob_store(ssb, blob_work->blob, blob_work->size, blob_work->id, sizeof(blob_work->id), &blob_work->is_new);
}
} }
static void _tf_ssb_db_blob_store_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_db_blob_store_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -1046,6 +1087,8 @@ bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_pub
if (out_private_key) if (out_private_key)
{ {
tf_ssb_id_str_to_bin(out_private_key, private); tf_ssb_id_str_to_bin(out_private_key, private);
/* HACK: tf_ssb_id_str_to_bin only produces 32 bytes even though the full private key is 32 + 32. */
tf_ssb_id_str_to_bin(out_private_key + crypto_sign_PUBLICKEYBYTES, public);
} }
return true; return true;
} }

View File

@ -55,6 +55,24 @@ bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id);
*/ */
bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_t* out_size); bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_t* out_size);
/**
** A function called when a blob is retrieved from the database.
** @param found Whether the blob was found.
** @param data The blob data if found.
** @param size The size of the blob data if found, in bytes.
** @param user_data The user data.
*/
typedef void(tf_ssb_db_blob_get_callback_t)(bool found, const uint8_t* data, size_t size, void* user_data);
/**
** Retrieve a blob from the database asynchronously.
** @param ssb The SSB instance.
** @param id The blob identifier.
** @param callback Callback called with the result.
** @param user_data The user data.
*/
void tf_ssb_db_blob_get_async(tf_ssb_t* ssb, const char* id, tf_ssb_db_blob_get_callback_t* callback, void* user_data);
/** /**
** A function called when a message is stored in the database. ** A function called when a message is stored in the database.
** @param id The message identifier. ** @param id The message identifier.

View File

@ -745,7 +745,7 @@ JSValue tf_ssb_connection_requests_to_object(tf_ssb_connection_t* connection);
typedef void(tf_ssb_scheduled_callback_t)(tf_ssb_connection_t* connection, void* user_data); typedef void(tf_ssb_scheduled_callback_t)(tf_ssb_connection_t* connection, void* user_data);
/** /**
** Schedule work to be run when the server is next idle. ** Schedule work to be run when the connection is next idle.
** @param connection The owning connection. ** @param connection The owning connection.
** @param callback The callback to call. ** @param callback The callback to call.
** @param user_data User data to pass to the callback. ** @param user_data User data to pass to the callback.
@ -1005,4 +1005,13 @@ bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_
*/ */
void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection, int delta); void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection, int delta);
/**
** Adjust write count. Work scheduled by tf_ssb_connection_schedule_idle will
** only start when this reaches zero.
** @param connection The connection on which to affect backpressure.
** @param delta The change in write count. Higher will pause processing
** scheduled idle work queue. Lower will resume it.
*/
void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta);
/** @} */ /** @} */

View File

@ -30,35 +30,80 @@ static JSClassID _tf_ssb_classId;
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
typedef struct _create_identity_t
{
char id[k_id_base64_len];
bool error_add;
bool error_too_many;
JSValue promise[2];
char user[];
} create_identity_t;
static void _tf_ssb_create_identity_work(tf_ssb_t* ssb, void* user_data)
{
create_identity_t* work = user_data;
int count = tf_ssb_db_identity_get_count_for_user(ssb, work->user);
if (count < 16)
{
char public[k_id_base64_len - 1];
char private[512];
tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private));
if (tf_ssb_db_identity_add(ssb, work->user, public, private))
{
snprintf(work->id, sizeof(work->id), "@%s", public);
}
else
{
work->error_add = true;
}
}
else
{
work->error_too_many = true;
}
}
static void _tf_ssb_create_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
create_identity_t* work = user_data;
if (work->error_too_many)
{
result = JS_ThrowInternalError(context, "Too many identities for user.");
}
else if (work->error_add)
{
result = JS_ThrowInternalError(context, "Unable to add identity.");
}
else
{
result = JS_NewString(context, work->id);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_createIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_createIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
if (ssb) if (ssb)
{ {
const char* user = JS_ToCString(context, argv[0]); size_t length = 0;
int count = tf_ssb_db_identity_get_count_for_user(ssb, user); const char* user = JS_ToCStringLen(context, &length, argv[0]);
if (count < 16) create_identity_t* work = tf_malloc(sizeof(create_identity_t) + length + 1);
{ *work = (create_identity_t) { 0 };
char public[512]; memcpy(work->user, user, length + 1);
char private[512];
tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private));
if (tf_ssb_db_identity_add(ssb, user, public, private))
{
char id[513];
snprintf(id, sizeof(id), "@%s", public);
result = JS_NewString(context, id);
}
else
{
result = JS_ThrowInternalError(context, "Unable to add identity.");
}
}
else
{
result = JS_ThrowInternalError(context, "Too many identities for user.");
}
JS_FreeCString(context, user); JS_FreeCString(context, user);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_create_identity_work, _tf_ssb_create_identity_after_work, work);
} }
return result; return result;
} }
@ -224,31 +269,70 @@ static JSValue _tf_ssb_set_server_following_me(JSContext* context, JSValueConst
typedef struct _identities_visit_t typedef struct _identities_visit_t
{ {
JSContext* context; JSContext* context;
JSValue array; JSValue promise[2];
const char** identities;
int count; int count;
char user[];
} identities_visit_t; } identities_visit_t;
static void _tf_ssb_getIdentities_visit(const char* identity, void* data) static void _tf_ssb_getIdentities_visit(const char* identity, void* user_data)
{ {
identities_visit_t* state = data; identities_visit_t* work = user_data;
work->identities = tf_resize_vec(work->identities, (work->count + 1) * sizeof(const char*));
char id[k_id_base64_len]; char id[k_id_base64_len];
snprintf(id, sizeof(id), "@%s", identity); snprintf(id, sizeof(id), "@%s", identity);
JS_SetPropertyUint32(state->context, state->array, state->count++, JS_NewString(state->context, id)); work->identities[work->count++] = tf_strdup(id);
}
static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data)
{
identities_visit_t* work = user_data;
tf_ssb_db_identity_visit(ssb, work->user, _tf_ssb_getIdentities_visit, user_data);
}
static void _tf_ssb_get_all_identities_work(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_db_identity_visit_all(ssb, _tf_ssb_getIdentities_visit, user_data);
}
static void _tf_ssb_get_identities_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
identities_visit_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->count; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->identities[i]));
tf_free((void*)work->identities[i]);
}
tf_free(work->identities);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
} }
static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_NewArray(context); JSValue result = JS_UNDEFINED;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb) if (ssb)
{ {
const char* user = JS_ToCString(context, argv[0]); size_t user_length = 0;
identities_visit_t state = { const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
identities_visit_t* work = tf_malloc(sizeof(identities_visit_t) + user_length + 1);
*work = (identities_visit_t) {
.context = context, .context = context,
.array = result,
}; };
tf_ssb_db_identity_visit(ssb, user, _tf_ssb_getIdentities_visit, &state); memcpy(work->user, user, user_length + 1);
JS_FreeCString(context, user); JS_FreeCString(context, user);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
} }
return result; return result;
} }
@ -286,15 +370,17 @@ static JSValue _tf_ssb_getServerIdentity(JSContext* context, JSValueConst this_v
static JSValue _tf_ssb_getAllIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_getAllIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_NewArray(context); JSValue result = JS_UNDEFINED;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb) if (ssb)
{ {
identities_visit_t state = { identities_visit_t* work = tf_malloc(sizeof(identities_visit_t));
*work = (identities_visit_t) {
.context = context, .context = context,
.array = result,
}; };
tf_ssb_db_identity_visit_all(ssb, _tf_ssb_getIdentities_visit, &state);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_all_identities_work, _tf_ssb_get_identities_after_work, work);
} }
return result; return result;
} }
@ -581,6 +667,29 @@ static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueCons
return result; return result;
} }
typedef struct _blob_get_t
{
JSContext* context;
JSValue promise[2];
} blob_get_t;
static void _tf_ssb_blobGet_callback(bool found, const uint8_t* data, size_t size, void* user_data)
{
blob_get_t* get = user_data;
JSValue result = JS_UNDEFINED;
if (found)
{
result = JS_NewArrayBufferCopy(get->context, data, size);
}
JSValue error = JS_Call(get->context, get->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(get->context, result);
JS_FreeValue(get->context, get->promise[0]);
JS_FreeValue(get->context, get->promise[1]);
tf_util_report_error(get->context, error);
JS_FreeValue(get->context, error);
tf_free(get);
}
static JSValue _tf_ssb_blobGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_blobGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_NULL; JSValue result = JS_NULL;
@ -588,13 +697,10 @@ static JSValue _tf_ssb_blobGet(JSContext* context, JSValueConst this_val, int ar
if (ssb) if (ssb)
{ {
const char* id = JS_ToCString(context, argv[0]); const char* id = JS_ToCString(context, argv[0]);
uint8_t* blob = NULL; blob_get_t* get = tf_malloc(sizeof(blob_get_t));
size_t size = 0; *get = (blob_get_t) { .context = context };
if (tf_ssb_db_blob_get(ssb, id, &blob, &size)) result = JS_NewPromiseCapability(context, get->promise);
{ tf_ssb_db_blob_get_async(ssb, id, _tf_ssb_blobGet_callback, get);
result = JS_NewArrayBufferCopy(context, blob, size);
tf_free(blob);
}
JS_FreeCString(context, id); JS_FreeCString(context, id);
} }
return result; return result;

View File

@ -480,6 +480,45 @@ static void _tf_ssb_rpc_connection_blobs_get(tf_ssb_connection_t* connection, co
JS_FreeValue(context, message); JS_FreeValue(context, message);
} }
typedef struct _blob_create_wants_work_t
{
tf_ssb_connection_t* connection;
char blob_id[k_blob_id_len];
bool out_result;
int64_t size;
size_t out_size;
} blob_create_wants_work_t;
static void _tf_ssb_rpc_connection_blobs_create_wants_work(tf_ssb_connection_t* connection, void* user_data)
{
blob_create_wants_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(work->connection);
work->out_result = tf_ssb_db_blob_get(ssb, work->blob_id, NULL, &work->out_size);
}
static void _tf_ssb_rpc_connection_blobs_create_wants_after_work(tf_ssb_connection_t* connection, int result, void* user_data)
{
blob_create_wants_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(work->connection);
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
JSContext* context = tf_ssb_get_context(ssb);
if (work->out_result)
{
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, work->blob_id, JS_NewInt64(context, work->out_size));
tf_ssb_connection_rpc_send_json(work->connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
else if (work->size == -1LL)
{
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, work->blob_id, JS_NewInt64(context, -2));
tf_ssb_connection_rpc_send_json(work->connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
tf_free(work);
}
static void _tf_ssb_rpc_connection_blobs_createWants_callback( static void _tf_ssb_rpc_connection_blobs_createWants_callback(
tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{ {
@ -489,7 +528,6 @@ static void _tf_ssb_rpc_connection_blobs_createWants_callback(
return; return;
} }
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_connection_get_context(connection); JSContext* context = tf_ssb_connection_get_context(connection);
JSValue name = JS_GetPropertyStr(context, args, "name"); JSValue name = JS_GetPropertyStr(context, args, "name");
@ -524,21 +562,13 @@ static void _tf_ssb_rpc_connection_blobs_createWants_callback(
} }
if (size < 0) if (size < 0)
{ {
size_t blob_size = 0; blob_create_wants_work_t* work = tf_malloc(sizeof(blob_create_wants_work_t));
if (tf_ssb_db_blob_get(ssb, blob_id, NULL, &blob_size)) *work = (blob_create_wants_work_t) {
{ .connection = connection,
JSValue message = JS_NewObject(context); .size = size,
JS_SetPropertyStr(context, message, blob_id, JS_NewInt64(context, blob_size)); };
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL); snprintf(work->blob_id, sizeof(work->blob_id), "%s", blob_id);
JS_FreeValue(context, message); tf_ssb_connection_run_work(connection, _tf_ssb_rpc_connection_blobs_create_wants_work, _tf_ssb_rpc_connection_blobs_create_wants_after_work, work);
}
else if (size == -1LL)
{
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, blob_id, JS_NewInt64(context, -2));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
} }
else else
{ {
@ -744,6 +774,7 @@ static void _tf_ssb_connection_send_history_stream_work(tf_ssb_connection_t* con
static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_t* connection, int result, void* user_data) static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_t* connection, int result, void* user_data)
{ {
tf_ssb_connection_send_history_stream_t* request = user_data; tf_ssb_connection_send_history_stream_t* request = user_data;
tf_ssb_connection_adjust_write_count(connection, -1);
if (tf_ssb_connection_is_connected(connection)) if (tf_ssb_connection_is_connected(connection))
{ {
for (int i = 0; i < request->out_messages_count; i++) for (int i = 0; i < request->out_messages_count; i++)
@ -768,6 +799,19 @@ static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_
tf_free(request); tf_free(request);
} }
static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t* connection, void* user_data)
{
tf_ssb_connection_adjust_write_count(connection, 1);
if (tf_ssb_connection_is_connected(connection))
{
tf_ssb_connection_run_work(connection, _tf_ssb_connection_send_history_stream_work, _tf_ssb_connection_send_history_stream_after_work, user_data);
}
else
{
_tf_ssb_connection_send_history_stream_after_work(connection, -1, user_data);
}
}
static void _tf_ssb_connection_send_history_stream(tf_ssb_connection_t* connection, int32_t request_number, const char* author, int64_t sequence, bool keys, bool live) static void _tf_ssb_connection_send_history_stream(tf_ssb_connection_t* connection, int32_t request_number, const char* author, int64_t sequence, bool keys, bool live)
{ {
tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t)); tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t));
@ -778,7 +822,7 @@ static void _tf_ssb_connection_send_history_stream(tf_ssb_connection_t* connecti
.live = live, .live = live,
}; };
snprintf(async->author, sizeof(async->author), "%s", author); snprintf(async->author, sizeof(async->author), "%s", author);
tf_ssb_connection_run_work(connection, _tf_ssb_connection_send_history_stream_work, _tf_ssb_connection_send_history_stream_after_work, async); tf_ssb_connection_schedule_idle(connection, _tf_ssb_connection_send_history_stream_callback, async);
} }
static void _tf_ssb_rpc_createHistoryStream( static void _tf_ssb_rpc_createHistoryStream(

View File

@ -266,20 +266,21 @@ static void _test_promise_remote_reject(const tf_test_options_t* options)
static void _test_database(const tf_test_options_t* options) static void _test_database(const tf_test_options_t* options)
{ {
_write_file("out/test.js", _write_file("out/test.js",
"var db = new Database('testdb');\n" "async function main() {\n"
"if (db.get('a')) {\n" " var db = new Database('testdb');\n"
" if (await db.get('a')) {\n"
" exit(1);\n" " exit(1);\n"
"}\n" " }\n"
"db.set('a', 1);\n" " await db.set('a', 1);\n"
"if (db.get('a') != 1) {\n" " if (await db.get('a') != 1) {\n"
" exit(2);\n" " exit(2);\n"
"}\n" " }\n"
"db.set('b', 2);\n" " await db.set('b', 2);\n"
"db.set('c', 3);\n" " await db.set('c', 3);\n"
"\n" "\n"
"var expected = ['a', 'b', 'c'];\n" " var expected = ['a', 'b', 'c'];\n"
"var have = db.getAll();\n" " var have = await db.getAll();\n"
"for (var i = 0; i < have.length; i++) {\n" " for (var i = 0; i < have.length; i++) {\n"
" var item = have[i];\n" " var item = have[i];\n"
" if (expected.indexOf(item) == -1) {\n" " if (expected.indexOf(item) == -1) {\n"
" print('Did not find ' + item + ' in db.');\n" " print('Did not find ' + item + ' in db.');\n"
@ -287,11 +288,13 @@ static void _test_database(const tf_test_options_t* options)
" } else {\n" " } else {\n"
" expected.splice(expected.indexOf(item), 1);\n" " expected.splice(expected.indexOf(item), 1);\n"
" }\n" " }\n"
"}\n" " }\n"
"if (expected.length) {\n" " if (expected.length) {\n"
" print('Expected but did not find: ' + JSON.stringify(expected));\n" " print('Expected but did not find: ' + JSON.stringify(expected));\n"
" exit(4);\n" " exit(4);\n"
"}\n"); " }\n"
"}\n"
"main();");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); unlink("out/test_db0.sqlite");

View File

@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.19" #define VERSION_NUMBER "0.0.20-wip"
#define VERSION_NAME "Don't let your loyalty become a burden." #define VERSION_NAME "One word all lowercase four words all uppercase."

View File

@ -42,20 +42,19 @@ try:
driver.switch_to.frame(driver.find_element(By.ID, 'document')) driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity'))).click() wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity'))).click()
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
# StaleElementReferenceException # StaleElementReferenceException
while True: while True:
try: try:
driver.switch_to.default_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')))) driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
driver.switch_to.alert.accept()
break break
except: except:
pass pass
wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
driver.switch_to.alert.accept()
# StaleElementReferenceException # StaleElementReferenceException
while True: while True:
try: try:

View File

@ -3,7 +3,7 @@
if [ -z $ANDROID_NDK_ROOT ]; then if [ -z $ANDROID_NDK_ROOT ]; then
ANDROID_NDK_ROOT=~/Android/Sdk/ndk/26.1.10909125 ANDROID_NDK_ROOT=~/Android/Sdk/ndk/26.1.10909125
fi fi
OPENSSL_VERSION=3.3.0 OPENSSL_VERSION=3.3.1
API_LEVEL=24 API_LEVEL=24

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
OPENSSL_VERSION=3.3.0 OPENSSL_VERSION=3.3.1
API_LEVEL=28 API_LEVEL=28
@ -14,7 +14,7 @@ if [ ! -d out/openssl-${OPENSSL_VERSION} ]
then then
if [ ! -f out/openssl-${OPENSSL_VERSION}.tar.gz ] if [ ! -f out/openssl-${OPENSSL_VERSION}.tar.gz ]
then then
curl https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz -o out/openssl-${OPENSSL_VERSION}.tar.gz || exit 128 curl -L https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz -o out/openssl-${OPENSSL_VERSION}.tar.gz || exit 128
fi fi
tar -C out/ -xzf out/openssl-${OPENSSL_VERSION}.tar.gz || exit 128 tar -C out/ -xzf out/openssl-${OPENSSL_VERSION}.tar.gz || exit 128
fi fi

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
OPENSSL_VERSION=3.3.0 OPENSSL_VERSION=3.3.1
API_LEVEL=24 API_LEVEL=24

View File

@ -1,4 +1,4 @@
VERSION=3.1.3 VERSION=3.1.4
wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js -O deps/lit/lit-all.min.js wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js -O deps/lit/lit-all.min.js
wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js.map -O deps/lit/lit-all.min.js.map wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js.map -O deps/lit/lit-all.min.js.map
cp -fv deps/lit/* apps/blog/ cp -fv deps/lit/* apps/blog/