Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
f4f560b164 | |||
14b7f9237b | |||
f3518b3d0f | |||
7964524e0a | |||
8ab8335baa | |||
cd43bf9dfa | |||
ccebf831e7 | |||
9f2f9bd8b0 | |||
adf8c14536 | |||
606e82d718 | |||
1621f1753a | |||
196ab66e14 | |||
38ab32dad9 | |||
86046e52f0 | |||
9e7c860414 | |||
7dc8b86ee2 | |||
6ecbfe3de6 | |||
f9940fc436 | |||
58e75ee276 | |||
e7771f539d | |||
c2f62cd8e0 | |||
f4b6812675 | |||
03e4b37c04 | |||
7b3a9e0f63 | |||
067f546580 | |||
2f7697b7ec | |||
1d214f89ed | |||
0b47207949 | |||
94dd573a81 | |||
6fa4896155 | |||
28c99f9d8b | |||
88fbb5f73b | |||
402c185dd4 | |||
ae2015a604 | |||
023731fc3f | |||
99998aac8a | |||
360d0bc110 | |||
817838e522 | |||
deb3cfb4b6 | |||
af61519632 | |||
b1714cf554 | |||
f0984b19f2 | |||
eb3c9cd6f3 | |||
e677b0ac3c | |||
dd909bfe53 | |||
b13b111614 | |||
5511530926 | |||
5e1ef01bc0 | |||
a060eadab7 | |||
70db31bb8f | |||
1292775a75 |
GNUmakefileREADME.md
apps
core
deps/quickjs
ChangelogMakefileTODOVERSION
doc
libbf.clibregexp.clibunicode-table.hqjs.cqjsc.cquickjs-atom.hquickjs-opcode.hquickjs.cquickjs.htest262.conftest262_errors.txtunicode_download.shunicode_gen_def.hsrc
android
http.chttp.hhttpd.js.chttpd.js.hmain.csocket.js.cssb.cssb.db.cssb.tests.ctask.ctests.ctrace.cutil.js.cutil.js.hversion.htools
25
GNUmakefile
25
GNUmakefile
@ -3,9 +3,9 @@
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
VERSION_CODE := 13
|
||||
VERSION_NUMBER := 0.0.13
|
||||
VERSION_NAME := Served on grilled naan or gluten free sweet potato flatbread.
|
||||
VERSION_CODE := 14
|
||||
VERSION_NUMBER := 0.0.14
|
||||
VERSION_NAME := Served on apple cider dressed winter greens.
|
||||
|
||||
PROJECT = tildefriends
|
||||
BUILD_DIR ?= out
|
||||
@ -42,6 +42,7 @@ $(error Unexpected host platform $(UNAME_S).)
|
||||
endif
|
||||
|
||||
CFLAGS += \
|
||||
-std=gnu11 \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
@ -448,8 +449,10 @@ $(SODIUM_OBJS): CFLAGS += \
|
||||
-Wno-attributes \
|
||||
-Ideps/libsodium/builds/msvc \
|
||||
-Ideps/libsodium/src/libsodium/include/sodium
|
||||
$(SODIUM_OBJS_unix): CFLAGS += \
|
||||
ifneq ($(UNAME_S),OpenBSD)
|
||||
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
|
||||
-DHAVE_ALLOCA_H
|
||||
endif
|
||||
|
||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||
@ -700,7 +703,7 @@ PACKAGE_DIRS := \
|
||||
deps/codemirror/ \
|
||||
deps/lit/
|
||||
|
||||
RAW_FILES := $(filter-out apps/gg% apps/welcome% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
||||
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
||||
|
||||
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
||||
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
||||
@ -722,19 +725,19 @@ out/apk/TildeFriends-arm-%.unsigned.apk:
|
||||
@cp out/apk/res.apk $@
|
||||
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||
@zip -u $@ -q -9 -x '*.map' --exclude=apps/gg* --exclude=apps/welcome* -r $(PACKAGE_DIRS) $(RAW_FILES)
|
||||
@zip -u $@ -q -9 $(RAW_FILES)
|
||||
|
||||
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||
@echo [aapt] $@
|
||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/
|
||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends
|
||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||
@cp out/apk/res.apk $@
|
||||
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||
@zip -u $@ -q -9 -x '*.map' --exclude=apps/gg* --exclude=apps/welcome* -r $(PACKAGE_DIRS) $(RAW_FILES)
|
||||
@zip -u $@ -q -9 $(RAW_FILES)
|
||||
|
||||
out/%.apk: out/apk/%.unsigned.apk
|
||||
@echo [apksigner] $(notdir $@)
|
||||
|
@ -17,7 +17,7 @@ Scuttlebutt, as well as a platform for writing and running web applications.
|
||||
generated in a subdirectory of `out/`.
|
||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
||||
the right dependencies in the right places. `make windebug winrelease
|
||||
iosdebug-ipa iosrelease-ipa apk`.
|
||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
||||
4. To build in docker, `docker build .`.
|
||||
|
||||
## Running
|
||||
|
5
apps/blog.json
Normal file
5
apps/blog.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🪵",
|
||||
"previous": "&YHBylHM7DlDDiGfuNj+g95Bf7NFxTZs/IKG14TbWnhs=.sha256"
|
||||
}
|
8
apps/blog/app.js
Normal file
8
apps/blog/app.js
Normal file
@ -0,0 +1,8 @@
|
||||
import * as blog from './blog.js';
|
||||
|
||||
async function main() {
|
||||
let blogs = await blog.get_posts();
|
||||
await app.setDocument(blog.render_html(blogs));
|
||||
}
|
||||
|
||||
main();
|
149
apps/blog/blog.js
Normal file
149
apps/blog/blog.js
Normal file
@ -0,0 +1,149 @@
|
||||
import * as commonmark from './commonmark.min.js';
|
||||
|
||||
function escape(text) {
|
||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
function escapeAttribute(text) {
|
||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function markdown(md) {
|
||||
let reader = new commonmark.Parser({safe: true});
|
||||
let writer = new commonmark.HtmlRenderer();
|
||||
let parsed = reader.parse(md || '');
|
||||
let walker = parsed.walker();
|
||||
let event, node;
|
||||
while ((event = walker.next())) {
|
||||
node = event.node;
|
||||
if (event.entering) {
|
||||
if (node.type == 'image') {
|
||||
if (node.destination.startsWith('&')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
export async function render_blog_post_html(blog_post) {
|
||||
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||
<div>${markdown(blob)}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
function render_blog_post(blog_post) {
|
||||
return `
|
||||
<div>
|
||||
<h2><a href="../ssb/#${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
|
||||
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||
<div>${markdown(blog_post.summary)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function render_html(blogs) {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>🪵Tilde Blog</title>
|
||||
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
||||
<style>
|
||||
html {
|
||||
background-color: #ccc;
|
||||
}
|
||||
</style>
|
||||
<base target="_blank">
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
||||
<h1>🪵Tilde Blog</h1>
|
||||
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||
</div>
|
||||
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function render_blog_post_atom(blog_post) {
|
||||
return `<entry>
|
||||
<title>${escape(blog_post.title)}</title>
|
||||
<link href="https://tildefriends.net/~cory/ssb/#${blog_post.id}" />
|
||||
<id>${blog_post.id}</id>
|
||||
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
||||
<summary>${escape(blog_post.summary)}</summary>
|
||||
<author>
|
||||
<name>${escape(blog_post.name)}</name>
|
||||
<feed>${escape(blog_post.author)}</feed>
|
||||
</author>
|
||||
</entry>`;
|
||||
}
|
||||
|
||||
export function render_atom(blogs) {
|
||||
return `<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>🪵Tilde Blog</title>
|
||||
<subtitle>A subtitle.</subtitle>
|
||||
<link href="https://tildefriends.net/~cory/blog/atom" rel="self"/>
|
||||
<link href="https://tildefriends.net/~cory/blog/"/>
|
||||
<id>https://www.tildefriends.net/~cory/blog/</id>
|
||||
<updated>${new Date().toString()}</updated>
|
||||
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
|
||||
</feed>`;
|
||||
}
|
||||
|
||||
export async function get_posts() {
|
||||
let blogs = [];
|
||||
await ssb.sqlAsync(`
|
||||
WITH
|
||||
blogs AS (
|
||||
SELECT
|
||||
messages.author,
|
||||
messages.id,
|
||||
json_extract(messages.content, '$.title') AS title,
|
||||
json_extract(messages.content, '$.summary') AS summary,
|
||||
json_extract(messages.content, '$.blog') AS blog,
|
||||
messages.timestamp
|
||||
FROM messages_fts('blog')
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
WHERE json_extract(messages.content, '$.type') = 'blog'),
|
||||
public AS (
|
||||
SELECT author FROM (
|
||||
SELECT
|
||||
messages.author,
|
||||
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||
json_extract(messages.content, '$.publicWebHosting') AS is_public
|
||||
FROM messages_fts('about')
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
|
||||
WHERE author_rank = 1 AND is_public),
|
||||
names AS (
|
||||
SELECT author, name FROM (
|
||||
SELECT
|
||||
messages.author,
|
||||
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||
json_extract(messages.content, '$.name') AS name
|
||||
FROM messages_fts('about')
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
WHERE json_extract(messages.content, '$.type') = 'about' AND
|
||||
json_extract(messages.content, '$.about') = messages.author AND
|
||||
name IS NOT NULL)
|
||||
WHERE author_rank = 1)
|
||||
SELECT blogs.*, names.name FROM blogs
|
||||
JOIN public ON public.author = blogs.author
|
||||
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||
`, [], function(row) {
|
||||
blogs.push(row);
|
||||
});
|
||||
return blogs;
|
||||
}
|
1
apps/blog/commonmark.min.js
vendored
Normal file
1
apps/blog/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
22
apps/blog/handler.js
Normal file
22
apps/blog/handler.js
Normal file
@ -0,0 +1,22 @@
|
||||
import * as blog from './blog.js';
|
||||
|
||||
async function main() {
|
||||
let blogs = await blog.get_posts();
|
||||
for (let blog_post of blogs) {
|
||||
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||
if (request.path === title) {
|
||||
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (request.path == 'atom') {
|
||||
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
||||
} else {
|
||||
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(function(error) {
|
||||
respond({data: `<!DOCTYPE html>
|
||||
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
||||
});
|
126
apps/blog/lit-all.min.js
vendored
Normal file
126
apps/blog/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/blog/lit-all.min.js.map
Normal file
1
apps/blog/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🐌",
|
||||
"previous": "&vIYnoUkbz97WRvyunV+ETe+Y6tJk7tTEVvgYuwkoDiM=.sha256"
|
||||
"previous": "&dO6ckMIPVv9QvSc+0TOg0S59qe+rirPo2a6p9xSHj9M=.sha256"
|
||||
}
|
@ -106,6 +106,7 @@ class TfProfileElement extends LitElement {
|
||||
name: original.name,
|
||||
description: original.description,
|
||||
image: original.image,
|
||||
publicWebHosting: original.publicWebHosting,
|
||||
};
|
||||
console.log(this.editing);
|
||||
}
|
||||
@ -220,7 +221,7 @@ class TfProfileElement extends LitElement {
|
||||
<textarea style="flex: 1 0" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
||||
<div>
|
||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
||||
<input 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>
|
||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📝",
|
||||
"previous": "&r+JXDhWclHwMbeZHYjueOnSPaYCAXUlnl1Gyjgw6TxM=.sha256"
|
||||
"previous": "&JHopifgZn2TsiMCQY8HUTlDqHEDDviiu2ifvr8HHNwo=.sha256"
|
||||
}
|
@ -240,6 +240,14 @@ class TfCollectionsAppElement extends LitElement {
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<style>
|
||||
.toc:hover {
|
||||
background-color: #0cc;
|
||||
}
|
||||
.toc.selected {
|
||||
background-color: #088;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker>
|
||||
</div>
|
||||
@ -278,12 +286,24 @@ class TfCollectionsAppElement extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html`
|
||||
<tf-wiki-doc
|
||||
whoami=${this.whoami}
|
||||
.wiki=${this.wiki}
|
||||
.value=${this.wiki_doc}></tf-wiki-doc>
|
||||
` : undefined}
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<div style="flex: 0 0">
|
||||
${Object.values(this.wikis || {}).sort((x, y) => x.name.localeCompare(y.name)).map(wiki => html`
|
||||
<div class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_changed({detail: {value: wiki}})}>${wiki.name}</div>
|
||||
<ul>
|
||||
${Object.values(self.wiki_docs || {}).filter(doc => doc.parent === wiki?.id).sort((x, y) => x.name.localeCompare(y.name)).map(doc => html`
|
||||
<li class="toc ${self.wiki_doc?.id === doc.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_doc_changed({detail: {value: doc}})}>${doc.name}</li>
|
||||
`)}
|
||||
</ul>
|
||||
`)}
|
||||
</div>
|
||||
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html`
|
||||
<tf-wiki-doc
|
||||
whoami=${this.whoami}
|
||||
.wiki=${this.wiki}
|
||||
.value=${this.wiki_doc}></tf-wiki-doc>
|
||||
` : undefined}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ class TfWikiDocElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
wiki: {type: String},
|
||||
wiki: {type: Object},
|
||||
value: {type: Object},
|
||||
blob: {type: String},
|
||||
blob_original: {type: String},
|
||||
@ -20,9 +20,9 @@ class TfWikiDocElement extends LitElement {
|
||||
}
|
||||
|
||||
markdown(md) {
|
||||
var reader = new commonmark.Parser({safe: true});
|
||||
var writer = new commonmark.HtmlRenderer();
|
||||
var parsed = reader.parse(md || '');
|
||||
let reader = new commonmark.Parser({safe: true});
|
||||
let writer = new commonmark.HtmlRenderer();
|
||||
let parsed = reader.parse(md || '');
|
||||
let walker = parsed.walker();
|
||||
let event;
|
||||
while ((event = walker.next())) {
|
||||
@ -33,12 +33,51 @@ class TfWikiDocElement extends LitElement {
|
||||
node.destination.indexOf('/') == -1) {
|
||||
node.destination = `#${this.wiki?.name}/${node.destination}`;
|
||||
}
|
||||
} else if (node.type == 'image') {
|
||||
if (node.destination.startsWith('&')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
title(md) {
|
||||
let lines = (md || '').split('\n');
|
||||
for (let line of lines) {
|
||||
let m = line.match(/#+ (.*)/);
|
||||
if (m) {
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary(md) {
|
||||
let lines = (md || '').split('\n');
|
||||
let result = [];
|
||||
let have_content = false;
|
||||
for (let line of lines) {
|
||||
if (have_content && !line.trim().length) {
|
||||
return result.join('\n');
|
||||
}
|
||||
if (!line.startsWith('#') && line.trim().length) {
|
||||
have_content = true;
|
||||
}
|
||||
if (!line.startsWith('#')) {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
thumbnail(md) {
|
||||
//let m = md ? md.match(/\!\[image:[^\]]+]\((\&.{44}\.sha256)\)/) : undefined;
|
||||
let m = md ? md.match(/.*\((\&.{44}\.sha256)\).*/) : undefined;
|
||||
console.log('thumb', m);
|
||||
return m ? m[1] : undefined;
|
||||
}
|
||||
|
||||
async load_blob() {
|
||||
let blob = await tfrpc.rpc.get_blob(this.value?.blob);
|
||||
if (blob.endsWith('.box')) {
|
||||
@ -62,8 +101,8 @@ class TfWikiDocElement extends LitElement {
|
||||
|
||||
async append_message(draft) {
|
||||
let blob = this.blob;
|
||||
if (draft) {
|
||||
blob = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, blob);
|
||||
if (draft || this.value?.private) {
|
||||
blob = await tfrpc.rpc.encrypt(this.whoami, this.wiki.editors, blob);
|
||||
}
|
||||
let id = await tfrpc.rpc.store_blob(blob);
|
||||
let message = {
|
||||
@ -71,13 +110,13 @@ class TfWikiDocElement extends LitElement {
|
||||
key: this.value.id,
|
||||
parent: this.value.parent,
|
||||
blob: id,
|
||||
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})),
|
||||
private: this.value?.private,
|
||||
};
|
||||
if (draft) {
|
||||
message.recps = this.value.editors;
|
||||
print(message);
|
||||
message = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, JSON.stringify(message));
|
||||
}
|
||||
print(message);
|
||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||
this.is_editing = false;
|
||||
}
|
||||
@ -90,6 +129,91 @@ class TfWikiDocElement extends LitElement {
|
||||
return this.append_message(false);
|
||||
}
|
||||
|
||||
async on_blog_publish() {
|
||||
let blob = this.blob;
|
||||
let id = await tfrpc.rpc.store_blob(blob);
|
||||
let message = {
|
||||
type: 'blog',
|
||||
key: this.value.id,
|
||||
parent: this.value.parent,
|
||||
title: this.title(blob),
|
||||
summary: this.summary(blob),
|
||||
thumbnail: this.thumbnail(blob),
|
||||
blog: id,
|
||||
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})),
|
||||
};
|
||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||
this.is_editing = false;
|
||||
}
|
||||
|
||||
convert_to_format(buffer, type, mime_type) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let img = new Image();
|
||||
img.onload = function() {
|
||||
let canvas = document.createElement('canvas');
|
||||
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||
let scale = Math.min(width_scale, height_scale);
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
let context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
let data_url = canvas.toDataURL(mime_type);
|
||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||
resolve(result);
|
||||
};
|
||||
img.onerror = function(event) {
|
||||
reject(new Error('Failed to load image.'));
|
||||
};
|
||||
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
||||
let original = `data:${type};base64,${btoa(raw)}`;
|
||||
img.src = original;
|
||||
});
|
||||
}
|
||||
|
||||
async add_file(editor, file) {
|
||||
try {
|
||||
let self = this;
|
||||
let buffer = await file.arrayBuffer();
|
||||
let type = file.type;
|
||||
if (type.startsWith('image/')) {
|
||||
let best_buffer;
|
||||
let best_type;
|
||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||
best_buffer = test_buffer;
|
||||
best_type = format;
|
||||
}
|
||||
}
|
||||
buffer = best_buffer;
|
||||
type = best_type;
|
||||
} else {
|
||||
buffer = Array.from(new Uint8Array(buffer));
|
||||
}
|
||||
let id = await tfrpc.rpc.store_blob(buffer);
|
||||
let name = type.split('/')[0] + ':' + file.name;
|
||||
editor.value += `\n`;
|
||||
self.on_edit({srcElement: editor});
|
||||
} catch(e) {
|
||||
alert(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
paste(event) {
|
||||
let self = this;
|
||||
for (let item of event.clipboardData.items) {
|
||||
if (item.type?.startsWith('image/')) {
|
||||
let file = item.getAsFile();
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
self.add_file(event.srcElement, file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let value = JSON.stringify(this.value);
|
||||
if (this.blob_for_value != value) {
|
||||
@ -99,19 +223,32 @@ class TfWikiDocElement extends LitElement {
|
||||
this.load_blob();
|
||||
}
|
||||
let self = this;
|
||||
let thumbnail_ref = this.thumbnail(this.blob);
|
||||
return html`
|
||||
<div style="display: inline-flex; flex-direction: row">
|
||||
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => self.is_editing = true}>Edit</button>
|
||||
<button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button>
|
||||
<button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button>
|
||||
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button>
|
||||
<button ?disabled=${!this.is_editing} @click=${() => self.value = Object.assign({}, self.value, {private: !self.value.private})}>${this.value?.private ? 'Make Public' : 'Make Private'}</button>
|
||||
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</button>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
|
||||
<div style="display: flex; flex-direction: row; ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
|
||||
<textarea
|
||||
?hidden=${!this.is_editing}
|
||||
style="flex: 1 1; min-height: 10em"
|
||||
@input=${this.on_edit} .value=${this.blob ?? ''}></textarea>
|
||||
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.blob))}</div>
|
||||
style="flex: 1 1; min-height: 10em; ${this.value?.private ? 'border: 4px solid #800' : ''}"
|
||||
@input=${this.on_edit}
|
||||
@paste=${this.paste}
|
||||
.value=${this.blob ?? ''}></textarea>
|
||||
<div style="flex: 1 1">
|
||||
<div ?hidden=${!this.is_editing} style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em">
|
||||
<img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view">
|
||||
<h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1>
|
||||
${unsafeHTML(this.markdown(this.summary(this.blob)))}
|
||||
</div>
|
||||
${unsafeHTML(this.markdown(this.blob))}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -120,19 +120,38 @@ class TfNavigationElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
set_access_key_title(event) {
|
||||
if (!event.srcElement.title) {
|
||||
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || event.srcElement.accessKey).toUpperCase()}]`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<style>
|
||||
${k_global_style}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: none;
|
||||
border: 1px solid black;
|
||||
padding: 4px;
|
||||
color: black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tooltip_parent:hover .tooltip {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center">
|
||||
<span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>😎</span>
|
||||
<span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span>
|
||||
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
|
||||
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
|
||||
<a accesskey="e" data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
|
||||
<a accesskey="p" data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
|
||||
<a accesskey="h" @mouseover=${this.set_access_key_title} data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
|
||||
<a accesskey="a" @mouseover=${this.set_access_key_title} data-tip="Open apps list." href="/~core/apps/">apps</a>
|
||||
<a accesskey="e" @mouseover=${this.set_access_key_title} data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
|
||||
<a accesskey="p" @mouseover=${this.set_access_key_title} data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
|
||||
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
|
||||
<span id="requests"></span>
|
||||
${this.render_permissions()}
|
||||
@ -1077,30 +1096,6 @@ window.addEventListener("load", function() {
|
||||
event.preventDefault();
|
||||
trace();
|
||||
});
|
||||
for (let tag of document.getElementsByTagName('a')) {
|
||||
if (tag.accessKey) {
|
||||
tag.classList.add('tooltip_parent');
|
||||
let tooltip = document.createElement('div');
|
||||
tooltip.classList.add('tooltip');
|
||||
if (tag.dataset.tip) {
|
||||
let description = document.createElement('div');
|
||||
description.innerText = tag.dataset.tip;
|
||||
tooltip.appendChild(description);
|
||||
}
|
||||
let parts = tag.accessKeyLabel ? tag.accessKeyLabel.split('+') : [];
|
||||
for (let i = 0; i < parts.length; i++)
|
||||
{
|
||||
let key = parts[i];
|
||||
let kbd = document.createElement('kbd');
|
||||
kbd.innerText = key;
|
||||
tooltip.appendChild(kbd);
|
||||
if (i < parts.length - 1) {
|
||||
tooltip.appendChild(document.createTextNode('+'));
|
||||
}
|
||||
}
|
||||
tag.appendChild(tooltip);
|
||||
}
|
||||
}
|
||||
connectSocket(window.location.pathname);
|
||||
|
||||
if (window.localStorage.getItem('editing') == '1') {
|
||||
|
24
core/core.js
24
core/core.js
@ -47,6 +47,11 @@ const k_global_settings = {
|
||||
default_value: '/~core/apps/',
|
||||
description: 'Default path.',
|
||||
},
|
||||
index_map: {
|
||||
type: 'textarea',
|
||||
default_value: undefined,
|
||||
description: 'Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"',
|
||||
},
|
||||
room: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
@ -947,10 +952,22 @@ function stringResponse(response, data) {
|
||||
}
|
||||
|
||||
loadSettings().then(function() {
|
||||
httpd.all("/login", auth.handler);
|
||||
httpd.all("", function(request, response) {
|
||||
let httpd_impl = (tildefriends.args.httpdc ? httpdc : httpd);
|
||||
httpd_impl.all("/login", auth.handler);
|
||||
httpd_impl.all("", function(request, response) {
|
||||
let match;
|
||||
if (request.uri === "/" || request.uri === "") {
|
||||
try {
|
||||
for (let line of (gGlobalSettings.index_map || '').split('\n')) {
|
||||
let parts = line.split('=');
|
||||
if (parts.length == 2 && request.headers.host.match(new RegExp(parts[0], 'i'))) {
|
||||
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + parts[1], "Content-Length": "0"});
|
||||
return response.end();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + gGlobalSettings.index, "Content-Length": "0"});
|
||||
return response.end();
|
||||
} else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) {
|
||||
@ -987,7 +1004,8 @@ loadSettings().then(function() {
|
||||
return response.end(data);
|
||||
}
|
||||
});
|
||||
httpd.registerSocketHandler("/app/socket", app.socket);
|
||||
httpd_impl.registerSocketHandler("/app/socket", app.socket);
|
||||
httpd_impl.start(tildefriends.http_port);
|
||||
}).catch(function(error) {
|
||||
print('Failed to load settings.');
|
||||
printError({print: print}, error);
|
||||
|
345
core/httpd.js
345
core/httpd.js
@ -203,131 +203,135 @@ function handleWebSocketRequest(request, response, client) {
|
||||
return;
|
||||
}
|
||||
|
||||
response.send = function(message, opCode) {
|
||||
if (opCode === undefined) {
|
||||
opCode = 0x2;
|
||||
}
|
||||
if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
|
||||
message = utf8Encode(message);
|
||||
}
|
||||
let fin = true;
|
||||
let packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
||||
let mask = false;
|
||||
if (message.length < 126) {
|
||||
packet.push((mask ? (1 << 7) : 0) | message.length);
|
||||
} else if (message.length < (1 << 16)) {
|
||||
packet.push((mask ? (1 << 7) : 0) | 126);
|
||||
packet.push((message.length >> 8) & 0xff);
|
||||
packet.push(message.length & 0xff);
|
||||
} else {
|
||||
let high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
|
||||
let low = message.length & 0xffffffff;
|
||||
packet.push((mask ? (1 << 7) : 0) | 127);
|
||||
packet.push((high >> 24) & 0xff);
|
||||
packet.push((high >> 16) & 0xff);
|
||||
packet.push((high >> 8) & 0xff);
|
||||
packet.push((high >> 0) & 0xff);
|
||||
packet.push((low >> 24) & 0xff);
|
||||
packet.push((low >> 16) & 0xff);
|
||||
packet.push((low >> 8) & 0xff);
|
||||
packet.push(low & 0xff);
|
||||
}
|
||||
if (client) {
|
||||
response.send = function(message, opCode) {
|
||||
if (opCode === undefined) {
|
||||
opCode = 0x2;
|
||||
}
|
||||
if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
|
||||
message = utf8Encode(message);
|
||||
}
|
||||
let fin = true;
|
||||
let packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
||||
let mask = false;
|
||||
if (message.length < 126) {
|
||||
packet.push((mask ? (1 << 7) : 0) | message.length);
|
||||
} else if (message.length < (1 << 16)) {
|
||||
packet.push((mask ? (1 << 7) : 0) | 126);
|
||||
packet.push((message.length >> 8) & 0xff);
|
||||
packet.push(message.length & 0xff);
|
||||
} else {
|
||||
let high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
|
||||
let low = message.length & 0xffffffff;
|
||||
packet.push((mask ? (1 << 7) : 0) | 127);
|
||||
packet.push((high >> 24) & 0xff);
|
||||
packet.push((high >> 16) & 0xff);
|
||||
packet.push((high >> 8) & 0xff);
|
||||
packet.push((high >> 0) & 0xff);
|
||||
packet.push((low >> 24) & 0xff);
|
||||
packet.push((low >> 16) & 0xff);
|
||||
packet.push((low >> 8) & 0xff);
|
||||
packet.push(low & 0xff);
|
||||
}
|
||||
|
||||
let array = new Uint8Array(packet.length + message.length);
|
||||
array.set(packet, 0);
|
||||
array.set(message, packet.length);
|
||||
try {
|
||||
return client.write(array);
|
||||
} catch (error) {
|
||||
client.close();
|
||||
throw error;
|
||||
let array = new Uint8Array(packet.length + message.length);
|
||||
array.set(packet, 0);
|
||||
array.set(message, packet.length);
|
||||
try {
|
||||
return client.write(array);
|
||||
} catch (error) {
|
||||
client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
response.onMessage = null;
|
||||
|
||||
let extra_headers = handler.invoke(request, response);
|
||||
|
||||
client.read(function(data) {
|
||||
if (data) {
|
||||
let newBuffer = new Uint8Array(buffer.length + data.length);
|
||||
newBuffer.set(buffer, 0);
|
||||
newBuffer.set(data, buffer.length);
|
||||
buffer = newBuffer;
|
||||
if (client) {
|
||||
client.read(function(data) {
|
||||
if (data) {
|
||||
let newBuffer = new Uint8Array(buffer.length + data.length);
|
||||
newBuffer.set(buffer, 0);
|
||||
newBuffer.set(data, buffer.length);
|
||||
buffer = newBuffer;
|
||||
|
||||
while (buffer.length >= 2) {
|
||||
let bits0 = buffer[0];
|
||||
let bits1 = buffer[1];
|
||||
if (bits1 & (1 << 7) == 0) {
|
||||
// Unmasked message.
|
||||
client.close();
|
||||
}
|
||||
let opCode = bits0 & 0xf;
|
||||
let fin = bits0 & (1 << 7);
|
||||
let payloadLength = bits1 & 0x7f;
|
||||
let maskStart = 2;
|
||||
|
||||
if (payloadLength == 126) {
|
||||
payloadLength = 0;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
payloadLength <<= 8;
|
||||
payloadLength |= buffer[2 + i];
|
||||
while (buffer.length >= 2) {
|
||||
let bits0 = buffer[0];
|
||||
let bits1 = buffer[1];
|
||||
if (bits1 & (1 << 7) == 0) {
|
||||
// Unmasked message.
|
||||
client.close();
|
||||
}
|
||||
maskStart = 4;
|
||||
} else if (payloadLength == 127) {
|
||||
payloadLength = 0;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
payloadLength <<= 8;
|
||||
payloadLength |= buffer[2 + i];
|
||||
}
|
||||
maskStart = 10;
|
||||
}
|
||||
let havePayload = buffer.length >= payloadLength + 2 + 4;
|
||||
if (havePayload) {
|
||||
let mask =
|
||||
buffer[maskStart + 0] |
|
||||
buffer[maskStart + 1] << 8 |
|
||||
buffer[maskStart + 2] << 16 |
|
||||
buffer[maskStart + 3] << 24;
|
||||
let dataStart = maskStart + 4;
|
||||
let payload = buffer.slice(dataStart, dataStart + payloadLength);
|
||||
let decoded = maskBytes(payload, mask);
|
||||
buffer = buffer.slice(dataStart + payloadLength);
|
||||
let opCode = bits0 & 0xf;
|
||||
let fin = bits0 & (1 << 7);
|
||||
let payloadLength = bits1 & 0x7f;
|
||||
let maskStart = 2;
|
||||
|
||||
if (frame) {
|
||||
let newBuffer = new Uint8Array(frame.length + decoded.length);
|
||||
newBuffer.set(frame, 0);
|
||||
newBuffer.set(decoded, frame.length);
|
||||
frame = newBuffer;
|
||||
} else {
|
||||
frame = decoded;
|
||||
}
|
||||
|
||||
if (opCode) {
|
||||
frameOpCode = opCode;
|
||||
}
|
||||
|
||||
if (fin) {
|
||||
if (response.onMessage) {
|
||||
response.onMessage({
|
||||
data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
|
||||
opCode: frameOpCode,
|
||||
});
|
||||
if (payloadLength == 126) {
|
||||
payloadLength = 0;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
payloadLength <<= 8;
|
||||
payloadLength |= buffer[2 + i];
|
||||
}
|
||||
frame = undefined;
|
||||
maskStart = 4;
|
||||
} else if (payloadLength == 127) {
|
||||
payloadLength = 0;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
payloadLength <<= 8;
|
||||
payloadLength |= buffer[2 + i];
|
||||
}
|
||||
maskStart = 10;
|
||||
}
|
||||
let havePayload = buffer.length >= payloadLength + 2 + 4;
|
||||
if (havePayload) {
|
||||
let mask =
|
||||
buffer[maskStart + 0] |
|
||||
buffer[maskStart + 1] << 8 |
|
||||
buffer[maskStart + 2] << 16 |
|
||||
buffer[maskStart + 3] << 24;
|
||||
let dataStart = maskStart + 4;
|
||||
let payload = buffer.slice(dataStart, dataStart + payloadLength);
|
||||
let decoded = maskBytes(payload, mask);
|
||||
buffer = buffer.slice(dataStart + payloadLength);
|
||||
|
||||
if (frame) {
|
||||
let newBuffer = new Uint8Array(frame.length + decoded.length);
|
||||
newBuffer.set(frame, 0);
|
||||
newBuffer.set(decoded, frame.length);
|
||||
frame = newBuffer;
|
||||
} else {
|
||||
frame = decoded;
|
||||
}
|
||||
|
||||
if (opCode) {
|
||||
frameOpCode = opCode;
|
||||
}
|
||||
|
||||
if (fin) {
|
||||
if (response.onMessage) {
|
||||
response.onMessage({
|
||||
data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
|
||||
opCode: frameOpCode,
|
||||
});
|
||||
}
|
||||
frame = undefined;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
response.onClose();
|
||||
client.close();
|
||||
}
|
||||
} else {
|
||||
response.onClose();
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
client.onError(function(error) {
|
||||
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
||||
response.onError(error);
|
||||
});
|
||||
});
|
||||
client.onError(function(error) {
|
||||
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
||||
response.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
let headers = {
|
||||
"Upgrade": "websocket",
|
||||
@ -342,7 +346,6 @@ function handleWebSocketRequest(request, response, client) {
|
||||
|
||||
function webSocketAcceptResponse(key) {
|
||||
let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
let kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
return base64Encode(sha1Digest(key + kMagic));
|
||||
}
|
||||
|
||||
@ -521,73 +524,75 @@ function handleConnection(client) {
|
||||
let kBacklog = 8;
|
||||
let kHost = platform() == 'haiku' ? 'localhost' : '::';
|
||||
|
||||
let socket = new Socket();
|
||||
socket.bind(kHost, tildefriends.http_port).then(function(port) {
|
||||
print("bound to", port);
|
||||
print("checking", tildefriends.args.out_http_port_file);
|
||||
if (tildefriends.args.out_http_port_file) {
|
||||
print("going to write the file");
|
||||
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
|
||||
print("wrote port file", tildefriends.args.out_http_port_file, r);
|
||||
}).catch(function() {
|
||||
print("failed to write port file");
|
||||
});
|
||||
}
|
||||
let listenResult = socket.listen(kBacklog, async function() {
|
||||
try {
|
||||
let client = await socket.accept();
|
||||
client.noDelay = true;
|
||||
handleConnection(client);
|
||||
} catch (error) {
|
||||
logError("[" + new Date() + "] accept error " + error);
|
||||
function start() {
|
||||
let socket = new Socket();
|
||||
socket.bind(kHost, tildefriends.http_port).then(function(port) {
|
||||
print("bound to", port);
|
||||
print("checking", tildefriends.args.out_http_port_file);
|
||||
if (tildefriends.args.out_http_port_file) {
|
||||
print("going to write the file");
|
||||
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
|
||||
print("wrote port file", tildefriends.args.out_http_port_file, r);
|
||||
}).catch(function() {
|
||||
print("failed to write port file");
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
logError("[" + new Date() + "] bind error " + error);
|
||||
});
|
||||
|
||||
if (tildefriends.https_port) {
|
||||
let tls = {};
|
||||
let secureSocket = new Socket();
|
||||
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
|
||||
return secureSocket.listen(kBacklog, async function() {
|
||||
let listenResult = socket.listen(kBacklog, async function() {
|
||||
try {
|
||||
let client = await secureSocket.accept();
|
||||
let client = await socket.accept();
|
||||
client.noDelay = true;
|
||||
client.tls = true;
|
||||
const kCertificatePath = "data/httpd/certificate.pem";
|
||||
const kPrivateKeyPath = "data/httpd/privatekey.pem";
|
||||
|
||||
let stat = await Promise.all([
|
||||
await File.stat(kCertificatePath),
|
||||
await File.stat(kPrivateKeyPath),
|
||||
]);
|
||||
if (!tls.context ||
|
||||
tls.certStat.mtime != stat[0].mtime ||
|
||||
tls.certStat.size != stat[0].size ||
|
||||
tls.keyStat.mtime != stat[1].mtime ||
|
||||
tls.keyStat.size != stat[1].size) {
|
||||
print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
|
||||
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
||||
let certificate = utf8Decode(await File.readFile(kCertificatePath));
|
||||
|
||||
tls.context = new TlsContext();
|
||||
tls.context.setPrivateKey(privateKey);
|
||||
tls.context.setCertificate(certificate);
|
||||
tls.certStat = stat[0];
|
||||
tls.keyStat = stat[1];
|
||||
}
|
||||
|
||||
let result = client.startTls(tls.context);
|
||||
handleConnection(client);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logError("[" + new Date() + "] " + error);
|
||||
logError("[" + new Date() + "] accept error " + error);
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
logError("[" + new Date() + "] bind error " + error);
|
||||
});
|
||||
|
||||
if (tildefriends.https_port) {
|
||||
let tls = {};
|
||||
let secureSocket = new Socket();
|
||||
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
|
||||
return secureSocket.listen(kBacklog, async function() {
|
||||
try {
|
||||
let client = await secureSocket.accept();
|
||||
client.noDelay = true;
|
||||
client.tls = true;
|
||||
const kCertificatePath = "data/httpd/certificate.pem";
|
||||
const kPrivateKeyPath = "data/httpd/privatekey.pem";
|
||||
|
||||
let stat = await Promise.all([
|
||||
await File.stat(kCertificatePath),
|
||||
await File.stat(kPrivateKeyPath),
|
||||
]);
|
||||
if (!tls.context ||
|
||||
tls.certStat.mtime != stat[0].mtime ||
|
||||
tls.certStat.size != stat[0].size ||
|
||||
tls.keyStat.mtime != stat[1].mtime ||
|
||||
tls.keyStat.size != stat[1].size) {
|
||||
print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
|
||||
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
||||
let certificate = utf8Decode(await File.readFile(kCertificatePath));
|
||||
|
||||
tls.context = new TlsContext();
|
||||
tls.context.setPrivateKey(privateKey);
|
||||
tls.context.setCertificate(certificate);
|
||||
tls.certStat = stat[0];
|
||||
tls.keyStat = stat[1];
|
||||
}
|
||||
|
||||
let result = client.startTls(tls.context);
|
||||
handleConnection(client);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logError("[" + new Date() + "] " + error);
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
logError("[" + new Date() + "] bind error " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { all, registerSocketHandler };
|
||||
export { all, start, registerSocketHandler };
|
||||
|
@ -125,34 +125,6 @@ a:active {
|
||||
.cyan { color: #2aa198; }
|
||||
.green { color: #859900; }
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: none;
|
||||
border: 1px solid black;
|
||||
padding: 4px;
|
||||
color: black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tooltip_parent:hover .tooltip {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: #eee;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #b4b4b4;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
font-size: .85em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.permissions {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
8
deps/quickjs/Changelog
vendored
8
deps/quickjs/Changelog
vendored
@ -1,3 +1,11 @@
|
||||
2023-12-09:
|
||||
|
||||
- added Object.hasOwn, {String|Array|TypedArray}.prototype.at,
|
||||
{Array|TypedArray}.prototype.findLast{Index}
|
||||
- BigInt support is enabled even if CONFIG_BIGNUM disabled
|
||||
- updated to Unicode 15.0.0
|
||||
- misc bug fixes
|
||||
|
||||
2021-03-27:
|
||||
|
||||
- faster Array.prototype.push and Array.prototype.unshift
|
||||
|
10
deps/quickjs/Makefile
vendored
10
deps/quickjs/Makefile
vendored
@ -47,7 +47,7 @@ prefix=/usr/local
|
||||
#CONFIG_PROFILE=y
|
||||
# use address sanitizer
|
||||
#CONFIG_ASAN=y
|
||||
# include the code for BigInt/BigFloat/BigDecimal and math mode
|
||||
# include the code for BigFloat/BigDecimal, math mode and faster large integers
|
||||
CONFIG_BIGNUM=y
|
||||
|
||||
OBJDIR=.obj
|
||||
@ -166,11 +166,10 @@ endif
|
||||
|
||||
all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS)
|
||||
|
||||
QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o
|
||||
QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o $(OBJDIR)/libbf.o
|
||||
|
||||
QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS)
|
||||
ifdef CONFIG_BIGNUM
|
||||
QJS_LIB_OBJS+=$(OBJDIR)/libbf.o
|
||||
QJS_OBJS+=$(OBJDIR)/qjscalc.o
|
||||
endif
|
||||
|
||||
@ -317,10 +316,7 @@ endif
|
||||
HELLO_SRCS=examples/hello.js
|
||||
HELLO_OPTS=-fno-string-normalize -fno-map -fno-promise -fno-typedarray \
|
||||
-fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \
|
||||
-fno-date -fno-module-loader
|
||||
ifdef CONFIG_BIGNUM
|
||||
HELLO_OPTS+=-fno-bigint
|
||||
endif
|
||||
-fno-date -fno-module-loader -fno-bigint
|
||||
|
||||
hello.c: $(QJSC) $(HELLO_SRCS)
|
||||
$(QJSC) -e $(HELLO_OPTS) -o $@ $(HELLO_SRCS)
|
||||
|
4
deps/quickjs/TODO
vendored
4
deps/quickjs/TODO
vendored
@ -66,5 +66,5 @@ Optimization ideas:
|
||||
Test262o: 0/11262 errors, 463 excluded
|
||||
Test262o commit: 7da91bceb9ce7613f87db47ddd1292a2dda58b42 (es5-tests branch)
|
||||
|
||||
Result: 35/75280 errors, 909 excluded, 585 skipped
|
||||
Test262 commit: 31126581e7290f9233c29cefd93f66c6ac78f1c9
|
||||
Result: 41/76133 errors, 1497 excluded, 8650 skipped
|
||||
Test262 commit: 6cbb6da9473c56d95358d8e679c5a6d2b4574efb
|
||||
|
2
deps/quickjs/VERSION
vendored
2
deps/quickjs/VERSION
vendored
@ -1 +1 @@
|
||||
2021-03-27
|
||||
2023-12-09
|
||||
|
BIN
deps/quickjs/doc/quickjs.pdf
vendored
BIN
deps/quickjs/doc/quickjs.pdf
vendored
Binary file not shown.
41
deps/quickjs/libbf.c
vendored
41
deps/quickjs/libbf.c
vendored
@ -37,10 +37,12 @@
|
||||
|
||||
/* enable it to check the multiplication result */
|
||||
//#define USE_MUL_CHECK
|
||||
#ifdef CONFIG_BIGNUM
|
||||
/* enable it to use FFT/NTT multiplication */
|
||||
#define USE_FFT_MUL
|
||||
/* enable decimal floating point support */
|
||||
#define USE_BF_DEC
|
||||
#endif
|
||||
|
||||
//#define inline __attribute__((always_inline))
|
||||
|
||||
@ -164,6 +166,21 @@ static inline slimb_t sat_add(slimb_t a, slimb_t b)
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline __maybe_unused limb_t shrd(limb_t low, limb_t high, long shift)
|
||||
{
|
||||
if (shift != 0)
|
||||
low = (low >> shift) | (high << (LIMB_BITS - shift));
|
||||
return low;
|
||||
}
|
||||
|
||||
static inline __maybe_unused limb_t shld(limb_t a1, limb_t a0, long shift)
|
||||
{
|
||||
if (shift != 0)
|
||||
return (a1 << shift) | (a0 >> (LIMB_BITS - shift));
|
||||
else
|
||||
return a1;
|
||||
}
|
||||
|
||||
#define malloc(s) malloc_is_forbidden(s)
|
||||
#define free(p) free_is_forbidden(p)
|
||||
#define realloc(p, s) realloc_is_forbidden(p, s)
|
||||
@ -236,7 +253,7 @@ int bf_set_ui(bf_t *r, uint64_t a)
|
||||
a1 = a >> 32;
|
||||
shift = clz(a1);
|
||||
r->tab[0] = a0 << shift;
|
||||
r->tab[1] = (a1 << shift) | (a0 >> (LIMB_BITS - shift));
|
||||
r->tab[1] = shld(a1, a0, shift);
|
||||
r->expn = 2 * LIMB_BITS - shift;
|
||||
}
|
||||
#endif
|
||||
@ -1585,7 +1602,9 @@ int bf_mul(bf_t *r, const bf_t *a, const bf_t *b, limb_t prec,
|
||||
r = &tmp;
|
||||
}
|
||||
if (bf_resize(r, a_len + b_len)) {
|
||||
#ifdef USE_FFT_MUL
|
||||
fail:
|
||||
#endif
|
||||
bf_set_nan(r);
|
||||
ret = BF_ST_MEM_ERROR;
|
||||
goto done;
|
||||
@ -2282,11 +2301,14 @@ static int bf_pow_ui_ui(bf_t *r, limb_t a1, limb_t b,
|
||||
bf_t a;
|
||||
int ret;
|
||||
|
||||
#ifdef USE_BF_DEC
|
||||
if (a1 == 10 && b <= LIMB_DIGITS) {
|
||||
/* use precomputed powers. We do not round at this point
|
||||
because we expect the caller to do it */
|
||||
ret = bf_set_ui(r, mp_pow_dec[b]);
|
||||
} else {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
bf_init(r->ctx, &a);
|
||||
ret = bf_set_ui(&a, a1);
|
||||
ret |= bf_pow_ui(r, &a, b, prec, flags);
|
||||
@ -5392,21 +5414,6 @@ int bf_acos(bf_t *r, const bf_t *a, limb_t prec, bf_flags_t flags)
|
||||
|
||||
#endif /* LIMB_BITS != 64 */
|
||||
|
||||
static inline __maybe_unused limb_t shrd(limb_t low, limb_t high, long shift)
|
||||
{
|
||||
if (shift != 0)
|
||||
low = (low >> shift) | (high << (LIMB_BITS - shift));
|
||||
return low;
|
||||
}
|
||||
|
||||
static inline __maybe_unused limb_t shld(limb_t a1, limb_t a0, long shift)
|
||||
{
|
||||
if (shift != 0)
|
||||
return (a1 << shift) | (a0 >> (LIMB_BITS - shift));
|
||||
else
|
||||
return a1;
|
||||
}
|
||||
|
||||
#if LIMB_DIGITS == 19
|
||||
|
||||
/* WARNING: hardcoded for b = 1e19. It is assumed that:
|
||||
|
23
deps/quickjs/libregexp.c
vendored
23
deps/quickjs/libregexp.c
vendored
@ -1071,11 +1071,10 @@ static int re_is_simple_quantifier(const uint8_t *bc_buf, int bc_buf_len)
|
||||
}
|
||||
|
||||
/* '*pp' is the first char after '<' */
|
||||
static int re_parse_group_name(char *buf, int buf_size,
|
||||
const uint8_t **pp, BOOL is_utf16)
|
||||
static int re_parse_group_name(char *buf, int buf_size, const uint8_t **pp)
|
||||
{
|
||||
const uint8_t *p;
|
||||
uint32_t c;
|
||||
const uint8_t *p, *p1;
|
||||
uint32_t c, d;
|
||||
char *q;
|
||||
|
||||
p = *pp;
|
||||
@ -1086,11 +1085,18 @@ static int re_parse_group_name(char *buf, int buf_size,
|
||||
p++;
|
||||
if (*p != 'u')
|
||||
return -1;
|
||||
c = lre_parse_escape(&p, is_utf16 * 2);
|
||||
c = lre_parse_escape(&p, 2); // accept surrogate pairs
|
||||
} else if (c == '>') {
|
||||
break;
|
||||
} else if (c >= 128) {
|
||||
c = unicode_from_utf8(p, UTF8_CHAR_LEN_MAX, &p);
|
||||
if (c >= 0xD800 && c <= 0xDBFF) {
|
||||
d = unicode_from_utf8(p, UTF8_CHAR_LEN_MAX, &p1);
|
||||
if (d >= 0xDC00 && d <= 0xDFFF) {
|
||||
c = 0x10000 + 0x400 * (c - 0xD800) + (d - 0xDC00);
|
||||
p = p1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p++;
|
||||
}
|
||||
@ -1140,8 +1146,7 @@ static int re_parse_captures(REParseState *s, int *phas_named_captures,
|
||||
/* potential named capture */
|
||||
if (capture_name) {
|
||||
p += 3;
|
||||
if (re_parse_group_name(name, sizeof(name), &p,
|
||||
s->is_utf16) == 0) {
|
||||
if (re_parse_group_name(name, sizeof(name), &p) == 0) {
|
||||
if (!strcmp(name, capture_name))
|
||||
return capture_index;
|
||||
}
|
||||
@ -1314,7 +1319,7 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir)
|
||||
} else if (p[2] == '<') {
|
||||
p += 3;
|
||||
if (re_parse_group_name(s->u.tmp_buf, sizeof(s->u.tmp_buf),
|
||||
&p, s->is_utf16)) {
|
||||
&p)) {
|
||||
return re_parse_error(s, "invalid group name");
|
||||
}
|
||||
if (find_group_name(s, s->u.tmp_buf) > 0) {
|
||||
@ -1378,7 +1383,7 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir)
|
||||
}
|
||||
p1 += 3;
|
||||
if (re_parse_group_name(s->u.tmp_buf, sizeof(s->u.tmp_buf),
|
||||
&p1, s->is_utf16)) {
|
||||
&p1)) {
|
||||
if (s->is_utf16 || re_has_named_captures(s))
|
||||
return re_parse_error(s, "invalid group name");
|
||||
else
|
||||
|
4294
deps/quickjs/libunicode-table.h
vendored
4294
deps/quickjs/libunicode-table.h
vendored
File diff suppressed because it is too large
Load Diff
2
deps/quickjs/qjs.c
vendored
2
deps/quickjs/qjs.c
vendored
@ -454,8 +454,10 @@ int main(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CONFIG_BIGNUM
|
||||
if (load_jscalc)
|
||||
bignum_ext = 1;
|
||||
#endif
|
||||
|
||||
if (trace_memory) {
|
||||
js_trace_malloc_init(&trace_data);
|
||||
|
2
deps/quickjs/qjsc.c
vendored
2
deps/quickjs/qjsc.c
vendored
@ -76,9 +76,7 @@ static const FeatureEntry feature_list[] = {
|
||||
{ "promise", "Promise" },
|
||||
#define FE_MODULE_LOADER 9
|
||||
{ "module-loader", NULL },
|
||||
#ifdef CONFIG_BIGNUM
|
||||
{ "bigint", "BigInt" },
|
||||
#endif
|
||||
};
|
||||
|
||||
void namelist_add(namelist_t *lp, const char *name, const char *short_name,
|
||||
|
6
deps/quickjs/quickjs-atom.h
vendored
6
deps/quickjs/quickjs-atom.h
vendored
@ -169,8 +169,8 @@ DEF(groups, "groups")
|
||||
DEF(status, "status")
|
||||
DEF(reason, "reason")
|
||||
DEF(globalThis, "globalThis")
|
||||
#ifdef CONFIG_BIGNUM
|
||||
DEF(bigint, "bigint")
|
||||
#ifdef CONFIG_BIGNUM
|
||||
DEF(bigfloat, "bigfloat")
|
||||
DEF(bigdecimal, "bigdecimal")
|
||||
DEF(roundingMode, "roundingMode")
|
||||
@ -209,15 +209,13 @@ DEF(Int16Array, "Int16Array")
|
||||
DEF(Uint16Array, "Uint16Array")
|
||||
DEF(Int32Array, "Int32Array")
|
||||
DEF(Uint32Array, "Uint32Array")
|
||||
#ifdef CONFIG_BIGNUM
|
||||
DEF(BigInt64Array, "BigInt64Array")
|
||||
DEF(BigUint64Array, "BigUint64Array")
|
||||
#endif
|
||||
DEF(Float32Array, "Float32Array")
|
||||
DEF(Float64Array, "Float64Array")
|
||||
DEF(DataView, "DataView")
|
||||
#ifdef CONFIG_BIGNUM
|
||||
DEF(BigInt, "BigInt")
|
||||
#ifdef CONFIG_BIGNUM
|
||||
DEF(BigFloat, "BigFloat")
|
||||
DEF(BigFloatEnv, "BigFloatEnv")
|
||||
DEF(BigDecimal, "BigDecimal")
|
||||
|
2
deps/quickjs/quickjs-opcode.h
vendored
2
deps/quickjs/quickjs-opcode.h
vendored
@ -279,7 +279,7 @@ def( scope_get_ref, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase
|
||||
def(scope_put_var_init, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */
|
||||
def(scope_get_private_field, 7, 1, 1, atom_u16) /* obj -> value, emitted in phase 1, removed in phase 2 */
|
||||
def(scope_get_private_field2, 7, 1, 2, atom_u16) /* obj -> obj value, emitted in phase 1, removed in phase 2 */
|
||||
def(scope_put_private_field, 7, 1, 1, atom_u16) /* obj value ->, emitted in phase 1, removed in phase 2 */
|
||||
def(scope_put_private_field, 7, 2, 0, atom_u16) /* obj value ->, emitted in phase 1, removed in phase 2 */
|
||||
|
||||
def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */
|
||||
|
||||
|
2064
deps/quickjs/quickjs.c
vendored
2064
deps/quickjs/quickjs.c
vendored
File diff suppressed because it is too large
Load Diff
6
deps/quickjs/quickjs.h
vendored
6
deps/quickjs/quickjs.h
vendored
@ -733,13 +733,13 @@ JSValue JS_GetPropertyStr(JSContext *ctx, JSValueConst this_obj,
|
||||
JSValue JS_GetPropertyUint32(JSContext *ctx, JSValueConst this_obj,
|
||||
uint32_t idx);
|
||||
|
||||
int JS_SetPropertyInternal(JSContext *ctx, JSValueConst this_obj,
|
||||
JSAtom prop, JSValue val,
|
||||
int JS_SetPropertyInternal(JSContext *ctx, JSValueConst obj,
|
||||
JSAtom prop, JSValue val, JSValueConst this_obj,
|
||||
int flags);
|
||||
static inline int JS_SetProperty(JSContext *ctx, JSValueConst this_obj,
|
||||
JSAtom prop, JSValue val)
|
||||
{
|
||||
return JS_SetPropertyInternal(ctx, this_obj, prop, val, JS_PROP_THROW);
|
||||
return JS_SetPropertyInternal(ctx, this_obj, prop, val, this_obj, JS_PROP_THROW);
|
||||
}
|
||||
int JS_SetPropertyUint32(JSContext *ctx, JSValueConst this_obj,
|
||||
uint32_t idx, JSValue val);
|
||||
|
44
deps/quickjs/test262.conf
vendored
44
deps/quickjs/test262.conf
vendored
@ -47,16 +47,25 @@ testdir=test262/test
|
||||
# Standard language features and proposed extensions
|
||||
# list the features that are included
|
||||
# skipped features are tagged as such to avoid warnings
|
||||
# Keep this list alpha-sorted (:sort i in vim)
|
||||
|
||||
__getter__
|
||||
__proto__
|
||||
__setter__
|
||||
AggregateError
|
||||
align-detached-buffer-semantics-with-web-reality
|
||||
arbitrary-module-namespace-names=skip
|
||||
Array.prototype.at=skip
|
||||
array-find-from-last
|
||||
array-grouping=skip
|
||||
Array.fromAsync=skip
|
||||
Array.prototype.at
|
||||
Array.prototype.flat
|
||||
Array.prototype.flatMap
|
||||
Array.prototype.flatten
|
||||
Array.prototype.includes
|
||||
Array.prototype.values
|
||||
ArrayBuffer
|
||||
arraybuffer-transfer=skip
|
||||
arrow-function
|
||||
async-functions
|
||||
async-iteration
|
||||
@ -64,12 +73,15 @@ Atomics
|
||||
Atomics.waitAsync=skip
|
||||
BigInt
|
||||
caller
|
||||
change-array-by-copy=skip
|
||||
class
|
||||
class-fields-private
|
||||
class-fields-private-in=skip
|
||||
class-fields-public
|
||||
class-methods-private
|
||||
class-static-fields-public
|
||||
class-static-block=skip
|
||||
class-static-fields-private
|
||||
class-static-fields-public
|
||||
class-static-methods-private
|
||||
cleanupSome=skip
|
||||
coalesce-expression
|
||||
@ -85,14 +97,17 @@ DataView.prototype.getInt8
|
||||
DataView.prototype.getUint16
|
||||
DataView.prototype.getUint32
|
||||
DataView.prototype.setUint8
|
||||
decorators=skip
|
||||
default-parameters
|
||||
destructuring-assignment
|
||||
destructuring-binding
|
||||
dynamic-import
|
||||
error-cause=skip
|
||||
exponentiation
|
||||
export-star-as-namespace-from-module
|
||||
FinalizationGroup=skip
|
||||
FinalizationRegistry=skip
|
||||
FinalizationRegistry.prototype.cleanupSome=skip
|
||||
FinalizationRegistry=skip
|
||||
Float32Array
|
||||
Float64Array
|
||||
for-in-order
|
||||
@ -101,11 +116,16 @@ generators
|
||||
globalThis
|
||||
hashbang
|
||||
host-gc-required=skip
|
||||
import-assertions=skip
|
||||
import-attributes=skip
|
||||
import.meta
|
||||
Int16Array
|
||||
Int32Array
|
||||
Int8Array
|
||||
IsHTMLDDA
|
||||
iterator-helpers=skip
|
||||
json-modules=skip
|
||||
json-parse-with-source=skip
|
||||
json-superset
|
||||
legacy-regexp=skip
|
||||
let
|
||||
@ -116,10 +136,12 @@ numeric-separator-literal
|
||||
object-rest
|
||||
object-spread
|
||||
Object.fromEntries
|
||||
Object.hasOwn
|
||||
Object.is
|
||||
optional-catch-binding
|
||||
optional-chaining
|
||||
Promise
|
||||
promise-with-resolvers=skip
|
||||
Promise.allSettled
|
||||
Promise.any
|
||||
Promise.prototype.finally
|
||||
@ -130,20 +152,27 @@ Reflect.construct
|
||||
Reflect.set
|
||||
Reflect.setPrototypeOf
|
||||
regexp-dotall
|
||||
regexp-duplicate-named-groups=skip
|
||||
regexp-lookbehind
|
||||
regexp-match-indices=skip
|
||||
regexp-named-groups
|
||||
regexp-unicode-property-escapes
|
||||
regexp-v-flag=skip
|
||||
resizable-arraybuffer=skip
|
||||
rest-parameters
|
||||
Set
|
||||
set-methods=skip
|
||||
ShadowRealm=skip
|
||||
SharedArrayBuffer
|
||||
string-trimming
|
||||
String.fromCodePoint
|
||||
String.prototype.at
|
||||
String.prototype.endsWith
|
||||
String.prototype.includes
|
||||
String.prototype.at=skip
|
||||
String.prototype.isWellFormed=skip
|
||||
String.prototype.matchAll
|
||||
String.prototype.replaceAll
|
||||
String.prototype.toWellFormed=skip
|
||||
String.prototype.trimEnd
|
||||
String.prototype.trimStart
|
||||
super
|
||||
@ -162,11 +191,13 @@ Symbol.split
|
||||
Symbol.toPrimitive
|
||||
Symbol.toStringTag
|
||||
Symbol.unscopables
|
||||
symbols-as-weakmap-keys=skip
|
||||
tail-call-optimization=skip
|
||||
template
|
||||
Temporal=skip
|
||||
top-level-await=skip
|
||||
TypedArray
|
||||
TypedArray.prototype.at=skip
|
||||
TypedArray.prototype.at
|
||||
u180e
|
||||
Uint16Array
|
||||
Uint32Array
|
||||
@ -176,9 +207,6 @@ WeakMap
|
||||
WeakRef=skip
|
||||
WeakSet
|
||||
well-formed-json-stringify
|
||||
__getter__
|
||||
__proto__
|
||||
__setter__
|
||||
|
||||
[exclude]
|
||||
# list excluded tests and directories here
|
||||
|
64
deps/quickjs/test262_errors.txt
vendored
64
deps/quickjs/test262_errors.txt
vendored
@ -1,35 +1,41 @@
|
||||
test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: Test262Error: Expected a ReferenceError but got a ReferenceError
|
||||
test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: strict mode: Test262Error: Expected a ReferenceError but got a ReferenceError
|
||||
test262/test/built-ins/RegExp/named-groups/non-unicode-property-names-valid.js:46: SyntaxError: invalid group name
|
||||
test262/test/built-ins/RegExp/named-groups/non-unicode-property-names-valid.js:46: strict mode: SyntaxError: invalid group name
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/BigInt/detached-buffer.js:46: Test262Error: (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/BigInt/detached-buffer.js:46: strict mode: Test262Error: (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/detached-buffer.js:47: Test262Error: (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/detached-buffer.js:47: strict mode: Test262Error: (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/detached-buffer-realm.js:37: strict mode: TypeError: out-of-bound numeric index (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/detached-buffer.js:34: TypeError: cannot convert bigint to number (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/detached-buffer.js:32: strict mode: TypeError: out-of-bound numeric index (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-minus-zero.js:20: Test262Error: Reflect.set("new TA([42n])", "-0", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-minus-zero.js:20: strict mode: Test262Error: Reflect.set("new TA([42n])", "-0", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-not-integer.js:21: Test262Error: Reflect.set("new TA([42n])", "1.1", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-not-integer.js:21: strict mode: Test262Error: Reflect.set("new TA([42n])", "1.1", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-out-of-bounds.js:27: Test262Error: Reflect.set("new TA([42n])", "-1", 1n) must return false Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-out-of-bounds.js:27: strict mode: Test262Error: Reflect.set("new TA([42n])", "-1", 1n) must return false Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/tonumber-value-detached-buffer.js:24: Test262Error: Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/tonumber-value-detached-buffer.js:24: strict mode: Test262Error: Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/detached-buffer-realm.js:37: strict mode: TypeError: out-of-bound numeric index (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/detached-buffer.js:32: strict mode: TypeError: out-of-bound numeric index (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-minus-zero.js:22: Test262Error: Reflect.set(sample, "-0", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-minus-zero.js:22: strict mode: Test262Error: Reflect.set(sample, "-0", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-not-integer.js:22: Test262Error: Reflect.set(sample, "1.1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-not-integer.js:22: strict mode: Test262Error: Reflect.set(sample, "1.1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds.js:22: Test262Error: Reflect.set(sample, "-1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds.js:22: strict mode: Test262Error: Reflect.set(sample, "-1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/tonumber-value-detached-buffer.js:39: Test262Error: Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArrayConstructors/internals/Set/tonumber-value-detached-buffer.js:39: strict mode: Test262Error: Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
|
||||
test262/test/annexB/language/eval-code/direct/script-decl-lex-collision-in-sloppy-mode.js:13: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all
|
||||
test262/test/built-ins/AsyncGeneratorPrototype/return/return-state-completed-broken-promise.js:53: TypeError: $DONE() not called
|
||||
test262/test/built-ins/AsyncGeneratorPrototype/return/return-state-completed-broken-promise.js:53: strict mode: TypeError: $DONE() not called
|
||||
test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-broken-promise.js:34: TypeError: $DONE() not called
|
||||
test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-broken-promise.js:34: strict mode: TypeError: $DONE() not called
|
||||
test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-broken-promise-try-catch.js:39: TypeError: $DONE() not called
|
||||
test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-broken-promise-try-catch.js:39: strict mode: TypeError: $DONE() not called
|
||||
test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: Test262Error: Expected a ReferenceError but got a different error constructor with the same name
|
||||
test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: strict mode: Test262Error: Expected a ReferenceError but got a different error constructor with the same name
|
||||
test262/test/built-ins/RegExp/lookahead-quantifier-match-groups.js:27: Test262Error: Expected [a, abc] and [a, undefined] to have the same contents. ? quantifier
|
||||
test262/test/built-ins/RegExp/lookahead-quantifier-match-groups.js:27: strict mode: Test262Error: Expected [a, abc] and [a, undefined] to have the same contents. ? quantifier
|
||||
test262/test/built-ins/RegExp/unicode_full_case_folding.js:20: Test262Error: \u0390 does not match \u1fd3
|
||||
test262/test/built-ins/RegExp/unicode_full_case_folding.js:20: strict mode: Test262Error: \u0390 does not match \u1fd3
|
||||
test262/test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js:62: Test262Error: String.prototype.localeCompare considers ö (\u006f\u0308) ≠ ö (\u00f6).
|
||||
test262/test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js:62: strict mode: Test262Error: String.prototype.localeCompare considers ö (\u006f\u0308) ≠ ö (\u00f6).
|
||||
test262/test/built-ins/TypedArray/prototype/sort/sort-tonumber.js:30: TypeError: ArrayBuffer is detached (Testing with Float64Array.)
|
||||
test262/test/built-ins/TypedArray/prototype/sort/sort-tonumber.js:30: strict mode: TypeError: ArrayBuffer is detached (Testing with Float64Array.)
|
||||
test262/test/language/expressions/assignment/target-member-computed-reference-null.js:32: Test262Error: Expected a DummyError but got a TypeError
|
||||
test262/test/language/expressions/assignment/target-member-computed-reference-null.js:32: strict mode: Test262Error: Expected a DummyError but got a TypeError
|
||||
test262/test/language/expressions/assignment/target-member-computed-reference-undefined.js:32: Test262Error: Expected a DummyError but got a TypeError
|
||||
test262/test/language/expressions/assignment/target-member-computed-reference-undefined.js:32: strict mode: Test262Error: Expected a DummyError but got a TypeError
|
||||
test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: TypeError: $DONE() not called
|
||||
test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: strict mode: TypeError: $DONE() not called
|
||||
test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:21: TypeError: cannot read property 'c' of undefined
|
||||
test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:15: strict mode: TypeError: cannot read property '_b' of undefined
|
||||
test262/test/language/global-code/script-decl-lex-var-declared-via-eval-sloppy.js:13: Test262Error: variable Expected a SyntaxError to be thrown but no exception was thrown at all
|
||||
test262/test/language/module-code/namespace/internals/define-own-property.js:30: Test262Error: Object.freeze: 1 Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/async-generator/yield-star-promise-not-unwrapped.js:25: TypeError: $DONE() not called
|
||||
test262/test/language/statements/async-generator/yield-star-promise-not-unwrapped.js:25: strict mode: TypeError: $DONE() not called
|
||||
test262/test/language/statements/async-generator/yield-star-return-then-getter-ticks.js:131: TypeError: $DONE() not called
|
||||
test262/test/language/statements/async-generator/yield-star-return-then-getter-ticks.js:131: strict mode: TypeError: $DONE() not called
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation-get-and-set.js:33: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation-get-and-set.js:33: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation-get.js:32: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation-get.js:32: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation-set.js:32: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation-set.js:32: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation.js:32: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/class/elements/private-method-double-initialisation.js:32: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
|
||||
test262/test/language/statements/for-of/head-lhs-async-invalid.js:14: unexpected error type: Test262: This statement should not be evaluated.
|
||||
test262/test/language/statements/for-of/head-lhs-async-invalid.js:14: strict mode: unexpected error type: Test262: This statement should not be evaluated.
|
||||
|
10
deps/quickjs/unicode_download.sh
vendored
10
deps/quickjs/unicode_download.sh
vendored
@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
url="ftp://ftp.unicode.org/Public/13.0.0/ucd"
|
||||
url="ftp://ftp.unicode.org/Public/15.0.0/ucd"
|
||||
emoji_url="${url}/emoji/emoji-data.txt"
|
||||
|
||||
files="CaseFolding.txt DerivedNormalizationProps.txt PropList.txt \
|
||||
@ -11,9 +11,9 @@ PropertyValueAliases.txt"
|
||||
|
||||
mkdir -p unicode
|
||||
|
||||
#for f in $files; do
|
||||
# g="${url}/${f}"
|
||||
# wget $g -O unicode/$f
|
||||
#done
|
||||
for f in $files; do
|
||||
g="${url}/${f}"
|
||||
wget $g -O unicode/$f
|
||||
done
|
||||
|
||||
wget $emoji_url -O unicode/emoji-data.txt
|
||||
|
7
deps/quickjs/unicode_gen_def.h
vendored
7
deps/quickjs/unicode_gen_def.h
vendored
@ -72,6 +72,7 @@ DEF(Coptic, "Copt,Qaac")
|
||||
DEF(Cuneiform, "Xsux")
|
||||
DEF(Cypriot, "Cprt")
|
||||
DEF(Cyrillic, "Cyrl")
|
||||
DEF(Cypro_Minoan, "Cpmn")
|
||||
DEF(Deseret, "Dsrt")
|
||||
DEF(Devanagari, "Deva")
|
||||
DEF(Dives_Akuru, "Diak")
|
||||
@ -104,6 +105,7 @@ DEF(Javanese, "Java")
|
||||
DEF(Kaithi, "Kthi")
|
||||
DEF(Kannada, "Knda")
|
||||
DEF(Katakana, "Kana")
|
||||
DEF(Kawi, "Kawi")
|
||||
DEF(Kayah_Li, "Kali")
|
||||
DEF(Kharoshthi, "Khar")
|
||||
DEF(Khmer, "Khmr")
|
||||
@ -138,6 +140,7 @@ DEF(Mro, "Mroo")
|
||||
DEF(Multani, "Mult")
|
||||
DEF(Myanmar, "Mymr")
|
||||
DEF(Nabataean, "Nbat")
|
||||
DEF(Nag_Mundari, "Nagm")
|
||||
DEF(Nandinagari, "Nand")
|
||||
DEF(New_Tai_Lue, "Talu")
|
||||
DEF(Newa, "Newa")
|
||||
@ -154,6 +157,7 @@ DEF(Old_Persian, "Xpeo")
|
||||
DEF(Old_Sogdian, "Sogo")
|
||||
DEF(Old_South_Arabian, "Sarb")
|
||||
DEF(Old_Turkic, "Orkh")
|
||||
DEF(Old_Uyghur, "Ougr")
|
||||
DEF(Oriya, "Orya")
|
||||
DEF(Osage, "Osge")
|
||||
DEF(Osmanya, "Osma")
|
||||
@ -192,8 +196,11 @@ DEF(Thai, "Thai")
|
||||
DEF(Tibetan, "Tibt")
|
||||
DEF(Tifinagh, "Tfng")
|
||||
DEF(Tirhuta, "Tirh")
|
||||
DEF(Tangsa, "Tnsa")
|
||||
DEF(Toto, "Toto")
|
||||
DEF(Ugaritic, "Ugar")
|
||||
DEF(Vai, "Vaii")
|
||||
DEF(Vithkuqi, "Vith")
|
||||
DEF(Wancho, "Wcho")
|
||||
DEF(Warang_Citi, "Wara")
|
||||
DEF(Yezidi, "Yezi")
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.unprompted.tildefriends"
|
||||
android:versionCode="13"
|
||||
android:versionName="0.0.13">
|
||||
android:versionCode="14"
|
||||
android:versionName="0.0.14">
|
||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
|
666
src/http.c
Normal file
666
src/http.c
Normal file
@ -0,0 +1,666 @@
|
||||
#include "http.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "util.js.h"
|
||||
|
||||
#include "picohttpparser.h"
|
||||
#include "uv.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdalign.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
typedef struct _tf_http_connection_t
|
||||
{
|
||||
tf_http_t* http;
|
||||
uv_tcp_t tcp;
|
||||
uv_shutdown_t shutdown;
|
||||
|
||||
int ref_count;
|
||||
|
||||
const char* method;
|
||||
const char* path;
|
||||
const char* query;
|
||||
int minor_version;
|
||||
|
||||
char headers_buffer[8192];
|
||||
size_t headers_buffer_length;
|
||||
int parsed_length;
|
||||
|
||||
char buffer[8192];
|
||||
size_t buffer_length;
|
||||
|
||||
struct phr_header headers[32];
|
||||
int headers_length;
|
||||
bool headers_done;
|
||||
|
||||
int flags;
|
||||
tf_http_callback_t* callback;
|
||||
tf_http_request_t* request;
|
||||
void* user_data;
|
||||
|
||||
bool is_websocket;
|
||||
int websocket_message_index;
|
||||
void* body;
|
||||
size_t body_length;
|
||||
size_t content_length;
|
||||
bool connection_close;
|
||||
} tf_http_connection_t;
|
||||
|
||||
typedef struct _tf_http_handler_t
|
||||
{
|
||||
const char* pattern;
|
||||
int flags;
|
||||
tf_http_callback_t* callback;
|
||||
void* user_data;
|
||||
} tf_http_handler_t;
|
||||
|
||||
typedef struct _tf_http_t
|
||||
{
|
||||
uv_tcp_t** listeners;
|
||||
int listeners_count;
|
||||
|
||||
tf_http_connection_t** connections;
|
||||
int connections_count;
|
||||
|
||||
tf_http_handler_t* handlers;
|
||||
int handlers_count;
|
||||
|
||||
int pending_closes;
|
||||
uv_loop_t* loop;
|
||||
} tf_http_t;
|
||||
|
||||
static const char* _http_connection_get_header(const tf_http_connection_t* connection, const char* name);
|
||||
static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason);
|
||||
static const char* _http_status_text(int status);
|
||||
|
||||
tf_http_t* tf_http_create(uv_loop_t* loop)
|
||||
{
|
||||
tf_http_t* http = tf_malloc(sizeof(tf_http_t));
|
||||
*http = (tf_http_t)
|
||||
{
|
||||
.loop = loop,
|
||||
};
|
||||
return http;
|
||||
}
|
||||
|
||||
void _http_allocate_buffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf)
|
||||
{
|
||||
tf_http_connection_t* connection = handle->data;
|
||||
if (!connection->headers_done)
|
||||
{
|
||||
*buf = uv_buf_init(connection->headers_buffer + connection->headers_buffer_length, sizeof(connection->headers_buffer) - connection->headers_buffer_length);
|
||||
}
|
||||
else
|
||||
{
|
||||
*buf = uv_buf_init(connection->buffer + connection->buffer_length, sizeof(connection->buffer) - connection->buffer_length);
|
||||
}
|
||||
}
|
||||
|
||||
bool _http_find_handler(tf_http_t* http, const char* path, int flags, tf_http_callback_t** out_callback, void** out_user_data)
|
||||
{
|
||||
for (int i = 0; i < http->handlers_count; i++)
|
||||
{
|
||||
if (http->handlers[i].flags == flags &&
|
||||
(!http->handlers[i].pattern ||
|
||||
strcmp(path, http->handlers[i].pattern) == 0 ||
|
||||
(strncmp(path, http->handlers[i].pattern, strlen(http->handlers[i].pattern)) == 0 && path[strlen(http->handlers[i].pattern)] == '/')))
|
||||
{
|
||||
*out_callback = http->handlers[i].callback;
|
||||
*out_user_data = http->handlers[i].user_data;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void _http_on_write(uv_write_t* write, int status)
|
||||
{
|
||||
tf_free(write);
|
||||
}
|
||||
|
||||
static void _http_connection_on_close(uv_handle_t* handle)
|
||||
{
|
||||
tf_http_connection_t* connection = handle->data;
|
||||
handle->data = NULL;
|
||||
_http_connection_destroy(connection, "handle closed");
|
||||
}
|
||||
|
||||
static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason)
|
||||
{
|
||||
if (connection->tcp.data)
|
||||
{
|
||||
uv_close((uv_handle_t*)&connection->tcp, _http_connection_on_close);
|
||||
}
|
||||
else if (connection->ref_count == 0)
|
||||
{
|
||||
tf_http_t* http = connection->http;
|
||||
for (int i = 0; i < http->connections_count; i++)
|
||||
{
|
||||
if (http->connections[i] == connection)
|
||||
{
|
||||
http->connections[i] = http->connections[--http->connections_count];
|
||||
}
|
||||
}
|
||||
if (connection->body)
|
||||
{
|
||||
tf_free(connection->body);
|
||||
connection->body = NULL;
|
||||
}
|
||||
tf_free(connection);
|
||||
}
|
||||
}
|
||||
|
||||
static void _http_builtin_404_handler(tf_http_request_t* request)
|
||||
{
|
||||
const char* k_payload = _http_status_text(404);
|
||||
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
|
||||
}
|
||||
|
||||
static void _http_reset_connection(tf_http_connection_t* connection)
|
||||
{
|
||||
connection->headers_done = false;
|
||||
connection->headers_buffer_length = 0;
|
||||
connection->body_length = 0;
|
||||
connection->content_length = 0;
|
||||
connection->parsed_length = 0;
|
||||
}
|
||||
|
||||
static void _http_websocket_mask_in_place(uint8_t* p, uint32_t mask, size_t size)
|
||||
{
|
||||
int i = 0;
|
||||
for (; ((intptr_t)(p + i)) % alignof(uint32_t); i++)
|
||||
{
|
||||
p[i] ^= (mask & 0xff);
|
||||
mask = ((mask >> 8) & 0xffffff) | ((mask & 0xff) << 24);
|
||||
}
|
||||
int aligned_start = i;
|
||||
for (; i + 4 < (int)size; i += 4)
|
||||
{
|
||||
*(uint32_t*)(p + i) ^= mask;
|
||||
}
|
||||
for (; i < (int)size; i++)
|
||||
{
|
||||
p[i] ^= ((mask >> (8 * ((i - aligned_start) % 4))) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
static void _http_add_body_bytes(tf_http_connection_t* connection, const void* data, size_t size)
|
||||
{
|
||||
if (connection->is_websocket)
|
||||
{
|
||||
connection->body = tf_realloc(connection->body, connection->body_length + size);
|
||||
memcpy((char*)connection->body + connection->body_length, data, size);
|
||||
connection->body_length += size;
|
||||
|
||||
while (connection->body_length >= 2)
|
||||
{
|
||||
uint8_t* p = connection->body;
|
||||
uint8_t bits0 = p[0];
|
||||
uint8_t bits1 = p[1];
|
||||
if ((bits1 & (1 << 7)) == 0)
|
||||
{
|
||||
/* Unmasked message. */
|
||||
_http_connection_destroy(connection, "websocket server received unmasked bytes");
|
||||
return;
|
||||
}
|
||||
uint8_t op_code = bits0 & 0xf;
|
||||
bool fin = (bits0 & (1 << 7)) != 0;
|
||||
size_t length = bits1 & 0x7f;
|
||||
int mask_start = 2;
|
||||
if (length == 126)
|
||||
{
|
||||
length = 0;
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
length <<= 8;
|
||||
length |= p[2 + i];
|
||||
}
|
||||
mask_start = 4;
|
||||
}
|
||||
else if (length == 127)
|
||||
{
|
||||
length = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
length <<= 8;
|
||||
length |= p[2 + i];
|
||||
}
|
||||
mask_start = 10;
|
||||
}
|
||||
if (connection->body_length >= mask_start + length + 4)
|
||||
{
|
||||
uint32_t mask =
|
||||
(uint32_t)p[mask_start + 0] |
|
||||
(uint32_t)p[mask_start + 1] << 8 |
|
||||
(uint32_t)p[mask_start + 2] << 16 |
|
||||
(uint32_t)p[mask_start + 3] << 24;
|
||||
_http_websocket_mask_in_place(p + mask_start + 4, mask, length);
|
||||
if (fin)
|
||||
{
|
||||
if (connection->request->on_message)
|
||||
{
|
||||
connection->request->on_message(connection->request, op_code, p + mask_start + 4, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("MESSAGE %d [%.*s]\n", op_code, (int)length, p + mask_start + 4);
|
||||
}
|
||||
}
|
||||
connection->websocket_message_index++;
|
||||
size_t total_length = mask_start + 4 + length;
|
||||
memmove(connection->body, (char*)connection->body + total_length, connection->body_length - total_length);
|
||||
connection->body_length -= total_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t fit = tf_min(connection->content_length - connection->body_length, size);
|
||||
if (fit > 0)
|
||||
{
|
||||
memcpy((char*)connection->body + connection->body_length, data, fit);
|
||||
connection->body_length += fit;
|
||||
}
|
||||
|
||||
if (connection->body_length == connection->content_length)
|
||||
{
|
||||
if (connection->flags & k_tf_http_handler_flag_websocket)
|
||||
{
|
||||
connection->is_websocket = true;
|
||||
}
|
||||
tf_http_request_t* request = tf_malloc(sizeof(tf_http_request_t));
|
||||
*request = (tf_http_request_t)
|
||||
{
|
||||
.connection = connection,
|
||||
.phase = k_http_callback_phase_headers_received,
|
||||
.method = connection->method,
|
||||
.path = connection->path,
|
||||
.flags = connection->flags,
|
||||
.query = connection->query,
|
||||
.body = connection->body,
|
||||
.content_length = connection->content_length,
|
||||
.headers = connection->headers,
|
||||
.headers_count = connection->headers_length,
|
||||
.user_data = connection->user_data,
|
||||
};
|
||||
connection->request = request;
|
||||
|
||||
tf_http_request_ref(request);
|
||||
connection->callback(request);
|
||||
tf_http_request_release(request);
|
||||
if (!connection->is_websocket)
|
||||
{
|
||||
_http_reset_connection(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _http_on_read(uv_stream_t* stream, ssize_t read_size, const uv_buf_t* buffer)
|
||||
{
|
||||
tf_http_connection_t* connection = stream->data;
|
||||
if (read_size > 0)
|
||||
{
|
||||
if (!connection->headers_done)
|
||||
{
|
||||
connection->headers_buffer_length += read_size;
|
||||
|
||||
const char* method = NULL;
|
||||
size_t method_length = 0;
|
||||
const char* path = NULL;
|
||||
size_t path_length = 0;
|
||||
size_t header_count = sizeof(connection->headers) / sizeof(*connection->headers);
|
||||
|
||||
int parse_result = phr_parse_request(connection->headers_buffer, connection->headers_buffer_length, &method, &method_length, &path, &path_length, &connection->minor_version, connection->headers, &header_count, connection->parsed_length);
|
||||
connection->parsed_length = connection->headers_buffer_length;
|
||||
if (parse_result > 0)
|
||||
{
|
||||
connection->headers_done = true;
|
||||
connection->headers_length = header_count;
|
||||
connection->method = method;
|
||||
((char*)connection->method)[method_length] = '\0';
|
||||
connection->path = path;
|
||||
((char*)connection->path)[path_length] = '\0';
|
||||
char* q = strchr(connection->path, '?');
|
||||
if (q)
|
||||
{
|
||||
*q = '\0';
|
||||
connection->query = q + 1;
|
||||
}
|
||||
|
||||
connection->connection_close = connection->minor_version == 0;
|
||||
|
||||
for (int i = 0; i < (int)header_count; i++)
|
||||
{
|
||||
for (size_t j = 0; j < connection->headers[i].name_len; j++)
|
||||
{
|
||||
if (connection->headers[i].name[j] >= 'A' &&
|
||||
connection->headers[i].name[j] <= 'Z')
|
||||
{
|
||||
((char*)connection->headers[i].name)[j] += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
((char*)connection->headers[i].name)[connection->headers[i].name_len] = '\0';
|
||||
((char*)connection->headers[i].value)[connection->headers[i].value_len] = '\0';
|
||||
if (strcasecmp(connection->headers[i].name, "content-length") == 0)
|
||||
{
|
||||
connection->content_length = strtoull(connection->headers[i].value, NULL, 10);
|
||||
}
|
||||
else if (strcasecmp(connection->headers[i].name, "connection") == 0)
|
||||
{
|
||||
if (strcasecmp(connection->headers[i].value, "close") == 0)
|
||||
{
|
||||
connection->connection_close = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connection->content_length)
|
||||
{
|
||||
connection->body = tf_malloc(connection->content_length);
|
||||
}
|
||||
|
||||
int flags = _http_connection_get_header(connection, "upgrade") ? k_tf_http_handler_flag_websocket : 0;
|
||||
connection->flags = flags;
|
||||
if (!_http_find_handler(connection->http, connection->path, flags, &connection->callback, &connection->user_data) || !connection->callback)
|
||||
{
|
||||
connection->callback = _http_builtin_404_handler;
|
||||
}
|
||||
|
||||
_http_add_body_bytes(connection, connection->headers_buffer + parse_result, connection->headers_buffer_length - parse_result);
|
||||
}
|
||||
else if (parse_result == -2)
|
||||
{
|
||||
/* Incomplete. Will try again next time. */
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("phr_parse_request: %d\n", parse_result);
|
||||
_http_connection_destroy(connection, "failed to parse request headers");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connection->buffer_length += read_size;
|
||||
_http_add_body_bytes(connection, connection->buffer, connection->buffer_length);
|
||||
connection->buffer_length = 0;
|
||||
}
|
||||
}
|
||||
else if (read_size < 0)
|
||||
{
|
||||
_http_connection_destroy(connection, uv_strerror(read_size));
|
||||
}
|
||||
}
|
||||
|
||||
static void _http_on_connection(uv_stream_t* stream, int status)
|
||||
{
|
||||
tf_http_t* http = stream->data;
|
||||
tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t));
|
||||
*connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection } };
|
||||
int r = uv_tcp_init(connection->http->loop, &connection->tcp);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_tcp_init: %s\n", uv_strerror(r));
|
||||
tf_free(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
r = uv_accept(stream, (uv_stream_t*)&connection->tcp);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_accept: %s\n", uv_strerror(r));
|
||||
uv_close((uv_handle_t*)&connection->tcp, NULL);
|
||||
tf_free(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
r = uv_read_start((uv_stream_t*)&connection->tcp, _http_allocate_buffer, _http_on_read);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_read-start: %s\n", uv_strerror(r));
|
||||
uv_close((uv_handle_t*)&connection->tcp, NULL);
|
||||
tf_free(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
http->connections = tf_realloc(http->connections, sizeof(tf_http_connection_t*) * (http->connections_count + 1));
|
||||
http->connections[http->connections_count++] = connection;
|
||||
}
|
||||
|
||||
void tf_http_listen(tf_http_t* http, int port)
|
||||
{
|
||||
uv_tcp_t* tcp = tf_malloc(sizeof(uv_tcp_t));
|
||||
*tcp = (uv_tcp_t) { .data = http };
|
||||
int r = uv_tcp_init(http->loop, tcp);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_tcp_init: %s\n", uv_strerror(r));
|
||||
}
|
||||
|
||||
if (r == 0)
|
||||
{
|
||||
struct sockaddr_in addr =
|
||||
{
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr = { .s_addr = INADDR_ANY },
|
||||
.sin_port = ntohs(port),
|
||||
};
|
||||
r = uv_tcp_bind(tcp, (struct sockaddr*)&addr, 0);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_tcp_bind: %s\n", uv_strerror(r));
|
||||
}
|
||||
}
|
||||
|
||||
if (r == 0)
|
||||
{
|
||||
r = uv_listen((uv_stream_t*)tcp, 16, _http_on_connection);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_listen: %s\n", uv_strerror(r));
|
||||
}
|
||||
}
|
||||
|
||||
if (r == 0)
|
||||
{
|
||||
http->listeners = tf_realloc(http->listeners, sizeof(uv_tcp_t*) * (http->listeners_count + 1));
|
||||
http->listeners[http->listeners_count++] = tcp;
|
||||
}
|
||||
}
|
||||
|
||||
void tf_http_add_handler(tf_http_t* http, const char* pattern, int flags, tf_http_callback_t* callback, void* user_data)
|
||||
{
|
||||
http->handlers = tf_realloc(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1));
|
||||
http->handlers[http->handlers_count++] = (tf_http_handler_t)
|
||||
{
|
||||
.pattern = tf_strdup(pattern),
|
||||
.flags = flags,
|
||||
.callback = callback,
|
||||
.user_data = user_data,
|
||||
};
|
||||
}
|
||||
|
||||
static void _http_free_on_close(uv_handle_t* handle)
|
||||
{
|
||||
handle->data = NULL;
|
||||
tf_free(handle);
|
||||
}
|
||||
|
||||
void tf_http_destroy(tf_http_t* http)
|
||||
{
|
||||
for (int i = 0; i < http->listeners_count; i++)
|
||||
{
|
||||
uv_close((uv_handle_t*)http->listeners[i], _http_free_on_close);
|
||||
}
|
||||
tf_free(http->listeners);
|
||||
http->listeners = NULL;
|
||||
http->listeners_count = 0;
|
||||
|
||||
for (int i = 0; i < http->handlers_count; i++)
|
||||
{
|
||||
if (http->handlers[i].pattern)
|
||||
{
|
||||
tf_free((void*)http->handlers[i].pattern);
|
||||
http->handlers[i].pattern = NULL;
|
||||
}
|
||||
}
|
||||
tf_free(http->handlers);
|
||||
http->handlers_count = 0;
|
||||
|
||||
tf_free(http->connections);
|
||||
http->connections_count = 0;
|
||||
|
||||
tf_free(http);
|
||||
}
|
||||
|
||||
static const char* _http_status_text(int status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case 101: return "Switching Protocols";
|
||||
case 200: return "OK";
|
||||
case 303: return "See other";
|
||||
case 304: return "Not Modified";
|
||||
case 400: return "Bad Request";
|
||||
case 401: return "Unauthorized";
|
||||
case 403: return "Forbidden";
|
||||
case 404: return "File not found";
|
||||
case 500: return "Internal server error";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void _http_on_shutdown(uv_shutdown_t* request, int status)
|
||||
{
|
||||
request->data = NULL;
|
||||
}
|
||||
|
||||
static void _http_write(tf_http_connection_t* connection, const void* data, size_t size)
|
||||
{
|
||||
uv_write_t* write = tf_malloc(sizeof(uv_write_t) + size);
|
||||
*write = (uv_write_t) { .data = connection };
|
||||
memcpy(write + 1, data, size);
|
||||
int r = uv_write(write, (uv_stream_t*)&connection->tcp, &(uv_buf_t) { .base = (void*)(write + 1), .len = size }, 1, _http_on_write);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_write: %s\n", uv_strerror(r));
|
||||
}
|
||||
}
|
||||
|
||||
void tf_http_request_send(tf_http_request_t* request, const void* data, size_t size)
|
||||
{
|
||||
_http_write(request->connection, data, size);
|
||||
}
|
||||
|
||||
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length)
|
||||
{
|
||||
const char* status_text = _http_status_text(status);
|
||||
/* HTTP/1.x 200 OK\r\n */
|
||||
bool sent_content_length = false;
|
||||
int headers_length = 8 + 1 + 3 + 1 + strlen(status_text) + 2;
|
||||
if (headers)
|
||||
{
|
||||
for (int i = 0; i < headers_count * 2; i += 2)
|
||||
{
|
||||
/* Key: Value\r\n */
|
||||
headers_length += strlen(headers[i]) + 2 + strlen(headers[i + 1]) + 2;
|
||||
if (strcasecmp(headers[i], "content-length") == 0)
|
||||
{
|
||||
sent_content_length = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* \r\n */
|
||||
headers_length += 2;
|
||||
|
||||
char content_length_buffer[32] = { 0 };
|
||||
int content_length_buffer_length = 0;
|
||||
if (!sent_content_length && status != 101)
|
||||
{
|
||||
content_length_buffer_length = snprintf(content_length_buffer, sizeof(content_length_buffer), "Content-Length: %zd\r\n", content_length);
|
||||
headers_length += content_length_buffer_length;
|
||||
}
|
||||
|
||||
uv_write_t* write = tf_malloc(sizeof(uv_write_t) + headers_length + content_length + 1);
|
||||
*write = (uv_write_t) { .data = request->connection };
|
||||
char* buffer = (char*)(write + 1);
|
||||
int offset = snprintf(buffer, headers_length + 1, "HTTP/1.%d %03d %s\r\n", request->connection->minor_version, status, status_text);
|
||||
if (headers)
|
||||
{
|
||||
for (int i = 0; i < headers_count * 2; i += 2)
|
||||
{
|
||||
offset += snprintf(buffer + offset, headers_length + 1 - offset, "%s: %s\r\n", headers[i], headers[i + 1]);
|
||||
}
|
||||
}
|
||||
if (!sent_content_length)
|
||||
{
|
||||
memcpy(buffer + offset, content_length_buffer, content_length_buffer_length);
|
||||
offset += content_length_buffer_length;
|
||||
}
|
||||
offset += snprintf(buffer + offset, headers_length + 1 - offset, "\r\n");
|
||||
assert(offset == headers_length);
|
||||
if (content_length)
|
||||
{
|
||||
memcpy(buffer + offset, body, content_length);
|
||||
}
|
||||
int r = uv_write(write, (uv_stream_t*)&request->connection->tcp, &(uv_buf_t) { .base = buffer, .len = headers_length + content_length }, 1, _http_on_write);
|
||||
if (r)
|
||||
{
|
||||
tf_printf("uv_write: %s\n", uv_strerror(r));
|
||||
}
|
||||
|
||||
if (request->connection->connection_close &&
|
||||
!request->connection->shutdown.data)
|
||||
{
|
||||
request->connection->shutdown.data = request->connection;
|
||||
uv_shutdown(&request->connection->shutdown, (uv_stream_t*)&request->connection->tcp, _http_on_shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
size_t tf_http_get_body(const tf_http_request_t* request, const void** out_data)
|
||||
{
|
||||
*out_data = request->connection->body;
|
||||
return request->connection->content_length;
|
||||
}
|
||||
|
||||
void tf_http_request_ref(tf_http_request_t* request)
|
||||
{
|
||||
request->ref_count++;
|
||||
request->connection->ref_count++;
|
||||
}
|
||||
|
||||
void tf_http_request_release(tf_http_request_t* request)
|
||||
{
|
||||
if (--request->connection->ref_count == 0)
|
||||
{
|
||||
/* Reset the connection? */
|
||||
}
|
||||
if (--request->ref_count == 0)
|
||||
{
|
||||
tf_free(request);
|
||||
}
|
||||
}
|
||||
|
||||
static const char* _http_connection_get_header(const tf_http_connection_t* connection, const char* name)
|
||||
{
|
||||
for (int i = 0; i < connection->headers_length; i++)
|
||||
{
|
||||
if (strcasecmp(connection->headers[i].name, name) == 0)
|
||||
{
|
||||
return connection->headers[i].value;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name)
|
||||
{
|
||||
return _http_connection_get_header(request->connection, name);
|
||||
}
|
54
src/http.h
Normal file
54
src/http.h
Normal file
@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct uv_loop_s uv_loop_t;
|
||||
typedef struct _tf_http_t tf_http_t;
|
||||
typedef struct _tf_http_connection_t tf_http_connection_t;
|
||||
typedef struct _tf_http_request_t tf_http_request_t;
|
||||
|
||||
typedef enum _tf_http_callback_phase_t
|
||||
{
|
||||
k_http_callback_phase_headers_received,
|
||||
k_http_callback_phase_request_done,
|
||||
} tf_http_callback_phase_t;
|
||||
|
||||
typedef void (tf_http_message_callback)(tf_http_request_t* request, int op_code, const void* data, size_t size);
|
||||
|
||||
typedef struct _tf_http_request_t
|
||||
{
|
||||
tf_http_callback_phase_t phase;
|
||||
tf_http_connection_t* connection;
|
||||
const char* method;
|
||||
const char* path;
|
||||
int flags;
|
||||
const char* query;
|
||||
void* body;
|
||||
size_t content_length;
|
||||
struct phr_header* headers;
|
||||
int headers_count;
|
||||
tf_http_message_callback* on_message;
|
||||
void* context;
|
||||
void* user_data;
|
||||
int ref_count;
|
||||
} tf_http_request_t;
|
||||
|
||||
typedef enum _tf_http_handler_flags_t
|
||||
{
|
||||
k_tf_http_handler_flag_none = 0,
|
||||
k_tf_http_handler_flag_websocket = 1,
|
||||
} tf_http_handler_flags_t;
|
||||
|
||||
typedef void (tf_http_callback_t)(tf_http_request_t* request);
|
||||
|
||||
tf_http_t* tf_http_create(uv_loop_t* loop);
|
||||
void tf_http_listen(tf_http_t* http, int port);
|
||||
void tf_http_add_handler(tf_http_t* http, const char* pattern, int flags, tf_http_callback_t* callback, void* user_data);
|
||||
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length);
|
||||
size_t tf_http_get_body(const tf_http_request_t* request, const void** out_data);
|
||||
void tf_http_destroy(tf_http_t* http);
|
||||
|
||||
void tf_http_request_ref(tf_http_request_t* request);
|
||||
void tf_http_request_release(tf_http_request_t* request);
|
||||
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name);
|
||||
void tf_http_request_send(tf_http_request_t* request, const void* data, size_t size);
|
327
src/httpd.js.c
Normal file
327
src/httpd.js.c
Normal file
@ -0,0 +1,327 @@
|
||||
#include "httpd.js.h"
|
||||
|
||||
#include "http.h"
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "task.h"
|
||||
#include "util.js.h"
|
||||
|
||||
#include "picohttpparser.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
static JSClassID _httpd_class_id;
|
||||
static JSClassID _httpd_request_class_id;
|
||||
|
||||
typedef struct _http_handler_data_t
|
||||
{
|
||||
JSContext* context;
|
||||
JSValue callback;
|
||||
} http_handler_data_t;
|
||||
|
||||
static JSValue _httpd_response_write_head(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JS_SetPropertyStr(context, this_val, "response_status", JS_DupValue(context, argv[0]));
|
||||
JS_SetPropertyStr(context, this_val, "response_headers", JS_DupValue(context, argv[1]));
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue _httpd_response_end(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_http_request_t* request = JS_GetOpaque(this_val, _httpd_request_class_id);
|
||||
size_t length = 0;
|
||||
const void* data = NULL;
|
||||
JSValue buffer;
|
||||
if (JS_IsString(argv[0]))
|
||||
{
|
||||
data = JS_ToCStringLen(context, &length, argv[0]);
|
||||
}
|
||||
else if ((data = tf_util_try_get_array_buffer(context, &length, argv[0])) != 0)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t offset;
|
||||
size_t size;
|
||||
size_t element_size;
|
||||
buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &size, &element_size);
|
||||
if (!JS_IsException(buffer))
|
||||
{
|
||||
data = tf_util_try_get_array_buffer(context, &length, buffer);
|
||||
}
|
||||
}
|
||||
JSValue response_status = JS_GetPropertyStr(context, this_val, "response_status");
|
||||
int status = 0;
|
||||
JS_ToInt32(context, &status, response_status);
|
||||
JS_FreeValue(context, response_status);
|
||||
|
||||
const char** headers = NULL;
|
||||
int headers_length = 0;
|
||||
JSValue response_headers = JS_GetPropertyStr(context, this_val, "response_headers");
|
||||
JSPropertyEnum* ptab;
|
||||
uint32_t plen;
|
||||
JS_GetOwnPropertyNames(context, &ptab, &plen, response_headers, JS_GPN_STRING_MASK);
|
||||
headers = alloca(sizeof(const char*) * plen * 2);
|
||||
headers_length = plen;
|
||||
for (uint32_t i = 0; i < plen; ++i)
|
||||
{
|
||||
JSValue key = JS_AtomToString(context, ptab[i].atom);
|
||||
JSPropertyDescriptor desc;
|
||||
JSValue key_value = JS_NULL;
|
||||
if (JS_GetOwnProperty(context, &desc, response_headers, ptab[i].atom) == 1)
|
||||
{
|
||||
key_value = desc.value;
|
||||
JS_FreeValue(context, desc.setter);
|
||||
JS_FreeValue(context, desc.getter);
|
||||
}
|
||||
headers[i * 2 + 0] = JS_ToCString(context, key);
|
||||
headers[i * 2 + 1] = JS_ToCString(context, key_value);
|
||||
}
|
||||
|
||||
tf_http_respond(request, status, headers, headers_length, data, length);
|
||||
|
||||
for (int i = 0; i < headers_length * 2; i++)
|
||||
{
|
||||
JS_FreeCString(context, headers[i]);
|
||||
}
|
||||
for (uint32_t i = 0; i < plen; ++i)
|
||||
{
|
||||
JS_FreeAtom(context, ptab[i].atom);
|
||||
}
|
||||
js_free(context, ptab);
|
||||
JS_FreeValue(context, buffer);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue _httpd_response_send(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_http_request_t* request = JS_GetOpaque(this_val, _httpd_request_class_id);
|
||||
int opcode = 0x1;
|
||||
JS_ToInt32(context, &opcode, argv[1]);
|
||||
uint64_t length = 0;
|
||||
size_t length_size = 0;
|
||||
const char* message = JS_ToCStringLen(context, &length_size, argv[0]);
|
||||
length = length_size;
|
||||
uint8_t* copy = tf_malloc(length + 16);
|
||||
bool fin = true;
|
||||
size_t header = 1;
|
||||
copy[0] = (fin ? (1 << 7) : 0) | (opcode & 0xf);
|
||||
if (length < 126)
|
||||
{
|
||||
copy[1] = length;
|
||||
header += 1;
|
||||
}
|
||||
else if (length < (1 << 16))
|
||||
{
|
||||
copy[1] = 126;
|
||||
copy[2] = (length >> 8) & 0xff;
|
||||
copy[3] = (length >> 0) & 0xff;
|
||||
header += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t high = (length >> 32) & 0xffffffff;
|
||||
uint32_t low = (length >> 0) & 0xffffffff;
|
||||
copy[1] = 127;
|
||||
copy[2] = (high >> 24) & 0xff;
|
||||
copy[3] = (high >> 16) & 0xff;
|
||||
copy[4] = (high >> 8) & 0xff;
|
||||
copy[5] = (high >> 0) & 0xff;
|
||||
copy[6] = (low >> 24) & 0xff;
|
||||
copy[7] = (low >> 16) & 0xff;
|
||||
copy[8] = (low >> 8) & 0xff;
|
||||
copy[9] = (low >> 0) & 0xff;
|
||||
header += 9;
|
||||
}
|
||||
memcpy(copy + header, message, length);
|
||||
tf_http_request_send(request, copy, header + length);
|
||||
tf_free(copy);
|
||||
JS_FreeCString(context, message);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static void _httpd_message_callback(tf_http_request_t* request, int op_code, const void* data, size_t size)
|
||||
{
|
||||
JSContext* context = request->context;
|
||||
JSValue response_object = JS_MKPTR(JS_TAG_OBJECT, request->user_data);
|
||||
JSValue on_message = JS_GetPropertyStr(context, response_object, "onMessage");
|
||||
|
||||
JSValue event = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, event, "opCode", JS_NewInt32(context, op_code));
|
||||
JS_SetPropertyStr(context, event, "data", JS_NewStringLen(context, data, size));
|
||||
JSValue response = JS_Call(context, on_message, JS_UNDEFINED, 1, &event);
|
||||
tf_util_report_error(context, response);
|
||||
JS_FreeValue(context, event);
|
||||
JS_FreeValue(context, on_message);
|
||||
}
|
||||
|
||||
static void _httpd_callback(tf_http_request_t* request)
|
||||
{
|
||||
if (request->flags & k_tf_http_handler_flag_websocket)
|
||||
{
|
||||
const char* header_connection = tf_http_request_get_header(request, "connection");
|
||||
const char* header_upgrade = tf_http_request_get_header(request, "upgrade");
|
||||
const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
|
||||
if (header_connection &&
|
||||
header_upgrade &&
|
||||
header_sec_websocket_key &&
|
||||
strstr(header_connection, "Upgrade") &&
|
||||
strcasecmp(header_upgrade, "websocket") == 0)
|
||||
{
|
||||
static const char* k_magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
size_t key_length = strlen(header_sec_websocket_key);
|
||||
size_t size = key_length + 36;
|
||||
uint8_t* key_magic = alloca(size);
|
||||
memcpy(key_magic, header_sec_websocket_key, key_length);
|
||||
memcpy(key_magic + key_length, k_magic, 36);
|
||||
uint8_t digest[20];
|
||||
SHA1(key_magic, size, digest);
|
||||
char key[41] = { 0 };
|
||||
tf_base64_encode(digest, sizeof(digest), key, sizeof(key));
|
||||
enum { k_headers_count = 4 };
|
||||
const char* headers[k_headers_count * 2] =
|
||||
{
|
||||
"Upgrade", "websocket",
|
||||
"Connection", "Upgrade",
|
||||
"Sec-WebSocket-Accept", key,
|
||||
"Sec-WebSocket-Version", "13",
|
||||
};
|
||||
bool send_version =
|
||||
!tf_http_request_get_header(request, "sec-websocket-version") ||
|
||||
strcmp(tf_http_request_get_header(request, "sec-websocket-version"), "13") != 0;
|
||||
tf_http_respond(request, 101, headers, send_version ? k_headers_count : (k_headers_count - 1), NULL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
http_handler_data_t* data = request->user_data;
|
||||
JSContext* context = data->context;
|
||||
JSValue request_object = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, request_object, "method", JS_NewString(context, request->method));
|
||||
JS_SetPropertyStr(context, request_object, "uri", JS_NewString(context, request->path));
|
||||
JSValue headers = JS_NewObject(context);
|
||||
for (int i = 0; i < request->headers_count; i++)
|
||||
{
|
||||
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
|
||||
}
|
||||
JS_SetPropertyStr(context, request_object, "headers", headers);
|
||||
if (request->query)
|
||||
{
|
||||
JS_SetPropertyStr(context, request_object, "query", JS_NewString(context, request->query));
|
||||
}
|
||||
if (request->body)
|
||||
{
|
||||
JS_SetPropertyStr(context, request_object, "body", tf_util_new_uint8_array(context, request->body, request->content_length));
|
||||
}
|
||||
|
||||
JSValue client = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, client, "tls", JS_FALSE);
|
||||
JS_SetPropertyStr(context, request_object, "client", client);
|
||||
|
||||
JSValue response_object = JS_NewObjectClass(context, _httpd_request_class_id);
|
||||
tf_http_request_ref(request);
|
||||
JS_SetOpaque(response_object, request);
|
||||
JS_SetPropertyStr(context, response_object, "writeHead", JS_NewCFunction(context, _httpd_response_write_head, "writeHead", 2));
|
||||
JS_SetPropertyStr(context, response_object, "end", JS_NewCFunction(context, _httpd_response_end, "end", 1));
|
||||
JS_SetPropertyStr(context, response_object, "send", JS_NewCFunction(context, _httpd_response_send, "send", 2));
|
||||
JSValue args[] =
|
||||
{
|
||||
request_object,
|
||||
response_object,
|
||||
};
|
||||
JSValue response = JS_Call(context, data->callback, JS_UNDEFINED, 2, args);
|
||||
tf_util_report_error(context, response);
|
||||
JS_FreeValue(context, request_object);
|
||||
//JS_FreeValue(context, response_object);
|
||||
JS_FreeValue(context, response);
|
||||
request->on_message = _httpd_message_callback;
|
||||
request->context = context;
|
||||
request->user_data = JS_VALUE_GET_PTR(response_object);
|
||||
}
|
||||
|
||||
static JSValue _httpd_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
|
||||
const char* pattern = JS_ToCString(context, argv[0]);
|
||||
http_handler_data_t* data = tf_malloc(sizeof(http_handler_data_t));
|
||||
*data = (http_handler_data_t) { .context = context, .callback = JS_DupValue(context, argv[1]) };
|
||||
tf_http_add_handler(http, pattern, k_tf_http_handler_flag_none, _httpd_callback, data);
|
||||
JS_FreeCString(context, pattern);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue _httpd_register_socket_handler(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
|
||||
const char* pattern = JS_ToCString(context, argv[0]);
|
||||
http_handler_data_t* data = tf_malloc(sizeof(http_handler_data_t));
|
||||
*data = (http_handler_data_t) { .context = context, .callback = JS_DupValue(context, argv[1]) };
|
||||
tf_http_add_handler(http, pattern, k_tf_http_handler_flag_websocket, _httpd_callback, data);
|
||||
JS_FreeCString(context, pattern);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue _httpd_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
|
||||
int port = 0;
|
||||
JS_ToInt32(context, &port, argv[0]);
|
||||
tf_http_listen(http, port);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
void _httpd_finalizer(JSRuntime* runtime, JSValue value)
|
||||
{
|
||||
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id);
|
||||
tf_http_destroy(http);
|
||||
}
|
||||
|
||||
void _httpd_request_finalizer(JSRuntime* runtime, JSValue value)
|
||||
{
|
||||
tf_http_request_t* request = JS_GetOpaque(value, _httpd_request_class_id);
|
||||
tf_http_request_release(request);
|
||||
}
|
||||
|
||||
void tf_httpd_register(JSContext* context)
|
||||
{
|
||||
JS_NewClassID(&_httpd_class_id);
|
||||
JS_NewClassID(&_httpd_request_class_id);
|
||||
JSClassDef httpd_def =
|
||||
{
|
||||
.class_name = "Httpd",
|
||||
.finalizer = &_httpd_finalizer,
|
||||
};
|
||||
if (JS_NewClass(JS_GetRuntime(context), _httpd_class_id, &httpd_def) != 0)
|
||||
{
|
||||
fprintf(stderr, "Failed to register Httpd.\n");
|
||||
}
|
||||
JSClassDef request_def =
|
||||
{
|
||||
.class_name = "Request",
|
||||
.finalizer = &_httpd_request_finalizer,
|
||||
};
|
||||
if (JS_NewClass(JS_GetRuntime(context), _httpd_request_class_id, &request_def) != 0)
|
||||
{
|
||||
fprintf(stderr, "Failed to register Request.\n");
|
||||
}
|
||||
JSValue global = JS_GetGlobalObject(context);
|
||||
JSValue httpd = JS_NewObjectClass(context, _httpd_class_id);
|
||||
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
uv_loop_t* loop = tf_task_get_loop(task);
|
||||
tf_http_t* http = tf_http_create(loop);
|
||||
JS_SetOpaque(httpd, http);
|
||||
|
||||
JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context));
|
||||
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_all, "all", 2));
|
||||
JS_SetPropertyStr(context, httpd, "registerSocketHandler", JS_NewCFunction(context, _httpd_register_socket_handler, "register_socket_handler", 2));
|
||||
JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_start, "start", 1));
|
||||
JS_SetPropertyStr(context, global, "httpdc", httpd);
|
||||
JS_FreeValue(context, global);
|
||||
}
|
5
src/httpd.js.h
Normal file
5
src/httpd.js.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "quickjs.h"
|
||||
|
||||
void tf_httpd_register(JSContext* context);
|
@ -688,15 +688,6 @@ void tf_run_thread_start(const char* zip_path)
|
||||
.args = args,
|
||||
};
|
||||
uv_thread_create(thread, _tf_run_task_thread, data);
|
||||
#if 0
|
||||
uv_thread_join(&threads[i]);
|
||||
if (data[i].result != 0)
|
||||
{
|
||||
result = data[i].result;
|
||||
}
|
||||
tf_free(data);
|
||||
tf_free(threads);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
int main(int argc, char* argv[])
|
||||
|
@ -130,7 +130,8 @@ static void _socket_gc_mark(JSRuntime* runtime, JSValueConst value, JS_MarkFunc
|
||||
JSValue tf_socket_register(JSContext* context)
|
||||
{
|
||||
JS_NewClassID(&_classId);
|
||||
JSClassDef def = {
|
||||
JSClassDef def =
|
||||
{
|
||||
.class_name = "Socket",
|
||||
.finalizer = &_socket_finalizer,
|
||||
.gc_mark = _socket_gc_mark,
|
||||
@ -269,7 +270,7 @@ void _socket_close_internal(socket_t* socket)
|
||||
}
|
||||
}
|
||||
|
||||
void _socket_finalizer(JSRuntime *runtime, JSValue value)
|
||||
void _socket_finalizer(JSRuntime* runtime, JSValue value)
|
||||
{
|
||||
socket_t* socket = JS_GetOpaque(value, _classId);
|
||||
socket->_object = JS_UNDEFINED;
|
||||
|
@ -1881,10 +1881,6 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
|
||||
{
|
||||
uv_close((uv_handle_t*)&connection->tcp, _tf_ssb_connection_on_close);
|
||||
}
|
||||
if (connection->connect.data && !uv_is_closing((uv_handle_t*)&connection->connect))
|
||||
{
|
||||
uv_close((uv_handle_t*)&connection->connect, _tf_ssb_connection_on_close);
|
||||
}
|
||||
|
||||
if (JS_IsUndefined(connection->object) &&
|
||||
!connection->async.data &&
|
||||
|
115
src/ssb.db.c
115
src/ssb.db.c
@ -1301,6 +1301,7 @@ typedef struct _following_t
|
||||
int depth;
|
||||
int ref_count;
|
||||
int block_ref_count;
|
||||
bool populated;
|
||||
} following_t;
|
||||
|
||||
static int _following_compare(const void* a, const void* b)
|
||||
@ -1310,6 +1311,11 @@ static int _following_compare(const void* a, const void* b)
|
||||
return strcmp(ida, (*followingb)->id);
|
||||
}
|
||||
|
||||
static bool _has_following_entry(const char* id, following_t** list, int count)
|
||||
{
|
||||
return count ? bsearch(id, list, count, sizeof(following_t*), _following_compare) != 0 : false;
|
||||
}
|
||||
|
||||
static bool _add_following_entry(following_t*** list, int* count, following_t* add)
|
||||
{
|
||||
int index = tf_util_insert_index(add->id, *list, *count, sizeof(following_t*), _following_compare);
|
||||
@ -1343,19 +1349,37 @@ static bool _remove_following_entry(following_t*** list, int* count, following_t
|
||||
return false;
|
||||
}
|
||||
|
||||
static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t*** following, int* following_count, int depth, int max_depth)
|
||||
typedef struct _block_node_t block_node_t;
|
||||
|
||||
typedef struct _block_node_t
|
||||
{
|
||||
following_t* entry;
|
||||
block_node_t* parent;
|
||||
} block_node_t;
|
||||
|
||||
static bool _is_blocked_by_active_blocks(const char* id, const block_node_t* blocks)
|
||||
{
|
||||
for (const block_node_t* b = blocks; b; b = b->parent)
|
||||
{
|
||||
if (_has_following_entry(id, b->entry->blocking, b->entry->blocking_count))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks)
|
||||
{
|
||||
if (_is_blocked_by_active_blocks(id, blocks))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
int index = tf_util_insert_index(id, *following, *following_count, sizeof(following_t*), _following_compare);
|
||||
following_t* entry = NULL;
|
||||
bool already_populated = false;
|
||||
if (index < *following_count && strcmp(id, (*following)[index]->id) == 0)
|
||||
{
|
||||
entry = (*following)[index];
|
||||
already_populated = entry->depth < max_depth;
|
||||
if (depth < entry->depth)
|
||||
{
|
||||
entry->depth = depth;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1368,29 +1392,34 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t***
|
||||
(*following)[index] = entry;
|
||||
(*following_count)++;
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->depth = INT_MAX;
|
||||
snprintf(entry->id, sizeof(entry->id), "%s", id);
|
||||
entry->depth = depth;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (depth < max_depth && !already_populated)
|
||||
static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks)
|
||||
{
|
||||
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db,
|
||||
"SELECT json_extract(content, '$.contact') AS contact, json_extract(content, '$.following'), json_extract(content, '$.blocking') "
|
||||
"FROM messages "
|
||||
"WHERE contact IS NOT NULL AND author = ? AND json_extract(content, '$.type') = 'contact' "
|
||||
"ORDER BY sequence",
|
||||
-1, &statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||
sqlite3_stmt* statement = NULL;
|
||||
if (sqlite3_prepare(db, "SELECT json_extract(content, '$.contact'), json_extract(content, '$.following'), json_extract(content, '$.blocking') FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'contact' ORDER BY sequence", -1, &statement, NULL) == SQLITE_OK)
|
||||
if (sqlite3_bind_text(statement, 1, entry->id, -1, NULL) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
|
||||
while (sqlite3_step(statement) == SQLITE_ROW)
|
||||
{
|
||||
while (sqlite3_step(statement) == SQLITE_ROW)
|
||||
const char* contact = (const char*)sqlite3_column_text(statement, 0);
|
||||
if (sqlite3_column_type(statement, 1) != SQLITE_NULL)
|
||||
{
|
||||
const char* contact = (const char*)sqlite3_column_text(statement, 0);
|
||||
if (!contact)
|
||||
bool is_following = sqlite3_column_int(statement, 1) != 0;
|
||||
following_t* next = _make_following_node(contact, following, following_count, active_blocks);
|
||||
if (next)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (sqlite3_column_type(statement, 1) != SQLITE_NULL)
|
||||
{
|
||||
bool is_following = sqlite3_column_int(statement, 1) != 0;
|
||||
following_t* next = _get_following(ssb, contact, following, following_count, depth + 1, max_depth);
|
||||
if (is_following)
|
||||
{
|
||||
if (_add_following_entry(&entry->following, &entry->following_count, next))
|
||||
@ -1406,10 +1435,13 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t***
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sqlite3_column_type(statement, 2) != SQLITE_NULL)
|
||||
}
|
||||
if (sqlite3_column_type(statement, 2) != SQLITE_NULL)
|
||||
{
|
||||
bool is_blocking = sqlite3_column_int(statement, 2) != 0;
|
||||
following_t* next = _make_following_node(contact, following, following_count, active_blocks);
|
||||
if (next)
|
||||
{
|
||||
bool is_blocking = sqlite3_column_int(statement, 2) != 0;
|
||||
following_t* next = _get_following(ssb, contact, following, following_count, depth + 1, 0 /* don't dig deeper into blocked users */);
|
||||
if (is_blocking)
|
||||
{
|
||||
if (_add_following_entry(&entry->blocking, &entry->blocking_count, next))
|
||||
@ -1427,11 +1459,32 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t***
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
tf_ssb_release_db_reader(ssb, db);
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
tf_ssb_release_db_reader(ssb, db);
|
||||
}
|
||||
|
||||
static void _get_following(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth, block_node_t* active_blocks)
|
||||
{
|
||||
entry->depth = tf_min(depth, entry->depth);
|
||||
if (depth < max_depth && !entry->populated && !_is_blocked_by_active_blocks(entry->id, active_blocks))
|
||||
{
|
||||
entry->populated = true;
|
||||
_populate_follows_and_blocks(ssb, entry, following, following_count, active_blocks);
|
||||
|
||||
if (depth < max_depth)
|
||||
{
|
||||
block_node_t blocks = { .entry = entry, .parent = active_blocks };
|
||||
for (int i = 0; i < entry->following_count; i++)
|
||||
{
|
||||
if (!_has_following_entry(entry->following[i]->id, entry->blocking, entry->blocking_count))
|
||||
{
|
||||
_get_following(ssb, entry->following[i], following, following_count, depth + 1, max_depth, &blocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth)
|
||||
@ -1440,7 +1493,8 @@ tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, in
|
||||
int following_count = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
following_t* entry = _get_following(ssb, ids[i], &following, &following_count, 0, depth);
|
||||
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL);
|
||||
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL);
|
||||
entry->ref_count++;
|
||||
}
|
||||
|
||||
@ -1487,7 +1541,8 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c
|
||||
int following_count = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
following_t* entry = _get_following(ssb, ids[i], &following, &following_count, 0, depth);
|
||||
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL);
|
||||
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL);
|
||||
entry->ref_count++;
|
||||
}
|
||||
|
||||
|
120
src/ssb.tests.c
120
src/ssb.tests.c
@ -496,6 +496,23 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
|
||||
uv_loop_close(&loop);
|
||||
}
|
||||
|
||||
static void _assert_visible(tf_ssb_t* ssb, const char* id, const char* contact, bool visible)
|
||||
{
|
||||
const char** ids = tf_ssb_db_following_deep_ids(ssb, &id, 1, 2);
|
||||
bool found = false;
|
||||
(void)found;
|
||||
for (int i = 0; ids[i]; i++)
|
||||
{
|
||||
if (strcmp(ids[i], contact) == 0)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tf_free(ids);
|
||||
assert(found == visible);
|
||||
}
|
||||
|
||||
void tf_ssb_test_following(const tf_test_options_t* options)
|
||||
{
|
||||
tf_printf("Testing following.\n");
|
||||
@ -503,91 +520,68 @@ void tf_ssb_test_following(const tf_test_options_t* options)
|
||||
uv_loop_t loop = { 0 };
|
||||
uv_loop_init(&loop);
|
||||
|
||||
tf_ssb_t* ssb0 = tf_ssb_create(&loop, NULL, ":memory:");
|
||||
unlink("out/test_db0.sqlite");
|
||||
tf_ssb_t* ssb0 = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite");
|
||||
tf_ssb_generate_keys(ssb0);
|
||||
|
||||
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, ":memory:");
|
||||
tf_ssb_generate_keys(ssb1);
|
||||
|
||||
tf_ssb_t* ssb2 = tf_ssb_create(&loop, NULL, ":memory:");
|
||||
tf_ssb_generate_keys(ssb2);
|
||||
|
||||
char id0[k_id_base64_len] = { 0 };
|
||||
char id1[k_id_base64_len] = { 0 };
|
||||
char id2[k_id_base64_len] = { 0 };
|
||||
tf_ssb_whoami(ssb0, id0, sizeof(id0));
|
||||
tf_ssb_whoami(ssb1, id1, sizeof(id1));
|
||||
tf_ssb_whoami(ssb2, id2, sizeof(id2));
|
||||
|
||||
char id0[k_id_base64_len] = { "@" };
|
||||
char id1[k_id_base64_len] = { "@" };
|
||||
char id2[k_id_base64_len] = { "@" };
|
||||
char id3[k_id_base64_len] = { "@" };
|
||||
char priv0b[512] = { 0 };
|
||||
char priv1b[512] = { 0 };
|
||||
char priv2b[512] = { 0 };
|
||||
char priv3b[512] = { 0 };
|
||||
uint8_t priv0[512] = { 0 };
|
||||
uint8_t priv1[512] = { 0 };
|
||||
uint8_t priv2[512] = { 0 };
|
||||
tf_ssb_get_private_key(ssb0, priv0, sizeof(priv0));
|
||||
tf_ssb_get_private_key(ssb1, priv1, sizeof(priv1));
|
||||
tf_ssb_get_private_key(ssb2, priv2, sizeof(priv2));
|
||||
uint8_t priv3[512] = { 0 };
|
||||
|
||||
JSContext* context = NULL;
|
||||
tf_ssb_generate_keys_buffer(id0 + 1, sizeof(id0) - 1, priv0b, sizeof(priv0b));
|
||||
tf_ssb_generate_keys_buffer(id1 + 1, sizeof(id1) - 1, priv1b, sizeof(priv1b));
|
||||
tf_ssb_generate_keys_buffer(id2 + 1, sizeof(id2) - 1, priv2b, sizeof(priv2b));
|
||||
tf_ssb_generate_keys_buffer(id3 + 1, sizeof(id3) - 1, priv3b, sizeof(priv3b));
|
||||
tf_base64_decode(priv0b, strlen(priv0b), priv0, sizeof(priv0));
|
||||
tf_base64_decode(priv1b, strlen(priv1b), priv1, sizeof(priv1));
|
||||
tf_base64_decode(priv2b, strlen(priv2b), priv2, sizeof(priv2));
|
||||
tf_base64_decode(priv3b, strlen(priv3b), priv3, sizeof(priv3));
|
||||
|
||||
JSContext* context = tf_ssb_get_context(ssb0);
|
||||
JSValue message;
|
||||
JSValue signed_message;
|
||||
bool stored;
|
||||
|
||||
#define FOLLOW(ssb, id, priv, follow) \
|
||||
context = tf_ssb_get_context(ssb); \
|
||||
#define FOLLOW_BLOCK(id, priv, contact, follow, block) \
|
||||
message = JS_NewObject(context); \
|
||||
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact")); \
|
||||
JS_SetPropertyStr(context, message, "contact", JS_NewString(context, id)); \
|
||||
JS_SetPropertyStr(context, message, "contact", JS_NewString(context, contact)); \
|
||||
JS_SetPropertyStr(context, message, "following", follow ? JS_TRUE : JS_FALSE); \
|
||||
signed_message = tf_ssb_sign_message(ssb, id, priv, message); \
|
||||
JS_SetPropertyStr(context, message, "blocking", block ? JS_TRUE : JS_FALSE); \
|
||||
signed_message = tf_ssb_sign_message(ssb0, id, priv, message); \
|
||||
stored = false; \
|
||||
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _message_stored, &stored); \
|
||||
_wait_stored(ssb, &stored); \
|
||||
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored); \
|
||||
_wait_stored(ssb0, &stored); \
|
||||
JS_FreeValue(context, signed_message); \
|
||||
JS_FreeValue(context, message); \
|
||||
context = NULL
|
||||
|
||||
#if 1
|
||||
/* TODO: This test doesn't actually really test anything anymore. */
|
||||
#define DUMP(id, depth)
|
||||
#else
|
||||
#define DUMP(id, depth) \
|
||||
do \
|
||||
{ \
|
||||
tf_printf("following %d:\n", depth); \
|
||||
const char** tf_ssb_get_following_deep(tf_ssb_t* ssb_param, const char** ids, int depth_param); \
|
||||
const char** f = tf_ssb_get_following_deep(ssb0, (const char*[]) { id, NULL }, depth); \
|
||||
for (const char** p = f; p && *p; p++) \
|
||||
{ \
|
||||
tf_printf("* %s\n", *p); \
|
||||
} \
|
||||
tf_printf("\n"); \
|
||||
tf_free(f); \
|
||||
} \
|
||||
while (0)
|
||||
#endif
|
||||
FOLLOW_BLOCK(id0, priv0, id1, true, false);
|
||||
FOLLOW_BLOCK(id1, priv1, id2, true, false);
|
||||
FOLLOW_BLOCK(id1, priv1, id3, true, false);
|
||||
_assert_visible(ssb0, id0, id0, true);
|
||||
_assert_visible(ssb0, id0, id1, true);
|
||||
_assert_visible(ssb0, id0, id2, true);
|
||||
_assert_visible(ssb0, id0, id3, true);
|
||||
FOLLOW_BLOCK(id0, priv0, id3, false, true);
|
||||
_assert_visible(ssb0, id0, id0, true);
|
||||
_assert_visible(ssb0, id0, id1, true);
|
||||
_assert_visible(ssb0, id0, id2, true);
|
||||
_assert_visible(ssb0, id0, id3, false);
|
||||
|
||||
FOLLOW(ssb0, id1, priv1, true);
|
||||
FOLLOW(ssb1, id2, priv2, true);
|
||||
FOLLOW(ssb2, id0, priv0, true);
|
||||
DUMP(id0, 2);
|
||||
DUMP(id1, 2);
|
||||
DUMP(id2, 2);
|
||||
FOLLOW(ssb0, id1, priv1, false);
|
||||
//FOLLOW(ssb0, id1, priv1, true);
|
||||
//FOLLOW(ssb0, id1, priv1, true);
|
||||
DUMP(id0, 1);
|
||||
DUMP(id1, 2);
|
||||
//FOLLOW(ssb0, id1, priv1, false);
|
||||
//DUMP(1);
|
||||
//DUMP(1);
|
||||
|
||||
#undef FOLLOW
|
||||
#undef DUMP
|
||||
#undef FOLLOW_BLOCK
|
||||
|
||||
uv_run(&loop, UV_RUN_DEFAULT);
|
||||
|
||||
tf_ssb_destroy(ssb0);
|
||||
tf_ssb_destroy(ssb1);
|
||||
tf_ssb_destroy(ssb2);
|
||||
|
||||
uv_loop_close(&loop);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include "bcrypt.js.h"
|
||||
#include "database.js.h"
|
||||
#include "file.js.h"
|
||||
#include "httpd.js.h"
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "packetstream.h"
|
||||
@ -1731,6 +1732,7 @@ void tf_task_activate(tf_task_t* task)
|
||||
JS_SetPropertyStr(context, global, "TlsContext", tf_tls_context_register(context));
|
||||
tf_file_register(context);
|
||||
tf_database_register(context);
|
||||
tf_httpd_register(context);
|
||||
|
||||
task->_ssb = tf_ssb_create(&task->_loop, task->_context, task->_db_path);
|
||||
tf_ssb_set_trace(task->_ssb, task->_trace);
|
||||
|
93
src/tests.c
93
src/tests.c
@ -1,5 +1,6 @@
|
||||
#include "tests.h"
|
||||
|
||||
#include "http.h"
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "ssb.tests.h"
|
||||
@ -11,6 +12,8 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <uv.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIFEXITED(x) 1
|
||||
#define WEXITSTATUS(x) (x)
|
||||
@ -667,6 +670,95 @@ static void _test_b64(const tf_test_options_t* options)
|
||||
unlink("out/test.js");
|
||||
}
|
||||
|
||||
typedef struct _test_http_t
|
||||
{
|
||||
uv_loop_t* loop;
|
||||
uv_async_t async;
|
||||
bool done;
|
||||
} test_http_t;
|
||||
|
||||
static void _test_http_async(uv_async_t* async)
|
||||
{
|
||||
}
|
||||
|
||||
static void _test_http_thread(void* data)
|
||||
{
|
||||
#if defined(__linux__)
|
||||
test_http_t* test = data;
|
||||
int r = system("curl -v http://localhost:23456/404");
|
||||
assert(WEXITSTATUS(r) == 0);
|
||||
tf_printf("curl returned %d\n", WEXITSTATUS(r));
|
||||
|
||||
r = system("curl -v http://localhost:23456/hello");
|
||||
assert(WEXITSTATUS(r) == 0);
|
||||
tf_printf("curl returned %d\n", WEXITSTATUS(r));
|
||||
|
||||
r = system("curl -v --data 'hello world' http://localhost:23456/post");
|
||||
assert(WEXITSTATUS(r) == 0);
|
||||
tf_printf("curl returned %d\n", WEXITSTATUS(r));
|
||||
|
||||
r = system("curl -v http://localhost:23456/hello http://localhost:23456/hello http://localhost:23456/hello");
|
||||
assert(WEXITSTATUS(r) == 0);
|
||||
tf_printf("curl returned %d\n", WEXITSTATUS(r));
|
||||
|
||||
test->done = true;
|
||||
|
||||
/* All to wake up the loop. */
|
||||
uv_async_send(&test->async);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void _test_http_handler(tf_http_request_t* request)
|
||||
{
|
||||
const char* headers[] =
|
||||
{
|
||||
"User-Agent", "TildeFriends/1.0",
|
||||
};
|
||||
const char* k_payload = "Hello, world!\n";
|
||||
tf_http_respond(request, 200, headers, 1, k_payload, strlen(k_payload));
|
||||
}
|
||||
|
||||
static void _test_http_handler_post(tf_http_request_t* request)
|
||||
{
|
||||
const void* body = NULL;
|
||||
size_t size = tf_http_get_body(request, &body);
|
||||
tf_printf("size = %zd body=%.*s\n", size, (int)size, (const char*)body);
|
||||
const char* headers[] =
|
||||
{
|
||||
"Connection", "close",
|
||||
};
|
||||
const char* k_payload = "Hello, world!\n";
|
||||
tf_http_respond(request, 200, headers, 1, k_payload, strlen(k_payload));
|
||||
}
|
||||
|
||||
static void _test_http(const tf_test_options_t* options)
|
||||
{
|
||||
tf_printf("Starting http.\n");
|
||||
uv_loop_t loop = { 0 };
|
||||
uv_loop_init(&loop);
|
||||
tf_http_t* http = tf_http_create(&loop);
|
||||
tf_http_add_handler(http, "/hello", k_tf_http_handler_flag_none, _test_http_handler, NULL);
|
||||
tf_http_add_handler(http, "/post", k_tf_http_handler_flag_none, _test_http_handler_post, NULL);
|
||||
tf_http_listen(http, 23456);
|
||||
|
||||
test_http_t test = { .loop = &loop };
|
||||
uv_async_init(&loop, &test.async, _test_http_async);
|
||||
uv_thread_t thread = { 0 };
|
||||
uv_thread_create(&thread, _test_http_thread, &test);
|
||||
while (!test.done)
|
||||
{
|
||||
uv_run(&loop, UV_RUN_ONCE);
|
||||
}
|
||||
uv_close((uv_handle_t*)&test.async, NULL);
|
||||
tf_printf("Done running.\n");
|
||||
|
||||
tf_http_destroy(http);
|
||||
uv_run(&loop, UV_RUN_DEFAULT);
|
||||
uv_loop_close(&loop);
|
||||
|
||||
uv_thread_join(&thread);
|
||||
}
|
||||
|
||||
static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options), bool opt_in)
|
||||
{
|
||||
bool specified = false;
|
||||
@ -706,6 +798,7 @@ static void _tf_test_run(const tf_test_options_t* options, const char* name, voi
|
||||
void tf_tests(const tf_test_options_t* options)
|
||||
{
|
||||
#if !TARGET_OS_IPHONE
|
||||
_tf_test_run(options, "http", _test_http, false);
|
||||
_tf_test_run(options, "ssb", tf_ssb_test_ssb, false);
|
||||
_tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false);
|
||||
_tf_test_run(options, "ssb_following", tf_ssb_test_following, false);
|
||||
|
13
src/trace.c
13
src/trace.c
@ -5,6 +5,7 @@
|
||||
#include "trace.h"
|
||||
|
||||
#include "mem.h"
|
||||
#include "util.js.h"
|
||||
|
||||
#include "sqlite3.h"
|
||||
#include "uv.h"
|
||||
@ -15,9 +16,7 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if !defined(_countof)
|
||||
#define _countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
|
||||
#endif
|
||||
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
|
||||
|
||||
enum
|
||||
{
|
||||
@ -138,7 +137,7 @@ void tf_trace_counter(tf_trace_t* trace, const char* name, int argc, const char*
|
||||
{
|
||||
p += snprintf(line + p, sizeof(line) - p, "\"%s\": %" PRId64 "%s", arg_names[i], arg_values[i], i == argc - 1 ? "}}," : ", ");
|
||||
}
|
||||
|
||||
p = tf_min(p, tf_countof(line));
|
||||
trace->callback(trace, line, p, trace->user_data);
|
||||
}
|
||||
|
||||
@ -228,7 +227,7 @@ static tf_trace_thread_t* _tf_trace_get_thread(tf_trace_t* trace, pthread_t self
|
||||
static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name, void* tag)
|
||||
{
|
||||
tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self);
|
||||
if (!thread->stack || thread->stack->count + 1 > (int)_countof(thread->stack->names))
|
||||
if (!thread->stack || thread->stack->count + 1 > tf_countof(thread->stack->names))
|
||||
{
|
||||
tf_trace_stack_t* stack = tf_malloc(sizeof(tf_trace_stack_t));
|
||||
memset(stack, 0, sizeof(*stack));
|
||||
@ -236,7 +235,7 @@ static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name,
|
||||
thread->stack = stack;
|
||||
}
|
||||
tf_trace_stack_t* stack = thread->stack;
|
||||
while (stack->count == 0 && stack->next && stack->next->count + 1 <= (int)_countof(thread->stack->names))
|
||||
while (stack->count == 0 && stack->next && stack->next->count + 1 <= tf_countof(thread->stack->names))
|
||||
{
|
||||
stack = stack->next;
|
||||
}
|
||||
@ -279,6 +278,7 @@ static void _tf_trace_begin_tagged(tf_trace_t* trace, const char* name, void* ta
|
||||
int p = snprintf(line, sizeof(line), "{\"ph\": \"B\", \"pid\": %d, \"tid\": \"0x%" PRIx64 "\", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)self, _trace_ts());
|
||||
p += _tf_trace_escape_name(line + p, sizeof(line) - p, name);
|
||||
p += snprintf(line + p, sizeof(line) - p, "\"},");
|
||||
p = tf_min(p, tf_countof(line));
|
||||
trace->callback(trace, line, p, trace->user_data);
|
||||
}
|
||||
|
||||
@ -305,6 +305,7 @@ static void _tf_trace_end_tagged(tf_trace_t* trace, void* tag)
|
||||
int p = snprintf(line, sizeof(line), "{\"ph\": \"E\", \"pid\": %d, \"tid\": \"0x%" PRIx64 "\", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)pthread_self(), _trace_ts());
|
||||
p += _tf_trace_escape_name(line + p, sizeof(line) - p, name);
|
||||
p += snprintf(line + p, sizeof(line) - p, "\"},");
|
||||
p = tf_min(p, tf_countof(line));
|
||||
trace->callback(trace, line, p, trace->user_data);
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,12 @@ bool tf_util_report_error(JSContext* context, JSValue value)
|
||||
else if (JS_IsException(value))
|
||||
{
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
tf_task_send_error_to_parent(task, value);
|
||||
if (!tf_task_send_error_to_parent(task, value))
|
||||
{
|
||||
JSValue exception = JS_GetException(context);
|
||||
tf_util_report_error(context, exception);
|
||||
JS_FreeValue(context, exception);
|
||||
}
|
||||
is_error = true;
|
||||
}
|
||||
return is_error;
|
||||
|
@ -22,4 +22,4 @@ const char* tf_util_backtrace_string();
|
||||
|
||||
const char* tf_util_function_to_string(void* function);
|
||||
|
||||
#define tf_min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; })
|
||||
#define tf_min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _b : _a; })
|
||||
|
@ -1,2 +1,2 @@
|
||||
#define VERSION_NUMBER "0.0.13"
|
||||
#define VERSION_NAME "Served on grilled naan or gluten free sweet potato flatbread."
|
||||
#define VERSION_NUMBER "0.0.14"
|
||||
#define VERSION_NAME "Served on apple cider dressed winter greens."
|
||||
|
@ -7,6 +7,7 @@ from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
for path in ('out/selenium.sqlite', 'out/selenium.sqlite-shm', 'out/selenium.sqlite-wal'):
|
||||
@ -14,7 +15,7 @@ for path in ('out/selenium.sqlite', 'out/selenium.sqlite-shm', 'out/selenium.sql
|
||||
os.unlink(path)
|
||||
except:
|
||||
pass
|
||||
tf = subprocess.Popen(['out/debug/tildefriends', 'run', '-d', 'out/selenium.sqlite', '-b', '0', '-p', '8888'])
|
||||
tf = subprocess.Popen(['out/debug/tildefriends', 'run', '-d', 'out/selenium.sqlite', '-b', '0', '-p', '8888'] + sys.argv[1:])
|
||||
|
||||
def exists_in_shadow_root(shadow_root, by, value):
|
||||
return lambda driver: shadow_root.find_element(by, value)
|
||||
|
@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash -e
|
||||
mkdir -p out/cory.app
|
||||
cp src/ios/Info.plist out/cory.app
|
||||
xcrun --sdk iphonesimulator clang src/ios.m -arch x86_64 -o out/cory.app/cory -framework CoreFoundation -framework UIKit -framework WebKit -framework Foundation
|
||||
xcrun simctl install booted out/cory.app
|
||||
xcrun simctl launch booted cory
|
Reference in New Issue
Block a user