51 Commits

Author SHA1 Message Date
f4f560b164 Let's call this 0.0.14. Cut some apps to squeeze in under 5MB.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4702 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 19:33:46 +00:00
14b7f9237b A uv_connect_t is not a handle that can be closed. Fixes a crash.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4701 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 18:58:58 +00:00
f3518b3d0f Fix some broken tooltips.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4700 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 18:12:14 +00:00
7964524e0a Fix websocket unmasking issues. Autotest works with C httpd, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4699 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 17:45:07 +00:00
8ab8335baa This is exchanging some websocket messages, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4698 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-25 23:50:55 +00:00
cd43bf9dfa Bugs galore, but this is sending and receiving some websocket messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4697 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-25 23:39:16 +00:00
ccebf831e7 A bit closer to websockets.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4696 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-25 22:53:05 +00:00
9f2f9bd8b0 Fixed some package math.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4695 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 22:09:09 +00:00
adf8c14536 Saw a websocket message go across the wire with this.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4694 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 22:06:11 +00:00
606e82d718 Saw a websocket message go across the wire with this.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4693 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 21:39:51 +00:00
1621f1753a WebSocket request/response header dance. Feels like the loop is getting close to closed, but I want to refactor everything.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4692 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 17:43:33 +00:00
196ab66e14 Treat the ?query string and body the same as httpd.js does. Now I can auth.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4691 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-23 19:52:59 +00:00
38ab32dad9 Fix some http request lifetime issues, and follow the same lowercase convention for headers.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4690 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 17:45:06 +00:00
86046e52f0 One less dynamic http allocation. Also one less crash.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4689 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 17:15:59 +00:00
9e7c860414 Compile fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4688 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 02:06:17 +00:00
7dc8b86ee2 Return legit responses for some static files.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4687 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 02:04:20 +00:00
6ecbfe3de6 Sort of barely starting to call httpd callbacks with the new implementation.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4686 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 01:27:57 +00:00
f9940fc436 Add a runtime switch between httpd implementions. One of which is totally not hooked up yet.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4685 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 00:56:16 +00:00
58e75ee276 I think we did some keep-alive.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4684 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 00:13:03 +00:00
e7771f539d Now we're uploading some data.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4683 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 00:00:15 +00:00
c2f62cd8e0 Now we're uploading some data.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4682 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-20 23:58:28 +00:00
f4b6812675 Auto-add a content-length header.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4681 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-20 23:13:03 +00:00
03e4b37c04 Make the http test complete successfully.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4680 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-18 17:51:15 +00:00
7b3a9e0f63 Send a valid HTTP response and shutdown the connection.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4679 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-17 17:44:54 +00:00
067f546580 Send a canned HTTP response.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4678 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-14 01:59:23 +00:00
2f7697b7ec These colors were bugging me.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4677 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-14 00:02:18 +00:00
1d214f89ed Work in progress HTTP server in C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4676 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-13 23:59:11 +00:00
0b47207949 Show selected and hovered items in the wiki table of contents.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4675 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-12 17:38:10 +00:00
94dd573a81 Show a tree of wikis and docs in the wiki app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4674 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-11 17:48:08 +00:00
6fa4896155 Fix OpenBSD.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4673 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 23:16:00 +00:00
28c99f9d8b We often have alloca.h.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4672 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 23:07:05 +00:00
88fbb5f73b Unused code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4671 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 01:50:33 +00:00
402c185dd4 Let's be clear about our C standard.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4670 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 00:49:53 +00:00
ae2015a604 Add the blog app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4669 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 20:25:49 +00:00
023731fc3f Make the wiki app produce the blog title.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4668 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 19:35:41 +00:00
99998aac8a Fixed some publicWebHostring UI issues in the ssb app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4667 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 19:26:33 +00:00
360d0bc110 Let the wiki app post blog messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4666 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 19:13:06 +00:00
817838e522 Call out a summary and thumbnail for wiki pages, for the purpose of using in blog posts.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4665 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 18:35:42 +00:00
deb3cfb4b6 quickjs-2023-12-09.tar.xz with Haiku+OpenBSD tweaks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4664 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 15:18:26 +00:00
af61519632 Compile fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4663 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-07 02:29:30 +00:00
b1714cf554 I think I fixed following calculations, again. Revived the test, though it's still very not thorough.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4662 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-07 02:28:49 +00:00
f0984b19f2 Encrypt wiki docs to all editors..
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4661 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-06 17:48:44 +00:00
eb3c9cd6f3 Compile fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4660 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-06 00:52:47 +00:00
e677b0ac3c Fix tf_min and some crashes in trace. Wow.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4659 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-06 00:40:34 +00:00
dd909bfe53 Give some feedback about encrypted wiki pages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4658 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-05 23:39:45 +00:00
b13b111614 Make privateness of wiki pages sticky.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4657 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-04 17:50:13 +00:00
5511530926 Fix a mention of a renamed make target.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4656 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 18:06:50 +00:00
5e1ef01bc0 Support pasting and showing images in the wiki.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4655 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 18:03:42 +00:00
a060eadab7 ios build is part of the makefile.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4654 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 17:12:52 +00:00
70db31bb8f Add an index_map which can be used to redirect different hostnames to different app paths so that I can host multiple domains of the same device.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4653 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 17:03:17 +00:00
1292775a75 Now we're 0.0.14-wip.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4652 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 16:29:49 +00:00
54 changed files with 5293 additions and 3722 deletions

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

@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&YHBylHM7DlDDiGfuNj+g95Bf7NFxTZs/IKG14TbWnhs=.sha256"
}

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

@ -0,0 +1,149 @@
import * as commonmark from './commonmark.min.js';
function escape(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
function escapeAttribute(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;');
}
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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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![${name}](${id})`;
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') {

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

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

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

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

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

@ -1 +1 @@
2021-03-27
2023-12-09

Binary file not shown.

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:

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

File diff suppressed because it is too large Load Diff

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

@ -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,

@ -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")

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

File diff suppressed because it is too large Load Diff

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

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

@ -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.

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

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

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

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

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

@ -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 &&

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

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

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

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