77 Commits

Author SHA1 Message Date
09a0cfd349 core: core.permissionsGranted() JS => C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m47s
2025-10-26 09:09:52 -04:00
5bf7346321 android: Disable strict mode dialogs. They're apparently problematic on some devices. #135
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m23s
2025-10-26 08:24:12 -04:00
dd558c57e0 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m59s
2025-10-23 12:33:52 -04:00
5647196924 ssb: Add a manual theme color picker.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m1s
2025-10-22 19:39:20 -04:00
49b1834bc6 docs: Prep release notes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m14s
2025-10-22 18:40:29 -04:00
d111647ea8 ssb: Merge the query tab into the search tab. Search for something starting with sql: to search for arbitrary SQL.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-22 18:21:22 -04:00
d7580dab9b update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m29s
2025-10-22 12:22:09 -04:00
28db8a8d5f update: speedscope 1.24.0.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-22 12:20:35 -04:00
e64d5617e7 ssb: Fix contact groups expanding/collapsing together.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m23s
2025-10-19 08:39:46 -04:00
acd114650a ssb: Slightly improved following/blocking messages. following=true blocking=false is a common situation that should be described as following.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m46s
2025-10-16 12:48:49 -04:00
7a47ffaa61 welcome: -OpenSSL.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-15 20:19:03 -04:00
42f7f66f35 cleanup: Some OpenSSL cruft.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m37s
2025-10-15 20:10:15 -04:00
b2b4ffeeae cleanup: Remove OpenSSL and consequently https support. Run behind a reverse proxy if you need https.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-15 20:02:59 -04:00
26de1f7daa update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-15 19:22:57 -04:00
07605933dc core: core.globalSettingsDescriptions JS => C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m48s
2025-10-15 19:09:41 -04:00
4bdc7ec616 core: core.globalSettingsGet JS => C. 2025-10-15 18:24:38 -04:00
8ca64550e5 test: Messages added callbacks are not a reliable way to count messages. Fix -t=invite sometimes stalling.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m38s
2025-10-09 12:50:25 -04:00
25dbac804c core: Minor cleanup and style. 2025-10-09 12:45:38 -04:00
6ab3fd168b update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m43s
2025-10-08 19:17:08 -04:00
94858e2371 core: ssb.getIdentities() + ssb.getOwnerIdentities() JS => C.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-08 19:13:54 -04:00
6d13502e94 core: ssb.getActiveIdentity JS => C. 2025-10-08 18:30:38 -04:00
77001e595c ssb: Faster queries through lots of trial and error.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m5s
2025-10-07 21:15:09 -04:00
6fad20ffa3 ssb: Recover some of the load time lost in filtering out subscribed channels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m20s
2025-10-06 12:45:40 -04:00
00fb6c9839 update: CoreMirror. 2025-10-06 06:56:08 -04:00
97fcf72d63 core: Move some simple properties JS => C. 2025-10-06 06:53:41 -04:00
5d8d02515d ssb: This query is slightly faster but not fast enough.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m48s
2025-10-02 12:47:57 -04:00
859fe1feb0 ssb: Exclude followed mentions from the general channel, too.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m18s
2025-10-01 20:02:37 -04:00
8f61d83f41 ssb: Exclude subscribed channels from general posts. 2025-10-01 19:52:02 -04:00
6423b3e479 ssb: Consolidate the connection sidebar options. Three copies on-screen is too many.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m47s
2025-10-01 19:12:31 -04:00
2bc8cec8a2 core: Move users()+permissionsForUser() from JS to C. 2025-10-01 18:36:50 -04:00
b49a6cd685 ssb: Place the message collapse better with the messages it collapses.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m8s
2025-10-01 18:06:45 -04:00
2885380f40 core: Remove some unused events.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-10-01 17:55:32 -04:00
2ec3b6a249 http: Add some logging to try to diagnose an intermittent shutdown issue. 2025-10-01 17:48:23 -04:00
3ef795452d ssb: Condense child messages a bit.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m55s
2025-10-01 12:27:04 -04:00
479d87c8b8 update: OpenSSL 3.6.0. 2025-10-01 12:10:07 -04:00
a56077dcc7 update: libbacktrace.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m24s
2025-09-30 12:56:51 -04:00
d3f4587c3b update: OpenSSL 3.5.4. 2025-09-30 12:52:22 -04:00
623705b7a1 core: Fix a crash in the apps app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m36s
2025-09-28 21:15:28 -04:00
8f87f4751d ios: Add iTunesArtwork icon.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m19s
2025-09-28 17:55:36 -04:00
2ac6dfde9d test: Make the testing replicating a blob check the correct client, and add another test that does it while already connected.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-09-28 17:35:54 -04:00
81ade7a400 core: Make the internal ssb.* API more explicitly not exposed to apps.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-09-28 17:19:58 -04:00
63f7ff9f27 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m20s
2025-09-28 16:01:02 -04:00
8a0fa17a79 build: Remove stale include. Fix clean builds.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m56s
2025-09-28 15:23:44 -04:00
0ead5ed967 cleanup: Remove server-side JS socket and HTTP request support. Not used/useful enough to justify keeping all this code around.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m6s
2025-09-28 13:43:28 -04:00
53261a6fbc docs: Remove some stale docs from the api app. 2025-09-28 13:27:37 -04:00
c60ff86a4d core: Use FreeBSD's public domain SHA1 instead of OpenSSL's so that jettisoning OpenSSL is an option.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m49s
2025-09-28 13:06:03 -04:00
83a0b017c5 cleanup: Remove the bcrypt JS API. Apps that need it should find their own implementation. Core does it all in C. 2025-09-28 12:48:25 -04:00
3746622a11 ssb: Give channel subscribe/unsubscribe similar grouping treatment to follows/blocks.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m16s
2025-09-27 16:16:34 -04:00
ccd50cf59f welcome: No longer just open testing.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m51s
2025-09-25 10:51:22 -04:00
93680eb43d build: Update nix, and start work on 0.2025.10.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m18s
2025-09-24 19:18:52 -04:00
3ae4b7086a build: Let's build 0.2025.9.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m53s
2025-09-24 15:02:22 -04:00
446b1f8600 ssb: Simplify some things to make funky layout issues with the sync now / stay connected buttons go away.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m3s
2025-09-24 13:43:33 -04:00
00fd208a2c ssb: Enough plumbing that if a blob is received, we will try to load the image again.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m24s
2025-09-24 11:34:34 -04:00
e574d03716 core: Prefer EXIT_FAILURE. 2025-09-24 10:09:40 -04:00
c1f3116c9d ssb: Clean up stale blob wants.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m43s
2025-09-23 20:06:50 -04:00
3aec7e6c14 ssb: Found that clicking on some message ids wasn't working.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m4s
2025-09-23 15:17:02 -04:00
9f0020dec8 update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-09-23 15:01:11 -04:00
6e78ad9729 ssb: Fix multiple issues with blob wants determination. 2025-09-23 14:40:53 -04:00
44d84a9b2a test: Add a blob replication test that succeeds for all the wrong reasons. 2025-09-22 08:18:15 -04:00
ac7809415c ssb: Fix bad escape on the query tab. 2025-09-22 07:48:11 -04:00
675cecaa20 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m27s
2025-09-17 03:56:14 -04:00
5d179cc088 update: OpenSSL 3.5.3. 2025-09-17 03:55:36 -04:00
d905618590 update: QuickJS 2025-09-13. 2025-09-17 03:51:07 -04:00
3fd9bc0b18 format
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m11s
2025-09-12 15:17:04 -04:00
39abee7f73 ssb: This fixes the compose menu being clipped sometimes. Probably breaks something somewhere else.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-09-09 19:27:32 -04:00
b770619111 ssb: This fixes the attach menu being clipped. I will discover later what it breaks, I'm sure. 2025-09-09 19:20:46 -04:00
1c44857da4 core: Move register and unregister to C. 2025-09-09 19:09:37 -04:00
bca4440867 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-09-09 09:43:42 -04:00
4855543961 docs: Not meant to be versioned.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m26s
2025-09-03 20:33:06 -04:00
cb3d6a98b9 docs: Appease all/both of the doxygen versions.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m45s
2025-09-03 18:47:34 -04:00
ada67a13d3 update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m6s
2025-09-03 12:14:58 -04:00
f4c928f26e android: Of course you can't put null in a LinkedBlockingQueue. Shrug. Fixes a shutdown crash.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 43m31s
2025-09-02 20:57:05 -04:00
91fd515d39 android: Cleaner shutdown still.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m38s
2025-08-27 20:23:02 -04:00
be6e841d3d android: This order seems more sensible.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 20:12:20 -04:00
af6afa6903 android: Don't log from the main thread. It might block?
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 19:20:02 -04:00
6ab5d2a28d build: Start work on 0.2025.9.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 18:55:24 -04:00
4be033f288 build: Do the nix dance. 2025-08-27 18:54:41 -04:00
81 changed files with 2455 additions and 3993 deletions

4
.gitmodules vendored
View File

@@ -19,10 +19,6 @@
[submodule "deps/picohttpparser"]
path = deps/picohttpparser
url = https://github.com/h2o/picohttpparser.git
[submodule "deps/openssl_src"]
path = deps/openssl_src
url = https://github.com/openssl/openssl.git
shallow = true
[submodule "deps/c-ares"]
path = deps/c-ares
url = https://github.com/c-ares/c-ares.git

View File

@@ -4,7 +4,6 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libc6-dev \
perl \
make
COPY . /app

358
Doxyfile
View File

@@ -1,4 +1,4 @@
# Doxyfile 1.9.8
# Doxyfile 1.9.4
# This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project.
@@ -19,8 +19,7 @@
# configuration file:
# doxygen -x [configFile]
# Use doxygen to compare the used configuration file with the template
# configuration file without replacing the environment variables or CMake type
# replacement variables:
# configuration file without replacing the environment variables:
# doxygen -x_noenv [configFile]
#---------------------------------------------------------------------------
@@ -86,7 +85,7 @@ CREATE_SUBDIRS = NO
# level increment doubles the number of directories, resulting in 4096
# directories at level 8 which is the default and also the maximum value. The
# sub-directories are organized in 2 levels, the first level always has a fixed
# number of 16 directories.
# numer of 16 directories.
# Minimum value: 0, maximum value: 8, default value: 8.
# This tag requires that the tag CREATE_SUBDIRS is set to YES.
@@ -363,17 +362,6 @@ MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5
# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
# generate identifiers for the Markdown headings. Note: Every identifier is
# unique.
# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
# sequence number starting at 0 and GITHUB use the lower case version of title
# with any whitespace replaced by '-' and punctuation characters removed.
# The default value is: DOXYGEN.
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
MARKDOWN_ID_STYLE = DOXYGEN
# When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or
@@ -498,14 +486,6 @@ LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 1
# If the TIMESTAMP tag is set different from NO then each generated page will
# contain the date or date and time when the page was generated. Setting this to
# NO can help when comparing the output of multiple runs.
# Possible values are: YES, NO, DATETIME and DATE.
# The default value is: NO.
TIMESTAMP = NO
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
@@ -587,8 +567,7 @@ HIDE_UNDOC_MEMBERS = NO
# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
# undocumented classes that are normally visible in the class hierarchy. If set
# to NO, these classes will be included in the various overviews. This option
# will also hide undocumented C++ concepts if enabled. This option has no effect
# if EXTRACT_ALL is enabled.
# has no effect if EXTRACT_ALL is enabled.
# The default value is: NO.
HIDE_UNDOC_CLASSES = NO
@@ -626,8 +605,7 @@ INTERNAL_DOCS = NO
# Windows (including Cygwin) and MacOS, users should typically set this option
# to NO, whereas on Linux or other Unix flavors it should typically be set to
# YES.
# Possible values are: SYSTEM, NO and YES.
# The default value is: SYSTEM.
# The default value is: system dependent.
CASE_SENSE_NAMES = YES
@@ -879,26 +857,11 @@ WARN_IF_INCOMPLETE_DOC = YES
WARN_NO_PARAMDOC = NO
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
# undocumented enumeration values. If set to NO, doxygen will accept
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled.
# The default value is: NO.
WARN_IF_UNDOC_ENUM_VAL = NO
# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
# at the end of the doxygen process doxygen will return with a non-zero status.
# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves
# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
# write the warning messages in between other messages but write them at the end
# of a run, in case a WARN_LOGFILE is defined the warning messages will be
# besides being in the defined file also be shown at the end of a run, unless
# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
# the behavior will remain as with the setting FAIL_ON_WARNINGS.
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# Possible values are: NO, YES and FAIL_ON_WARNINGS.
# The default value is: NO.
WARN_AS_ERROR = NO
@@ -947,7 +910,6 @@ INPUT = README.md \
core/app.js \
core/client.js \
core/core.js \
core/http.js \
core/tfrpc.js \
docs/ \
src/
@@ -957,21 +919,10 @@ INPUT = README.md \
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see:
# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
# See also: INPUT_FILE_ENCODING
# The default value is: UTF-8.
INPUT_ENCODING = UTF-8
# This tag can be used to specify the character encoding of the source files
# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify
# character encoding on a per file pattern basis. Doxygen will compare the file
# name with each pattern and apply the encoding instead of the default
# INPUT_ENCODING) if there is a match. The character encodings are a list of the
# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding
# "INPUT_ENCODING" for further information on supported encodings.
INPUT_FILE_ENCODING =
# If the value of the INPUT tag contains directories, you can use the
# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
# *.h) to filter out the source-files in the directories.
@@ -983,12 +934,12 @@ INPUT_FILE_ENCODING =
# Note the list of default checked file patterns might differ from the list of
# default file extension mappings.
#
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,
# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl,
# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php,
# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be
# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
# *.vhdl, *.ucf, *.qsf and *.ice.
FILE_PATTERNS = *.h \
*.js \
@@ -1030,6 +981,9 @@ EXCLUDE_PATTERNS =
# output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories use the pattern */test/*
EXCLUDE_SYMBOLS =
@@ -1074,11 +1028,6 @@ IMAGE_PATH = docs/images/
# code is scanned, but not when the output code is generated. If lines are added
# or removed, the anchors will not be placed correctly.
#
# Note that doxygen will use the data processed and written to standard output
# for further processing, therefore nothing else, like debug statements or used
# commands (so in case of a Windows batch file always use @echo OFF), should be
# written to standard output.
#
# Note that for custom extensions or not directly supported extensions you also
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by doxygen.
@@ -1120,15 +1069,6 @@ FILTER_SOURCE_PATTERNS =
USE_MDFILE_AS_MAINPAGE = README.md
# The Fortran standard specifies that for fixed formatted Fortran code all
# characters from position 72 are to be considered as comment. A common
# extension is to allow longer lines before the automatic comment starts. The
# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can
# be processed before the automatic comment starts.
# Minimum value: 7, maximum value: 10000, default value: 72.
FORTRAN_COMMENT_AFTER = 72
#---------------------------------------------------------------------------
# Configuration options related to source browsing
#---------------------------------------------------------------------------
@@ -1266,11 +1206,10 @@ CLANG_DATABASE_PATH =
ALPHABETICAL_INDEX = YES
# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)
# that should be ignored while generating the index headers. The IGNORE_PREFIX
# tag works for classes, function and member names. The entity will be placed in
# the alphabetical list under the first letter of the entity name that remains
# after removing the prefix.
# In case all classes in a project start with a common prefix, all classes will
# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
# can be used to specify a prefix (or a list of prefixes) that should be ignored
# while generating the index headers.
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
IGNORE_PREFIX =
@@ -1349,12 +1288,7 @@ HTML_STYLESHEET =
# Doxygen will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the
# list).
# Note: Since the styling of scrollbars can currently not be overruled in
# Webkit/Chromium, the styling will be left out of the default doxygen.css if
# one or more extra stylesheets have been specified. So if scrollbar
# customization is desired it has to be added explicitly. For an example see the
# documentation.
# list). For an example see the documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET =
@@ -1369,19 +1303,6 @@ HTML_EXTRA_STYLESHEET =
HTML_EXTRA_FILES =
# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
# should be rendered with a dark or light theme.
# Possible values are: LIGHT always generate light mode output, DARK always
# generate dark mode output, AUTO_LIGHT automatically set the mode according to
# the user preference, use light mode if no preference is set (the default),
# AUTO_DARK automatically set the mode according to the user preference, use
# dark mode if no preference is set and TOGGLE allow to user to switch between
# light and dark mode via a button.
# The default value is: AUTO_LIGHT.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE = AUTO_LIGHT
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to
# this color. Hue is specified as an angle on a color-wheel, see
@@ -1412,6 +1333,15 @@ HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
# page will contain the date and time when the page was generated. Setting this
# to YES can help to show when doxygen was last run and thus if the
# documentation is up to date.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
#HTML_TIMESTAMP = NO
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will
@@ -1431,13 +1361,6 @@ HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = NO
# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
# dynamically folded and expanded in the generated HTML source code.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_CODE_FOLDING = YES
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to
@@ -1568,16 +1491,6 @@ BINARY_TOC = NO
TOC_EXPAND = NO
# The SITEMAP_URL tag is used to specify the full URL of the place where the
# generated documentation will be placed on the server by the user during the
# deployment of the documentation. The generated sitemap is called sitemap.xml
# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
# is specified no sitemap is generated. For information about the sitemap
# protocol see https://www.sitemaps.org
# This tag requires that the tag GENERATE_HTML is set to YES.
SITEMAP_URL =
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
@@ -1753,6 +1666,17 @@ HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
# generated for formulas are transparent PNGs. Transparent PNGs are not
# supported properly for IE 6.0, but are supported on all modern browsers.
#
# Note that when changing this option you need to delete any form_*.png files in
# the HTML output directory before the changes have effect.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
#FORMULA_TRANSPARENT = YES
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
# to create new LaTeX commands to be used in formulas as building blocks. See
# the section "Including formulas" for details.
@@ -1814,8 +1738,8 @@ MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
# extension names that should be enabled during MathJax rendering. For example
# for MathJax version 2 (see
# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):
# for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html
# #tex-and-latex-extensions):
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
# For example for MathJax version 3 (see
# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
@@ -2066,16 +1990,9 @@ PDF_HYPERLINKS = YES
USE_PDFLATEX = YES
# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.
# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch
# mode nothing is printed on the terminal, errors are scrolled as if <return> is
# hit at every error; missing files that TeX tries to input or request from
# keyboard input (\read on a not open input stream) cause the job to abort,
# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
# but there is no possibility of user interaction just like in batch mode,
# SCROLL In scroll mode, TeX will stop only for missing files to input or if
# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
# each error, asking for user intervention.
# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
# command to the generated LaTeX files. This will instruct LaTeX to keep running
# if errors occur, instead of asking the user for help.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
@@ -2096,6 +2013,14 @@ LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
# page will contain the date and time when the page was generated. Setting this
# to NO can help when comparing the output of multiple runs.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
#LATEX_TIMESTAMP = NO
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the
@@ -2261,39 +2186,13 @@ DOCBOOK_OUTPUT = docbook
#---------------------------------------------------------------------------
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures
# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
# the structure of the code including all documentation. Note that this feature
# is still experimental and incomplete at the moment.
# The default value is: NO.
GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to Sqlite3 output
#---------------------------------------------------------------------------
# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
# database with symbols found by doxygen stored in tables.
# The default value is: NO.
#GENERATE_SQLITE3 = NO
# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
# in front of it.
# The default directory is: sqlite3.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
#SQLITE3_OUTPUT = sqlite3
# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db
# database file will be recreated with each doxygen run. If set to NO, doxygen
# will warn if an a database file is already found and not modify it.
# The default value is: YES.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
#SQLITE3_RECREATE_DB = YES
#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------
@@ -2436,15 +2335,15 @@ TAGFILES =
GENERATE_TAGFILE =
# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces
# will be listed in the class and namespace index. If set to NO, only the
# inherited external classes will be listed.
# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
# the class index. If set to NO, only the inherited external classes will be
# listed.
# The default value is: NO.
ALLEXTERNALS = NO
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
# in the topic index. If set to NO, only the current project's groups will be
# in the modules index. If set to NO, only the current project's groups will be
# listed.
# The default value is: YES.
@@ -2458,9 +2357,16 @@ EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
#---------------------------------------------------------------------------
# Configuration options related to diagram generator tools
# Configuration options related to the dot tool
#---------------------------------------------------------------------------
# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
DIA_PATH =
# If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class.
# The default value is: YES.
@@ -2469,10 +2375,10 @@ HIDE_UNDOC_RELATIONS = YES
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see:
# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is
# set to NO
# The default value is: YES.
# The default value is: NO.
HAVE_DOT = YES
@@ -2486,51 +2392,37 @@ HAVE_DOT = YES
DOT_NUM_THREADS = 0
# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
# subgraphs. When you want a differently looking font in the dot files that
# doxygen generates you can specify fontname, fontcolor and fontsize attributes.
# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,
# Edge and Graph Attributes specification</a> You need to make sure dot is able
# to find the font, which can be done by putting it in a standard location or by
# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
# directory containing the font. Default graphviz fontsize is 14.
# The default value is: fontname=Helvetica,fontsize=10.
# When you want a differently looking font in the dot files that doxygen
# generates you can specify the font name using DOT_FONTNAME. You need to make
# sure dot is able to find the font, which can be done by putting it in a
# standard location or by setting the DOTFONTPATH environment variable or by
# setting DOT_FONTPATH to the directory containing the font.
# The default value is: Helvetica.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10"
#DOT_FONTNAME = Helvetica
# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can
# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a
# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about
# arrows shapes.</a>
# The default value is: labelfontname=Helvetica,labelfontsize=10.
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
# dot graphs.
# Minimum value: 4, maximum value: 24, default value: 10.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10"
#DOT_FONTSIZE = 10
# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes
# around nodes set 'shape=plain' or 'shape=plaintext' <a
# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>
# The default value is: shape=box,height=0.2,width=0.4.
# By default doxygen will tell dot to use the default font as specified with
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
# the path where dot can find it using this tag.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4"
#DOT_FONTPATH =
# You can set the path where dot can find font specified with fontname in
# DOT_COMMON_ATTR and others dot attributes.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_FONTPATH =
# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will
# generate a graph for each documented class showing the direct and indirect
# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.
# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance
# relations will be shown as texts / links.
# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.
# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
# graph for each documented class showing the direct and indirect inheritance
# relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
# to TEXT the direct and indirect inheritance relations will be shown as texts /
# links.
# Possible values are: NO, YES, TEXT and GRAPH.
# The default value is: YES.
CLASS_GRAPH = YES
@@ -2538,21 +2430,15 @@ CLASS_GRAPH = YES
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the
# class with other documented classes. Explicit enabling a collaboration graph,
# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
# command \collaborationgraph. Disabling a collaboration graph can be
# accomplished by means of the command \hidecollaborationgraph.
# class with other documented classes.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
COLLABORATION_GRAPH = YES
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
# groups, showing the direct groups dependencies. Explicit enabling a group
# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means
# of the command \groupgraph. Disabling a directory graph can be accomplished by
# means of the command \hidegroupgraph. See also the chapter Grouping in the
# manual.
# groups, showing the direct groups dependencies. See also the chapter Grouping
# in the manual.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2612,9 +2498,7 @@ TEMPLATE_RELATIONS = NO
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented
# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,
# can be accomplished by means of the command \includegraph. Disabling an
# include graph can be accomplished by means of the command \hideincludegraph.
# files.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2623,10 +2507,7 @@ INCLUDE_GRAPH = YES
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented
# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set
# to NO, can be accomplished by means of the command \includedbygraph. Disabling
# an included by graph can be accomplished by means of the command
# \hideincludedbygraph.
# files.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2666,10 +2547,7 @@ GRAPHICAL_HIERARCHY = YES
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the
# files in the directories. Explicit enabling a directory graph, when
# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
# \directorygraph. Disabling a directory graph can be accomplished by means of
# the command \hidedirectorygraph.
# files in the directories.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2685,13 +2563,12 @@ DIR_GRAPH_MAX_DEPTH = 1
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot. For an explanation of the image formats see the section
# output formats in the documentation of the dot tool (Graphviz (see:
# https://www.graphviz.org/)).
# http://www.graphviz.org/)).
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement).
# Possible values are: png, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd,
# gif, gif:cairo, gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd,
# png:cairo, png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
# png:gdiplus:gdiplus.
# The default value is: png.
# This tag requires that the tag HAVE_DOT is set to YES.
@@ -2723,12 +2600,11 @@ DOT_PATH =
DOTFILE_DIRS =
# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).
DIA_PATH =
MSCFILE_DIRS =
# The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile
@@ -2778,6 +2654,18 @@ DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
# background. This is disabled by default, because dot on Windows does not seem
# to support this out of the box.
#
# Warning: Depending on the platform used, enabling this option may lead to
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
# read).
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
#DOT_TRANSPARENT = NO
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
@@ -2805,19 +2693,3 @@ GENERATE_LEGEND = YES
# The default value is: YES.
DOT_CLEANUP = YES
# You can define message sequence charts within doxygen comments using the \msc
# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
# use a built-in version of mscgen tool to produce the charts. Alternatively,
# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
# specifying prog as the value, doxygen will call the tool as prog -T
# <outfile_format> -o <outputfile> <inputfile>. The external tool should support
# output file formats "png", "eps", "svg", and "ismap".
MSCGEN_TOOL =
# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).
MSCFILE_DIRS =

View File

@@ -16,9 +16,9 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 42
VERSION_CODE_IOS := 16
VERSION_NUMBER := 0.2025.8
VERSION_CODE := 44
VERSION_CODE_IOS := 18
VERSION_NUMBER := 0.2025.10-wip
VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.0
@@ -38,7 +38,6 @@ BUNDLETOOL = out/bundletool.jar
HAVE_WIN :=
HAVE_CROSS_AARCH64 :=
USE_SYSTEM_SSL :=
export SOURCE_DATE_EPOCH=1
export TZ=UTC
@@ -65,7 +64,6 @@ LDFLAGS += \
-lbsd \
-lnetwork \
-Wno-stringop-overflow
USE_SYSTEM_SSL := 1
HAVE_ANDROID = 0
HAVE_LINUX_IOS = 0
HAVE_LINUX_MACOS = 0
@@ -80,13 +78,12 @@ LDFLAGS += \
HAVE_ANDROID :=
HAVE_LINUX_IOS :=
HAVE_LINUX_MACOS :=
USE_SYSTEM_SSL := 1
else
$(error Unexpected host platform $(UNAME_S).)
endif
# Everything is set above.
$(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0) system_ssl=$(if $(USE_SYSTEM_SSL),1,0))
$(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0))
CFLAGS += \
-std=gnu11 \
@@ -270,16 +267,12 @@ $(WINDOWS_TARGETS): AS = $(CC)
$(WINDOWS_TARGETS): CFLAGS += \
-D_WIN32_WINNT=0x0A00 \
-DWINVER=0x0A00 \
-DNTDDI_VERSION=NTDDI_WIN10 \
-Iout/openssl/$(UNAME_S)/mingw64/usr/local/include
-DNTDDI_VERSION=NTDDI_WIN10
$(WINDOWS_TARGETS): LDFLAGS += \
-static \
-lm \
-Lout/openssl/$(UNAME_S)/mingw64/usr/local/lib
-lm
$(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc
$(AARCH64_TARGETS): AS = $(CC)
$(AARCH64_TARGETS): CFLAGS += -Iout/openssl/Linux/aarch64/usr/local/include
$(AARCH64_TARGETS): LDFLAGS += -Lout/openssl/Linux/aarch64/usr/local/lib
ifeq ($(UNAME_S),Darwin)
$(HOST_TARGETS): CC = xcrun clang
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
@@ -304,39 +297,12 @@ $(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-Wno-unknown-warning-option
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Iout/openssl/android/armeabi-v7a/usr/local/include
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Lout/openssl/android/armeabi-v7a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Iout/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Lout/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_TARGETS): CFLAGS += -Iout/openssl/android/x86/usr/local/include
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
$(ANDROID_X86_TARGETS): LDFLAGS += -Lout/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Iout/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Lout/openssl/android/x86_64/usr/local/lib
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(MACOS_TARGETS): LDFLAGS += -Wl,-dead_strip
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections -Wl,--as-needed
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
ifeq ($(UNAME_S),Darwin)
$(IOS_TARGETS): CFLAGS += -Iout/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/ios/ios64-xcrun/usr/local/lib
else
$(IOS_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/ios64-cross/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/ios64-cross/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
endif
$(IOSSIM_TARGETS): CFLAGS += -Iout/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Lout/openssl/ios/iossimulator-xcrun/usr/local/lib
$(HOST_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
$(HOST_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux)
@@ -824,9 +790,6 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \
-pthread \
-lm
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS) $(filter-out $(HOST_TARGETS),$(MACOS_TARGETS)): LDFLAGS += \
-lssl \
-lcrypto
ifneq ($(UNAME_S),Haiku)
ifneq ($(UNAME_S),OpenBSD)
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
@@ -834,8 +797,6 @@ $(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
endif
endif
$(WINDOWS_TARGETS): LDFLAGS += \
-lssl \
-lcrypto \
-lcrypt32 \
-ldbghelp \
-liphlpapi \
@@ -848,9 +809,7 @@ $(WINDOWS_TARGETS): LDFLAGS += \
$(ANDROID_TARGETS): LDFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-ldl \
-llog \
-lssl \
-lcrypto
-llog
$(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
-Wno-unknown-warning-option
$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
@@ -1193,6 +1152,7 @@ out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@echo "[ipa] $@"
@rm -rf $@.tmp $@
@mkdir -p $@.tmp/Payload/tildefriends.app/
@cp src/ios/tildefriends512.png $@.tmp/iTunesArtwork
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
@rm -rf $@.tmp/
@@ -1224,94 +1184,6 @@ iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install,
xcrun simctl launch booted com.unprompted.tildefriends
.PHONY: iossimdebuggo
ANDROID_DEPS := out/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@export ANDROID_NDK_ROOT=$(ANDROID_NDK)
+@export BUILD_PLATFORM=android
+@export TOOLCHAIN=$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64
+@PATH="$$TOOLCHAIN/x86_64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86_64 SSL_TARGET=android-x86_64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/i686-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86 SSL_TARGET=android-x86 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/arm-linux-androideabi/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=armeabi-v7a SSL_TARGET=android-arm OPTIONS="--target=armv7a-linux-androideabi -Wl,--fix-cortex-a8 -D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/aarch64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=arm64-v8a SSL_TARGET=android-arm64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(UNAME_S),Linux)
ifneq ($(USE_SYSTEM_SSL),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS="--cross-compile-prefix=aarch64-linux-gnu-" BUILD_TARGET=aarch64 SSL_TARGET=linux-aarch64 tools/ssl-local
$(filter $(BUILD_DIR)/armdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/armrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_IOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/ios64-cross/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=deps/ios_toolchain/target/bin:$$PATH \
BUILD_TARGET=ios64-cross \
SSL_TARGET=ios64-cross \
CROSS_COMPILE=../../deps/ios_toolchain/target/bin/arm-apple-darwin11- \
CROSS_TOP=../../deps/ios_toolchain/target \
CROSS_SDK=iPhoneOS18.2.sdk \
CC=clang \
OPTIONS=-miphoneos-version-min=$(IPHONEOS_VERSION_MIN) \
tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_MACOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-arm/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-arm \
SSL_TARGET=darwin64-arm64 \
CC=../../deps/macos_toolchain/bin/oa64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/arm64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-arm/% $(BUILD_DIR)/macosdebug-arm/%,$(APP_OBJS)): | $(LOCAL_DEPS)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-x86_64 \
SSL_TARGET=darwin64-x86_64 \
CC=../../deps/macos_toolchain/bin/o64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-x86_64/% $(BUILD_DIR)/macosdebug-x86_64/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
endif
ifeq ($(UNAME_S),Darwin)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := out/openssl/$(UNAME_S)/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS):
+@BUILD_TARGET=mingw64 SSL_TARGET=mingw64 OPTIONS="--cross-compile-prefix=x86_64-w64-mingw32-" tools/ssl-local
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
endif
ifeq ($(UNAME_S),Darwin)
IOS_DEPS := out/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
$(IOS_DEPS):
+@BUILD_PLATFORM=ios BUILD_TARGET=ios64-xcrun SSL_TARGET=ios64-xcrun OPTIONS="-fPIC -Wno-macro-redefined -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)" tools/ssl-local
+@BUILD_PLATFORM=ios BUILD_TARGET=iossimulator-xcrun SSL_TARGET=iossimulator-xcrun OPTIONS="-fPIC -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends
@echo [lipo] $@
@mkdir -p $(@D)

View File

@@ -38,8 +38,6 @@ dependencies in the right places.
### Requirements
System OpenSSL libraries are assumed to be available on Haiku and OpenBSD.
On MacOS, Xcode's command-line tools are expected to be available.
### Build Commands

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📜",
"previous": "&BEf0nraBdHk/+PWqx6tOSu5rheWVaxaL7orAOz3285M=.sha256"
"previous": "&sJqeyYjHys6Z8IqqtZ2ij2ZC1E2xieu/FU/u2hE+O1U=.sha256"
}

View File

@@ -55,6 +55,9 @@ app.setDocument(`<head>
</head>
<body style="color:#fff">
${markdown(docs.docs.global)}
<!--
${Object.keys(docs.docs).filter(x => [...treeify('', globalThis)].indexOf(x) == -1).map(x => `<p>STALE: ${x}</p>`).join('')}
-->
${[...treeify('', globalThis)].map(x => document(x)).join('\n')}
<a id="Database"></a>
${markdown(docs.docs.database)}

View File

@@ -195,51 +195,6 @@ Call a function after some delay.
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
`;
docs['parseHttpRequest()'] = `
Parses an HTTP request.
### Parameters
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
### Returns
* *Integer* **-2** if the request is incomplete.
* *Integer* **-1** if the request could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
`;
docs['parseHttpResponse()'] = `
Parses an HTTP response.
### Parameters
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
### Returns
* *Integer* **-2** if the response is incomplete.
* *Integer* **-1** if the response could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
`;
docs['sha1Digest()'] = `
Calculates a SHA1 digest.
Completes synchronously.
### Parameters
* *String* **value** The value for which to calculate the digest.
### Returns
*String* The SHA1 digest of UTF-8 encoded \`value\`.
`;
docs['maskBytes()'] = `
Masks bytes for WebSocket communication.
Completes synchronously.
### Parameters
* *Uint8Array* **bytes** The byte array of data to mask.
* *Uint32* **mask** The mask to apply.
### Returns
*Uint32Array* The masked bytes.
`;
docs['exit()'] = `
Exits the app. But why would you want to do that?

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&Hd6CuhhnZIf13PdFJYZBUYLYZO84WdaKvWXLC29M7Ac=.sha256"
"previous": "&ceivZzkWd2Bvvdzdvdv68bZm2Y9oC0J352C78ZmMMU4=.sha256"
}

View File

@@ -76,6 +76,9 @@ tfrpc.register(function setHash(hash) {
core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
core.register('onBlob', async function (id) {
await tfrpc.rpc.notifyNewBlob(id);
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);

View File

@@ -1,6 +1,6 @@
import * as tfrpc from '/static/tfrpc.js';
import {html, render} from './lit-all.min.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
let g_emojis;
@@ -140,6 +140,9 @@ export async function picker(callback, anchor, author, recent) {
<style>
${styles}
</style>
<style>
${generate_theme()}
</style>
<div
class="w3-modal"
style="display: block; box-sizing: border-box; z-index: 10"

View File

@@ -12,12 +12,14 @@ import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js';
import * as tf_styles from './tf-styles.js';
window.addEventListener('load', function () {
let style = document.createElement('style');
style.innerText = tf_styles.styles;
Promise.resolve(tf_styles.generate_theme()).then(function (x) {
style.innerText += x;
});
document.body.appendChild(style);
});

View File

@@ -1,6 +1,6 @@
import {LitElement, html, css, guard, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfElement extends LitElement {
static get properties() {
@@ -65,6 +65,17 @@ class TfElement extends LitElement {
tfrpc.register(async function notifyNewMessage(id) {
await self.fetch_new_message(id);
});
tfrpc.register(async function notifyNewBlob(id) {
window.dispatchEvent(
new CustomEvent('blob-stored', {
bubbles: true,
composed: true,
detail: {
id: id,
},
})
);
});
tfrpc.register(function set(name, value) {
if (name === 'broadcasts') {
self.broadcasts = value;
@@ -195,8 +206,6 @@ class TfElement extends LitElement {
this.tab = 'search';
} else if (this.hash === '#connections') {
this.tab = 'connections';
} else if (this.hash.startsWith('#sql=')) {
this.tab = 'query';
} else {
this.tab = 'news';
}
@@ -725,17 +734,6 @@ class TfElement extends LitElement {
: null}
></tf-tab-search>
`;
} else if (this.tab === 'query') {
return html`
<tf-tab-query
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#sql=')
? decodeURIComponent(this.hash.substring(5))
: null}
></tf-tab-query>
`;
}
}
@@ -746,8 +744,6 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections');
} else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql=');
}
}
@@ -767,6 +763,17 @@ class TfElement extends LitElement {
}
}
async pick_color() {
let input = document.createElement('input');
input.type = 'color';
input.value = (await tfrpc.rpc.localStorageGet('color')) ?? '#ff0000';
input.addEventListener('change', async function () {
await tfrpc.rpc.localStorageSet('color', input.value);
window.location.reload();
});
input.click();
}
render() {
let self = this;
@@ -781,7 +788,6 @@ class TfElement extends LitElement {
'📰': 'news',
'📡': 'connections',
'🔍': 'search',
'👩‍💻': 'query',
};
let tabs = html`
@@ -789,14 +795,13 @@ class TfElement extends LitElement {
class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10"
>
${this.is_administrator && self.tab != 'news'
${this.is_administrator
? html`
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh}
>
@@ -825,6 +830,12 @@ class TfElement extends LitElement {
</button>
`
)}
<button
class="w3-bar-item w3-button w3-right"
@click=${this.pick_color}
>
🎨<span class="w3-hide-small">Color</span>
</button>
</div>
`;
let contents = this.guest
@@ -860,6 +871,9 @@ class TfElement extends LitElement {
`
: undefined;
return html`
<style>
${generate_theme()}
</style>
<div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark"

View File

@@ -1,7 +1,7 @@
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
import * as tfutils from './tf-utils.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
import Tribute from './tribute.esm.js';
class TfComposeElement extends LitElement {
@@ -603,6 +603,9 @@ class TfComposeElement extends LitElement {
🔐 Encrypt
</button>`;
let result = html`
<style>
${generate_theme()}
</style>
<style>
.w3-input:empty::before {
content: attr(placeholder);
@@ -625,7 +628,7 @@ class TfComposeElement extends LitElement {
<div class="w3-half">
<span
class="w3-input w3-theme-d1 w3-border"
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
style="resize: vertical; width: 100%; white-space: pre-wrap"
placeholder="Write a post here."
id="edit"
@input=${this.input}

View File

@@ -4,12 +4,14 @@ import {
html,
repeat,
render,
unsafeCSS,
unsafeHTML,
until,
} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import * as emojis from './emojis.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfMessageElement extends LitElement {
static get properties() {
@@ -24,6 +26,7 @@ class TfMessageElement extends LitElement {
channel: {type: String},
channel_unread: {type: Number},
recent_reactions: {type: Array},
depth: {type: Number},
};
}
@@ -40,16 +43,20 @@ class TfMessageElement extends LitElement {
this.expanded = {};
this.channel_unread = -1;
this.recent_reactions = [];
this.depth = 0;
}
connectedCallback() {
super.connectedCallback();
this._click_callback = this.document_click.bind(this);
this._blob_stored = this.blob_stored.bind(this);
document.body.addEventListener('mouseup', this._click_callback);
window.addEventListener('blob-stored', this._blob_stored);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('blob-stored', this._blob_stored);
document.body.removeEventListener('mouseup', this._click_callback);
}
@@ -61,6 +68,16 @@ class TfMessageElement extends LitElement {
}
}
blob_stored(event) {
let search = `/${event.detail.id}/view`;
for (let img of this.shadowRoot.querySelectorAll('img')) {
if (img.src.indexOf(search) != -1) {
let src = img.src.split('?')[0];
img.src = `${src}?${new Date().valueOf()}`;
}
}
}
show_reply() {
let event = new CustomEvent('tf-draft', {
bubbles: true,
@@ -311,7 +328,9 @@ class TfMessageElement extends LitElement {
}
expanded_key() {
return this.message?.id || this.messages?.map((x) => x.id).join(':');
return (
this.message?.id || this.message?.messages?.map((x) => x.id).join(':')
);
}
set_expanded(expanded, tag) {
@@ -349,12 +368,13 @@ class TfMessageElement extends LitElement {
</button>
`;
} else {
return html` <div class="w3-container w3-margin-bottom">
${repeat(
this.message.child_messages || [],
(x) => x.id,
(x) =>
html`<tf-message
return html` <ul class="w3-container w3-margin-bottom w3-ul w3-card-4">
${repeat(
this.message.child_messages || [],
(x) => x.id,
(x) =>
html`<li style="padding: 0">
<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
@@ -363,16 +383,20 @@ class TfMessageElement extends LitElement {
channel=${this.channel}
channel_unread=${this.channel_unread}
.recent_reactions=${this.recent_reactions}
></tf-message>`
)}
</div>
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(false)}
>
Collapse
</button>`;
depth=${this.depth + 1}
></tf-message>
</li>`
)}
<li style="padding: 0" class="w3-margin-bottom">
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(false)}
>
Collapse
</button>
</li>
</ul>`;
}
} else {
return undefined;
@@ -536,8 +560,11 @@ class TfMessageElement extends LitElement {
}
</style>
<div
class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top"
style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%"
class="w3-card-4 ${this.class_background()} w3-border-theme ${this
.depth == 0
? 'w3-margin-top'
: ''}"
style="overflow-wrap: anywhere; display: block; max-width: 100%"
>
${inner}
</div>
@@ -563,6 +590,7 @@ class TfMessageElement extends LitElement {
channel=${self.channel}
channel_unread=${self.channel_unread}
.recent_reactions=${self.recent_reactions}
depth=${self.depth + 1}
></tf-message>
`
)}
@@ -598,15 +626,17 @@ class TfMessageElement extends LitElement {
let sorted = this.message.messages
.map((x) => [
x.author,
x.content.blocking !== undefined
? x.content.blocking
? 'is blocking'
: 'is no longer blocking'
: x.content.following !== undefined
? x.content.following
? 'is following'
: 'is no longer following'
: '',
x.content.following && x.content.blocking
? 'is following and blocking'
: x.content.following
? 'is following'
: x.content.blocking
? 'is blocking'
: x.content.blocking !== undefined
? 'is no longer blocking'
: x.content.following !== undefined
? 'is no longer following'
: '',
x.content.contact,
x,
])
@@ -631,6 +661,35 @@ class TfMessageElement extends LitElement {
return result;
}
channel_group_by_author() {
let sorted = this.message.messages
.map((x) => [
x.author,
x.content.subscribed ? 'subscribed to' : 'unsubscribed from',
x.content.channel,
x,
])
.sort();
let result = [];
let last;
let group;
for (let row of sorted) {
if (last && last[0] == row[0] && last[1] == row[1]) {
group.push(row[2]);
} else {
if (group) {
result.push({author: last[0], action: last[1], channels: group});
}
last = row;
group = [row[2]];
}
}
if (group) {
result.push({author: last[0], action: last[1], channels: group});
}
return result;
}
allow_unread() {
return (
this.channel == '@' ||
@@ -644,7 +703,7 @@ class TfMessageElement extends LitElement {
: undefined;
}
render() {
_render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
@@ -665,6 +724,7 @@ class TfMessageElement extends LitElement {
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
depth=${this.depth + 1}
></tf-message>`
)}
</div>
@@ -706,6 +766,56 @@ class TfMessageElement extends LitElement {
</button>
`);
}
} else if (this.message?.type === 'channel_group') {
if (this.expanded[this.expanded_key()]) {
return this.render_frame(html`
<div class="w3-padding">
${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
depth=${this.depth + 1}
></tf-message>`
)}
</div>
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(false)}
>
Collapse
</button>
`);
} else {
return this.render_frame(html`
<div class="w3-padding">
${this.channel_group_by_author().map(
(x) => html`
<div>
<tf-user id=${x.author} .users=${this.users}></tf-user>
${x.action}
${x.channels.map(
(y) => html` <tf-tag tag=${'#' + y}></tf-tag> `
)}
</div>
`
)}
</div>
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(true)}
>
Expand
</button>
`);
}
} else if (this.message.placeholder) {
return this.render_frame(
html`<div>
@@ -751,6 +861,7 @@ class TfMessageElement extends LitElement {
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
depth=${this.depth + 1}
></tf-message>
`
)}
@@ -986,6 +1097,15 @@ class TfMessageElement extends LitElement {
return this.render_small_frame(this.render_raw());
}
}
render() {
return html`
<style>
${generate_theme()}
</style>
${this._render()}
`;
}
}
customElements.define('tf-message', TfMessageElement);

View File

@@ -1,6 +1,6 @@
import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfNewsElement extends LitElement {
static get properties() {
@@ -160,11 +160,29 @@ class TfNewsElement extends LitElement {
return recursive_sort(roots, true);
}
group_following(messages) {
group_messages(messages) {
let result = [];
let group = [];
let type = undefined;
for (let message of messages) {
if (message?.content?.type === 'contact') {
if (
message?.content?.type === 'contact' ||
message?.content?.type === 'channel'
) {
if (type && message.content.type !== type) {
if (group.length == 1) {
result.push(group[0]);
group = [];
} else if (group.length > 1) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: `${type}_group`,
messages: group,
});
group = [];
}
}
type = message.content.type;
group.push(message);
} else {
if (group.length == 1) {
@@ -173,12 +191,13 @@ class TfNewsElement extends LitElement {
} else if (group.length > 1) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
type: `${type}_group`,
messages: group,
});
group = [];
}
result.push(message);
type = undefined;
}
}
if (group.length == 1) {
@@ -187,7 +206,7 @@ class TfNewsElement extends LitElement {
} else if (group.length > 1) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
type: `${type}_group`,
messages: group,
});
}
@@ -200,7 +219,7 @@ class TfNewsElement extends LitElement {
load_and_render(messages) {
let messages_by_id = this.process_messages(messages);
let final_messages = this.group_following(
let final_messages = this.group_messages(
this.finalize_messages(messages_by_id)
);
let unread_rowid = -1;
@@ -212,6 +231,9 @@ class TfNewsElement extends LitElement {
}
}
return html`
<style>
${generate_theme()}
</style>
<div>
${repeat(
final_messages,

View File

@@ -1,7 +1,7 @@
import {LitElement, html, until, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfProfileElement extends LitElement {
static get properties() {
@@ -316,7 +316,9 @@ class TfProfileElement extends LitElement {
}
image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description;
return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
return html`
<style>${generate_theme()}</style>
<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<header class="w3-container">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
</header>

View File

@@ -1,5 +1,5 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfReactionsModalElement extends LitElement {
static get properties() {
@@ -24,50 +24,57 @@ class TfReactionsModalElement extends LitElement {
render() {
let self = this;
return this.votes?.length
? html` <div
class="w3-modal w3-animate-opacity"
style="display: block; box-sizing: border-box; z-index: 10"
@click=${this.clear}
>
? html` <style>
${generate_theme()}
</style>
<div
class="w3-modal-content w3-card-4 w3-theme-d1"
onclick="event.stopPropagation()"
class="w3-modal w3-animate-opacity"
style="display: block; box-sizing: border-box; z-index: 10"
@click=${this.clear}
>
<div class="w3-container w3-padding">
<header class="w3-container">
<h2>Reactions</h2>
<span class="w3-button w3-display-topright" @click=${this.clear}
>&times;</span
>
</header>
<ul class="w3-theme-dark w3-container w3-ul">
${this.votes
.sort((x, y) => y.timestamp - x.timestamp)
.map(
(x) => html`
<li style="display: flex; flex-direction: row; gap: 4px">
<span style="flex-basis: 3em"
>${x?.content?.vote?.expression}</span
<div
class="w3-modal-content w3-card-4 w3-theme-d1"
onclick="event.stopPropagation()"
>
<div class="w3-container w3-padding">
<header class="w3-container">
<h2>Reactions</h2>
<span
class="w3-button w3-display-topright"
@click=${this.clear}
>&times;</span
>
</header>
<ul class="w3-theme-dark w3-container w3-ul">
${this.votes
.sort((x, y) => y.timestamp - x.timestamp)
.map(
(x) => html`
<li
style="display: flex; flex-direction: row; gap: 4px"
>
<tf-user
style="flex: 1 1"
id=${x.author}
.users=${this.users}
></tf-user>
<span
style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>${new Date(x?.timestamp).toLocaleString()}</span
>
</li>
`
)}
</ul>
<footer class="w3-container w3-padding">
<button class="w3-button" @click=${this.clear}>Close</button>
</footer>
<span style="flex-basis: 3em"
>${x?.content?.vote?.expression}</span
>
<tf-user
style="flex: 1 1"
id=${x.author}
.users=${this.users}
></tf-user>
<span
style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>${new Date(x?.timestamp).toLocaleString()}</span
>
</li>
`
)}
</ul>
<footer class="w3-container w3-padding">
<button class="w3-button" @click=${this.clear}>Close</button>
</footer>
</div>
</div>
</div>
</div>`
</div>`
: undefined;
}
}

View File

@@ -1,4 +1,5 @@
import {css, unsafeCSS} from './lit-all.min.js';
import {css, unsafeCSS, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
const tf = css`
img {
@@ -408,16 +409,8 @@ function is_dark(hex, value) {
return (r * 299 + g * 587 + b * 114) / 1000 < value;
}
function generated() {
let now = new Date();
let k_color = rgb_to_hex([
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
]);
//let k_color = '#034f84';
//let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]);
let [r, g, b] = hex_to_rgb(k_color);
export function generate(color) {
let [r, g, b] = hex_to_rgb(color);
let [h, s, l] = rgb_to_hsl(r, g, b);
let theme1 = {
@@ -461,4 +454,28 @@ function generated() {
return unsafeCSS(result);
}
export let styles = [tf, w3, generated()];
let g_theme;
export function generate_theme() {
return g_theme
? g_theme
: until(
tfrpc.rpc.localStorageGet('color').then(function (value) {
g_theme = generate(value ?? '#034f84');
return g_theme;
}),
generated_now()
);
}
function generated_now() {
let now = new Date();
return generate(
rgb_to_hex([
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
])
);
}
export let styles = [tf, w3];

View File

@@ -1,6 +1,6 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfTabConnectionsElement extends LitElement {
static get properties() {
@@ -251,6 +251,9 @@ class TfTabConnectionsElement extends LitElement {
render() {
let self = this;
return html`
<style>
${generate_theme()}
</style>
<div class="w3-container" style="box-sizing: border-box">
<h2>New Connection</h2>
<textarea class="w3-input w3-theme-d1" id="code"></textarea>

View File

@@ -1,6 +1,6 @@
import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfTabNewsFeedElement extends LitElement {
static get properties() {
@@ -256,14 +256,24 @@ class TfTabNewsFeedElement extends LitElement {
let t0 = new Date();
let initial_messages = await tfrpc.rpc.query(
`
WITH
channels AS (SELECT '#' || value AS value FROM json_each(?5))
SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
messages.content ->> 'type' != 'vote'
FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
messages.content ->> 'type' != 'vote' AND
(messages.content ->> 'channel' IS NULL OR ('#' || (messages.content ->> 'channel')) NOT IN (SELECT * FROM channels)) AND
NOT EXISTS (SELECT * FROM messages_refs JOIN channels ON messages_refs.message = messages.id AND messages_refs.ref = channels.value)
ORDER BY timestamp DESC LIMIT ?4
`,
[JSON.stringify(this.following), start_time, end_time, k_max_results]
[
JSON.stringify(this.following),
start_time,
end_time,
k_max_results,
JSON.stringify(Object.keys(this.channels_latest)),
]
);
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
@@ -396,6 +406,9 @@ class TfTabNewsFeedElement extends LitElement {
this._private_messages =
JSON.stringify(this.private_messages) +
JSON.stringify(this.grouped_private_messages);
this._channels_latest = JSON.stringify(
Object.keys(this.channels_latest ?? {})
);
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
@@ -471,7 +484,9 @@ class TfTabNewsFeedElement extends LitElement {
this._messages_following !== JSON.stringify(this.following) ||
this._private_messages !==
JSON.stringify(this.private_messages) +
JSON.stringify(this.grouped_private_messages)
JSON.stringify(this.grouped_private_messages) ||
this._channels_latest !==
JSON.stringify(Object.keys(this.channels_latest))
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
@@ -523,6 +538,9 @@ class TfTabNewsFeedElement extends LitElement {
`;
}
return cache(html`
<style>
${generate_theme()}
</style>
${this.unread_allowed()
? html`<button
class="w3-button w3-theme-d1"

View File

@@ -7,7 +7,7 @@ import {
until,
} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfTabNewsElement extends LitElement {
static get properties() {
@@ -211,35 +211,6 @@ class TfTabNewsElement extends LitElement {
>
&times;
</div>
${this.is_administrator
? html`
<button
class="w3-bar-item w3-button"
@click=${() =>
this.dispatchEvent(
new Event('refresh', {bubbles: true, composed: true})
)}
>
<span style="display: inline-block; width: 1.8em">↻</span>
Sync now
</button>
<button
class="w3-bar-item w3-button w3-ripple"
@click=${() =>
this.dispatchEvent(
new Event('toggle_stay_connected', {
bubbles: true,
composed: true,
})
)}
>
<span style="display: inline-block; width: 1.8em"
>${this.stay_connected ? '🔗' : '⛓️‍💥'}</span
>
${this.stay_connected ? 'Online mode' : 'Passive mode'}
</button>
`
: undefined}
${this.hash.startsWith('##') &&
this.channels.indexOf(this.hash.substring(2)) == -1
? html`
@@ -334,6 +305,21 @@ class TfTabNewsElement extends LitElement {
>
↻ Sync now
</button>
<button
class="w3-bar-item w3-button w3-ripple"
@click=${() =>
this.dispatchEvent(
new Event('toggle_stay_connected', {
bubbles: true,
composed: true,
})
)}
>
<span style="display: inline-block; width: 1.8em"
>${this.stay_connected ? '🔗' : '⛓️‍💥'}</span
>
${this.stay_connected ? 'Online mode' : 'Passive mode'}
</button>
<button
class=${'w3-bar-item w3-button' +
(this.peer_exchange !== false ? ' w3-hide' : '')}
@@ -409,9 +395,12 @@ class TfTabNewsElement extends LitElement {
</div>`;
}
return cache(html`
<style>
${generate_theme()}
</style>
${this.render_sidebar()}
<div
style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto; contain: layout"
style="margin-left: 2in; padding: 0px; top: 0; height: 100vh; max-height: 100%; overflow: auto; contain: layout"
id="main"
class="w3-main"
>

View File

@@ -1,136 +0,0 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabQueryElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
following: {type: Array},
query: {type: String},
expanded: {type: Object},
results: {type: Array},
error: {type: Object},
duration: {type: Number},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.following = [];
this.expanded = {};
this.duration = undefined;
}
async search(query) {
console.log('Searching...', this.whoami, query);
this.results = [];
this.error = undefined;
this.duration = undefined;
let search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
}
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
let start_time = new Date();
try {
this.results = await tfrpc.rpc.query(query, []);
} catch (error) {
this.error = error;
}
let end_time = new Date();
this.duration = (end_time - start_time).valueOf();
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
}
}
search_keydown(event) {
if (event.keyCode == 13 && event.ctrlKey) {
this.query = this.renderRoot.getElementById('search').value;
event.preventDefault();
}
}
on_expand(event) {
if (event.detail.expanded) {
let expand = {};
expand[event.detail.id] = true;
this.expanded = Object.assign({}, this.expanded, expand);
} else {
delete this.expanded[event.detail.id];
this.expanded = Object.assign({}, this.expanded);
}
}
render_results() {
if (!this.results?.length) {
return html`<div>No results.</div>`;
} else {
let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%">
<tr>
${keys.map((key) => html`<th>${key}</th>`)}
</tr>
${this.results.map(
(row) =>
html`<tr>
${keys.map((key) => html`<td>${row[key]}</td>`)}
</tr>`
)}
</table>`;
}
}
render_error() {
if (this.error) {
return html`<h2 style="color: red">${this.error.message}</h2>
<pre style="color: red">${this.error.stack}</pre>`;
}
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
}
let self = this;
return html`
<div style="display: flex; flex-direction: row; gap: 4px">
<textarea
id="search"
rows="8"
class="w3-input w3-theme-d1"
style="flex: 1; resize: vertical"
@keydown=${this.search_keydown}
>
${this.query}</textarea
>
<button
class="w3-button w3-theme-d1"
@click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)}
>
Execute
</button>
</div>
<div ?hidden=${this.duration === undefined}>
Took ${this.duration / 1000.0} seconds.
</div>
<div ?hidden=${this.duration !== undefined}>Executing...</div>
${this.render_error()} ${this.render_results()}
`;
}
}
customElements.define('tf-tab-query', TfTabQueryElement);

View File

@@ -1,6 +1,6 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfTabSearchElement extends LitElement {
static get properties() {
@@ -11,6 +11,9 @@ class TfTabSearchElement extends LitElement {
following: {type: Array},
query: {type: String},
expanded: {type: Object},
messages: {type: Array},
results: {type: Array},
error: {type: Object},
};
}
@@ -38,24 +41,40 @@ class TfTabSearchElement extends LitElement {
search.select();
}
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
this.error = undefined;
this.results = [];
this.messages = [];
if (query.startsWith('sql:')) {
this.messages = [];
try {
this.results = await tfrpc.rpc.query(
query.substring('sql:'.length),
[]
);
} catch (e) {
this.results = [];
this.error = e;
}
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
}
this.renderRoot.getElementById('news').messages = results;
}
search_keydown(event) {
@@ -87,6 +106,39 @@ class TfTabSearchElement extends LitElement {
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
render_results() {
if (this.error) {
return html`<h2 style="color: red">${this.error.message}</h2>
<pre style="color: red">${this.error.stack}</pre>`;
} else if (this.messages?.length) {
return html`<tf-news
id="news"
whoami=${this.whoami}
.messages=${this.messages}
.users=${this.users}
.expanded=${this.expanded}
.drafts=${this.drafts}
@tf-expand=${this.on_expand}
@tf-draft=${this.draft}
></tf-news>`;
} else if (this.results?.length) {
let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%">
<tr>
${keys.map((key) => html`<th>${key}</th>`)}
</tr>
${this.results.map(
(row) =>
html`<tr>
${keys.map((key) => html`<td>${row[key]}</td>`)}
</tr>`
)}
</table>`;
} else {
return html`<div>No results.</div>`;
}
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
@@ -94,11 +146,14 @@ class TfTabSearchElement extends LitElement {
}
let self = this;
return html`
<div style="display: flex; flex-direction: row; gap: 4px">
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
<style>${generate_theme()}</style>
<div class="w3-padding">
<div style="display: flex; flex-direction: row; gap: 4px">
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div>
${this.render_results()}
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
`;
}
}

View File

@@ -1,5 +1,5 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfTagElement extends LitElement {
static get properties() {
@@ -17,11 +17,15 @@ class TfTagElement extends LitElement {
render() {
let number = this.count ? html` (${this.count})` : undefined;
return html`<a
href=${'#' + encodeURIComponent(this.tag)}
class="w3-tag w3-theme-d1 w3-round-4 w3-button"
>${this.tag}${number}</a
> `;
return html`
<style>
${generate_theme()}</style
><a
href=${'#' + encodeURIComponent(this.tag)}
class="w3-tag w3-theme-d1 w3-round-4 w3-button"
>${this.tag}${number}</a
>
`;
}
}

View File

@@ -1,6 +1,6 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import {styles, generate_theme} from './tf-styles.js';
class TfUserElement extends LitElement {
static get properties() {
@@ -58,12 +58,15 @@ class TfUserElement extends LitElement {
/>`;
}
}
return html` <div
style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' +
(this.nolink ? '' : '; font-weight: bold')}
>
${image} ${name}
</div>`;
return html` <style>
${generate_theme()}
</style>
<div
style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' +
(this.nolink ? '' : '; font-weight: bold')}
>
${image} ${name}
</div>`;
}
}

View File

@@ -104,12 +104,12 @@ export function markdown(md) {
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + node.destination;
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + node.destination;
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&5NkMRSgcMqCYF3xcLOBmaytkoxfV9zx4br7JladKPTs=.sha256"
"previous": "&n1QkPkB5JoduFSx8UKOY3IlZqS2GwLiTUZv4ZrEOthQ=.sha256"
}

View File

@@ -104,7 +104,7 @@
src="googleplay.svg"
style="height: 2em; margin: 0"
/>
Get it on Google Play (Open Testing)
Get it on Google Play
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
@@ -298,7 +298,7 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Built the Old Fashioned Way</b></h1>
<h1 class="w3-jumbo"><b>Built to Last</b></h1>
<p>
Tilde Friends strives to use only simple and widely adopted dependencies
in order to keep it easy to build for all sorts of platforms and
@@ -340,10 +340,6 @@
<i class="fa fa-lock w3-text-purple w3-jumbo"></i>
<p>libsodium</p>
</a>
<a href="https://github.com/openssl/openssl/releases" class="w3-col s3">
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
<p>OpenSSL</p>
</a>
<a
href="https://github.com/ianlancetaylor/libbacktrace"
class="w3-col s3"
@@ -351,13 +347,13 @@
<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
<p>libbacktrace</p>
</a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://codemirror.net/docs/changelog/" class="w3-col s3">
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
<p>CodeMirror</p>
</a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://github.com/jlfwong/speedscope/" class="w3-col s3">
<i class="fa fa-microscope w3-text-orange w3-jumbo"></i>
<p>Speedscope</p>
@@ -370,9 +366,6 @@
<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
<p>c-ares</p>
</a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://www.gnu.org/software/make/" class="w3-col s3">
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
<p>GNU Make</p>

View File

@@ -149,7 +149,7 @@ exports.app_socket = async function socket(request, response) {
parentApp: parentApp,
id: blobId,
},
await ssb.getIdentityInfo(
await ssb_internal.getIdentityInfo(
credentials?.session?.name,
packageOwner,
packageName

View File

@@ -9,18 +9,17 @@
import {LitElement, html, css, svg} from '/lit/lit-all.min.js';
let cm6;
let gSocket;
let gCurrentFile;
let gFiles = {};
let gApp = {files: {}, emoji: '📦'};
let gEditor;
let gOriginalInput;
let gUnloading;
let g_socket;
let g_current_file;
let g_files = {};
let g_app = {files: {}, emoji: '📦'};
let g_editor;
let g_unloading;
let kErrorColor = '#dc322f';
let kDisconnectColor = '#f00';
let kStatusColor = '#fff';
let k_color_error = '#dc322f';
let k_color_disconnect = '#f00';
let k_color_status = '#fff';
/** \endcond */
/** Functions that server-side app code can call through the app object. */
@@ -410,7 +409,7 @@ class TfNavigationElement extends LitElement {
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div
class="w3-bar-item"
style="color: ${this.status.color ?? kStatusColor}"
style="color: ${this.status.color ?? k_color_status}"
>
${this.status.message}
</div>
@@ -429,7 +428,7 @@ class TfNavigationElement extends LitElement {
<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1">
<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em">
<span id="close_error" @click=${self.clear_error} class="w3-button w3-display-topright">&times;</span>
<div style="color: ${this.status.color ?? kErrorColor}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div>
<div style="color: ${this.status.color ?? k_color_error}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div>
</div>
</div>
`
@@ -438,6 +437,9 @@ class TfNavigationElement extends LitElement {
}
}
/**
* Create a tf-navigation element.
*/
customElements.define('tf-navigation', TfNavigationElement);
/**
@@ -518,7 +520,7 @@ class TfFilesElement extends LitElement {
for (let file of event.dataTransfer.files) {
let buffer = await file.arrayBuffer();
let text = new TextDecoder('latin1').decode(buffer);
gFiles[file.name] = {
g_files[file.name] = {
doc: cm6.EditorState.create({
doc: text,
extensions: cm6.extensions,
@@ -526,9 +528,9 @@ class TfFilesElement extends LitElement {
buffer: buffer,
isNew: true,
};
gCurrentFile = file.name;
g_current_file = file.name;
}
openFile(gCurrentFile);
openFile(g_current_file);
updateFiles();
}
@@ -892,11 +894,11 @@ async function edit() {
: 'flex';
try {
if (!gEditor) {
if (!g_editor) {
cm6 = await import('/codemirror/cm6.js');
gEditor = cm6.TildeFriendsEditorView(document.getElementById('editor'));
g_editor = cm6.TildeFriendsEditorView(document.getElementById('editor'));
}
gEditor.onDocChange = updateFiles;
g_editor.onDocChange = updateFiles;
await load();
} catch (error) {
alert(`${error.message}\n\n${error.stack}`);
@@ -929,13 +931,13 @@ function loadFile(name, id) {
return response.text();
})
.then(function (text) {
gFiles[name].doc = cm6.EditorState.create({
g_files[name].doc = cm6.EditorState.create({
doc: text,
extensions: cm6.extensions,
});
gFiles[name].original = gFiles[name].doc.doc.toString();
if (!Object.values(gFiles).some((x) => !x.doc)) {
openFile(Object.keys(gFiles).sort()[0]);
g_files[name].original = g_files[name].doc.doc.toString();
if (!Object.values(g_files).some((x) => !x.doc)) {
openFile(Object.keys(g_files).sort()[0]);
}
});
}
@@ -953,31 +955,31 @@ async function load(path) {
} else if (response.status != 404) {
throw new Error(response.status + ' ' + response.statusText);
}
gFiles = {};
g_files = {};
let isApp = false;
let promises = [];
if (json && json['type'] == 'tildefriends-app') {
isApp = true;
Object.keys(json['files']).forEach(function (name) {
gFiles[name] = {};
g_files[name] = {};
promises.push(loadFile(name, json['files'][name]));
});
if (Object.keys(json['files']).length == 0) {
document.getElementById('editPane').style.display = 'flex';
}
gApp = json;
gApp.emoji = gApp.emoji || '📦';
document.getElementById('icon').innerHTML = gApp.emoji;
g_app = json;
g_app.emoji = g_app.emoji || '📦';
document.getElementById('icon').innerHTML = g_app.emoji;
}
if (!isApp) {
document.getElementById('editPane').style.display = 'flex';
let text = '// New script.\n';
gCurrentFile = 'app.js';
gFiles[gCurrentFile] = {
g_current_file = 'app.js';
g_files[g_current_file] = {
doc: cm6.EditorState.create({doc: text, extensions: cm6.extensions}),
};
openFile(gCurrentFile);
openFile(g_current_file);
}
return Promise.all(promises);
}
@@ -998,13 +1000,14 @@ function closeEditor() {
*/
function save(save_to) {
document.getElementById('save').disabled = true;
if (gCurrentFile) {
gFiles[gCurrentFile].doc = gEditor.state;
if (g_current_file) {
g_files[g_current_file].doc = g_editor.state;
if (
!gFiles[gCurrentFile].isNew &&
!gFiles[gCurrentFile].doc.doc.toString() == gFiles[gCurrentFile].original
!g_files[g_current_file].isNew &&
!g_files[g_current_file].doc.doc.toString() ==
g_files[g_current_file].original
) {
delete gFiles[gCurrentFile].buffer;
delete g_files[g_current_file].buffer;
}
}
@@ -1019,8 +1022,8 @@ function save(save_to) {
}
let promises = [];
for (let name of Object.keys(gFiles)) {
let file = gFiles[name];
for (let name of Object.keys(g_files)) {
let file = g_files[name];
if (!file.isNew && file.doc.doc.toString() == file.original) {
continue;
}
@@ -1062,14 +1065,14 @@ function save(save_to) {
let app = {
type: 'tildefriends-app',
files: Object.fromEntries(
Object.keys(gFiles).map((x) => [x, gFiles[x].id || gApp.files[x]])
Object.keys(g_files).map((x) => [x, g_files[x].id || g_app.files[x]])
),
emoji: gApp.emoji || '📦',
emoji: g_app.emoji || '📦',
};
Object.values(gFiles).forEach(function (file) {
Object.values(g_files).forEach(function (file) {
delete file.id;
});
gApp = JSON.parse(JSON.stringify(app));
g_app = JSON.parse(JSON.stringify(app));
return fetch(save_path + 'save', {
method: 'POST',
@@ -1084,7 +1087,7 @@ function save(save_to) {
if (save_path != window.location.pathname) {
alert('Saved to ' + save_path + '.');
} else if (!gFiles['app.js']) {
} else if (!g_files['app.js']) {
window.location.reload();
} else {
reconnect(save_path);
@@ -1096,7 +1099,7 @@ function save(save_to) {
})
.finally(function () {
document.getElementById('save').disabled = false;
Object.values(gFiles).forEach(function (file) {
Object.values(g_files).forEach(function (file) {
file.original = file.doc.doc.toString();
});
updateFiles();
@@ -1109,8 +1112,8 @@ function save(save_to) {
function changeIcon() {
let value = prompt('Enter a new app icon emoji:');
if (value !== undefined) {
gApp.emoji = value || '📦';
document.getElementById('icon').innerHTML = gApp.emoji;
g_app.emoji = value || '📦';
document.getElementById('icon').innerHTML = g_app.emoji;
}
}
@@ -1187,9 +1190,12 @@ function api_postMessage(message) {
function api_error(error) {
if (error) {
if (typeof error == 'string') {
setStatusMessage('⚠️ ' + error, kErrorColor);
setStatusMessage('⚠️ ' + error, k_color_error);
} else {
setStatusMessage('⚠️ ' + error.message + '\n' + error.stack, kErrorColor);
setStatusMessage(
'⚠️ ' + error.message + '\n' + error.stack,
k_color_error
);
}
}
console.log('error', error);
@@ -1306,7 +1312,7 @@ function api_setHash(hash) {
*/
function _receive_websocket_message(message) {
if (message && message.action == 'session') {
setStatusMessage('🟢 Executing...', kStatusColor);
setStatusMessage('🟢 Executing...', k_color_status);
let navigation = document.getElementsByTagName('tf-navigation')[0];
navigation.credentials = message.credentials;
navigation.identities = message.identities;
@@ -1406,7 +1412,7 @@ function setStatusMessage(message, color) {
document.getElementsByTagName('tf-navigation')[0].status = {
message: message,
color: color,
is_error: color == kErrorColor,
is_error: color == k_color_error,
};
}
@@ -1416,11 +1422,11 @@ function setStatusMessage(message, color) {
*/
function send(value) {
try {
if (gSocket && gSocket.readyState == gSocket.OPEN) {
gSocket.send(JSON.stringify(value));
if (g_socket && g_socket.readyState == g_socket.OPEN) {
g_socket.send(JSON.stringify(value));
}
} catch (error) {
setStatusMessage('🤷 Send failed: ' + error.toString(), kErrorColor);
setStatusMessage('🤷 Send failed: ' + error.toString(), k_color_error);
}
}
@@ -1435,7 +1441,7 @@ function hashChange() {
* Make sure the app is connected on window focus, and notify the app.
*/
function focus() {
if (gSocket && gSocket.readyState == gSocket.CLOSED) {
if (g_socket && g_socket.readyState == g_socket.CLOSED) {
connectSocket();
} else {
send({event: 'focus'});
@@ -1446,9 +1452,7 @@ function focus() {
* Notify the app of lost focus.
*/
function blur() {
if (gSocket && gSocket.readyState == gSocket.OPEN) {
send({event: 'blur'});
}
send({event: 'blur'});
}
/**
@@ -1505,8 +1509,8 @@ function message(event) {
* @param path The path to which the WebSocket should be connected.
*/
function reconnect(path) {
let oldSocket = gSocket;
gSocket = null;
let oldSocket = g_socket;
g_socket = null;
if (oldSocket) {
oldSocket.onopen = null;
oldSocket.onclose = null;
@@ -1521,24 +1525,24 @@ function reconnect(path) {
* @param path The path to which to connect.
*/
function connectSocket(path) {
if (!gSocket || gSocket.readyState != gSocket.OPEN) {
if (gSocket) {
gSocket.onopen = null;
gSocket.onclose = null;
gSocket.onmessage = null;
gSocket.close();
if (!g_socket || g_socket.readyState != g_socket.OPEN) {
if (g_socket) {
g_socket.onopen = null;
g_socket.onclose = null;
g_socket.onmessage = null;
g_socket.close();
}
setStatusMessage('⚪ Connecting...', kStatusColor);
gSocket = new WebSocket(
setStatusMessage('⚪ Connecting...', k_color_status);
g_socket = new WebSocket(
(window.location.protocol == 'https:' ? 'wss://' : 'ws://') +
window.location.hostname +
(window.location.port.length ? ':' + window.location.port : '') +
'/app/socket'
);
gSocket.onopen = function () {
setStatusMessage('🟡 Authenticating...', kStatusColor);
g_socket.onopen = function () {
setStatusMessage('🟡 Authenticating...', k_color_status);
let connect_path = path ?? window.location.pathname;
gSocket.send(
g_socket.send(
JSON.stringify({
action: 'hello',
path: connect_path,
@@ -1550,12 +1554,12 @@ function connectSocket(path) {
})
);
};
gSocket.onmessage = function (event) {
g_socket.onmessage = function (event) {
_receive_websocket_message(JSON.parse(event.data));
};
gSocket.onclose = function (event) {
if (gUnloading) {
setStatusMessage('⚪ Closing...', kStatusColor);
g_socket.onclose = function (event) {
if (g_unloading) {
setStatusMessage('⚪ Closing...', k_color_status);
} else {
const k_codes = {
1000: 'Normal closure',
@@ -1576,7 +1580,7 @@ function connectSocket(path) {
};
setStatusMessage(
'🔴 Closed: ' + (k_codes[event.code] || event.code),
kDisconnectColor
k_color_disconnect
);
}
};
@@ -1589,24 +1593,24 @@ function connectSocket(path) {
*/
function openFile(name) {
let newDoc =
name && gFiles[name]
? gFiles[name].doc
name && g_files[name]
? g_files[name].doc
: cm6.EditorState.create({doc: '', extensions: cm6.extensions});
let oldDoc = gEditor.state;
gEditor.setState(newDoc);
let oldDoc = g_editor.state;
g_editor.setState(newDoc);
if (gFiles[gCurrentFile]) {
gFiles[gCurrentFile].doc = oldDoc;
if (g_files[g_current_file]) {
g_files[g_current_file].doc = oldDoc;
if (
!gFiles[gCurrentFile].isNew &&
gFiles[gCurrentFile].doc.doc.toString() == oldDoc.doc.toString()
!g_files[g_current_file].isNew &&
g_files[g_current_file].doc.doc.toString() == oldDoc.doc.toString()
) {
delete gFiles[gCurrentFile].buffer;
delete g_files[g_current_file].buffer;
}
}
gCurrentFile = name;
g_current_file = name;
updateFiles();
gEditor.focus();
g_editor.focus();
}
/**
@@ -1616,19 +1620,19 @@ function updateFiles() {
let files = document.getElementsByTagName('tf-files-pane')[0];
if (files) {
files.files = Object.fromEntries(
Object.keys(gFiles).map((file) => [
Object.keys(g_files).map((file) => [
file,
{
clean:
(file == gCurrentFile
? gEditor.state.doc.toString()
: gFiles[file].doc.doc.toString()) == gFiles[file].original,
(file == g_current_file
? g_editor.state.doc.toString()
: g_files[file].doc.doc.toString()) == g_files[file].original,
},
])
);
files.current = gCurrentFile;
files.current = g_current_file;
}
gEditor.focus();
g_editor.focus();
}
/**
@@ -1636,7 +1640,7 @@ function updateFiles() {
* @param name The file's name.
*/
function makeNewFile(name) {
gFiles[name] = {
g_files[name] = {
doc: cm6.EditorState.create({extensions: cm6.extensions}),
};
openFile(name);
@@ -1647,7 +1651,7 @@ function makeNewFile(name) {
*/
function newFile() {
let name = prompt('Name of new file:', 'file.js');
if (name && !gFiles[name]) {
if (name && !g_files[name]) {
makeNewFile(name);
}
}
@@ -1656,9 +1660,9 @@ function newFile() {
* Prompt to remove a file.
*/
function removeFile() {
if (confirm('Remove ' + gCurrentFile + '?')) {
delete gFiles[gCurrentFile];
openFile(Object.keys(gFiles)[0]);
if (confirm('Remove ' + g_current_file + '?')) {
delete g_files[g_current_file];
openFile(Object.keys(g_files)[0]);
}
}
@@ -1674,13 +1678,13 @@ async function appExport() {
`${name}.json`,
JSON.stringify({
type: 'tildefriends-app',
emoji: gApp.emoji || '📦',
emoji: g_app.emoji || '📦',
})
);
for (let file of Object.keys(gFiles)) {
for (let file of Object.keys(g_files)) {
zip.file(
`${name}/${file}`,
gFiles[file].buffer ?? gFiles[file].doc.doc.toString()
g_files[file].buffer ?? g_files[file].doc.doc.toString()
);
}
let content = await zip.generateAsync({
@@ -1799,9 +1803,9 @@ async function sourcePretty() {
let babel = (await import('/prettier/babel.mjs')).default;
let estree = (await import('/prettier/estree.mjs')).default;
let prettier_html = (await import('/prettier/html.mjs')).default;
let source = gEditor.state.doc.toString();
let source = g_editor.state.doc.toString();
let formatted = await prettier.format(source, {
parser: gCurrentFile?.toLowerCase()?.endsWith('.html') ? 'html' : 'babel',
parser: g_current_file?.toLowerCase()?.endsWith('.html') ? 'html' : 'babel',
plugins: [babel, estree, prettier_html],
trailingComma: 'es5',
useTabs: true,
@@ -1810,10 +1814,10 @@ async function sourcePretty() {
bracketSpacing: false,
});
if (source !== formatted) {
gEditor.dispatch({
g_editor.dispatch({
changes: {
from: 0,
to: gEditor.state.doc.length,
to: g_editor.state.doc.length,
insert: formatted,
},
});
@@ -1858,7 +1862,7 @@ window.addEventListener('load', function () {
window.addEventListener('message', message, false);
window.addEventListener('online', connectSocket);
window.addEventListener('beforeunload', function () {
gUnloading = true;
g_unloading = true;
});
document.getElementById('name').value = window.location.pathname;
document

View File

@@ -7,7 +7,6 @@
/** \cond */
import * as app from './app.js';
import * as http from './http.js';
export {invoke, getProcessBlob};
/** \endcond */
@@ -179,6 +178,7 @@ async function getProcessBlob(blobId, key, options) {
process.task = new Task();
process.packageOwner = options.packageOwner;
process.packageName = options.packageName;
process.url = options?.url;
process.eventHandlers = {};
if (!options?.script || options?.script === 'app.js') {
process.app = new app.App();
@@ -192,54 +192,13 @@ async function getProcessBlob(blobId, key, options) {
});
gProcesses[key] = process;
process.task.onExit = function (exitCode, terminationSignal) {
broadcastEvent('onSessionEnd', [getUser(process, process)]);
process.task = null;
delete gProcesses[key];
};
let imports = {
core: {
broadcast: broadcast.bind(process),
register: function (eventName, handler) {
if (!process.eventHandlers[eventName]) {
process.eventHandlers[eventName] = [];
}
process.eventHandlers[eventName].push(handler);
},
unregister: function (eventName, handler) {
if (process.eventHandlers[eventName]) {
let index = process.eventHandlers[eventName].indexOf(handler);
if (index != -1) {
process.eventHandlers[eventName].splice(index, 1);
}
if (process.eventHandlers[eventName].length == 0) {
delete process.eventHandlers[eventName];
}
}
},
user: getUser(process, process),
users: async function () {
try {
return JSON.parse(await new Database('auth').get('users'));
} catch {
return [];
}
},
permissionsGranted: async function () {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (
user &&
options?.packageOwner &&
options?.packageName &&
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner]
) {
return settings.userPermissions[user][options.packageOwner][
options.packageName
];
}
},
allPermissionsGranted: async function () {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
@@ -253,11 +212,6 @@ async function getProcessBlob(blobId, key, options) {
return settings.userPermissions[user];
}
},
permissionsForUser: async function (user) {
let settings = await loadSettings();
return settings?.permissions?.[user] ?? [];
},
getSockets: getSockets,
permissionTest: async function (permission) {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
@@ -318,11 +272,6 @@ async function getProcessBlob(blobId, key, options) {
throw Error(`Permission denied: ${permission}.`);
}
},
app: {
owner: options?.packageOwner,
name: options?.packageName,
},
url: options?.url,
},
};
process.sendIdentities = async function () {
@@ -331,7 +280,7 @@ async function getProcessBlob(blobId, key, options) {
{
action: 'identities',
},
await ssb.getIdentityInfo(
await ssb_internal.getIdentityInfo(
process?.credentials?.session?.name,
options?.packageOwner,
options?.packageName
@@ -374,7 +323,7 @@ async function getProcessBlob(blobId, key, options) {
options.packageName,
'setActiveIdentity',
[
await ssb.getActiveIdentity(
await imports.ssb.getActiveIdentity(
process.credentials?.session?.name,
options.packageOwner,
options.packageName
@@ -387,19 +336,6 @@ async function getProcessBlob(blobId, key, options) {
}
};
if (process.credentials?.permissions?.administration) {
imports.core.globalSettingsDescriptions = async function () {
let settings = Object.assign({}, defaultGlobalSettings());
for (let [key, value] of Object.entries(await loadSettings())) {
if (settings[key]) {
settings[key].value = value;
}
}
return settings;
};
imports.core.globalSettingsGet = async function (key) {
let settings = await loadSettings();
return settings?.[key];
};
imports.core.globalSettingsSet = async function (key, value) {
await imports.core.permissionTest('set_global_setting');
print('Setting', key, value);
@@ -481,26 +417,6 @@ async function getProcessBlob(blobId, key, options) {
}
};
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
imports.ssb.getActiveIdentity = () =>
ssb.getActiveIdentity(
process.credentials?.session?.name,
options.packageOwner,
options.packageName
);
imports.ssb.getOwnerIdentities = function () {
if (options.packageOwner) {
return ssb.getIdentities(options.packageOwner);
}
};
imports.ssb.getIdentities = function () {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.getIdentities(process.credentials.session.name);
}
};
imports.ssb.getPrivateKey = function (id) {
if (
process.credentials &&
@@ -570,13 +486,6 @@ async function getProcessBlob(blobId, key, options) {
);
}
};
imports.ssb.addEventListener = undefined;
imports.ssb.removeEventListener = undefined;
imports.ssb.getIdentityInfo = undefined;
imports.fetch = async function (url, options) {
let settings = await loadSettings();
return http.fetch(url, options, settings?.fetch_hosts);
};
if (
process.credentials &&
@@ -680,7 +589,6 @@ async function getProcessBlob(blobId, key, options) {
} catch (e) {
printError(e);
}
broadcastEvent('onSessionBegin', [getUser(process, process)]);
if (process.app) {
process.app.send({action: 'ready', version: version()});
await process.sendPermissions();
@@ -703,15 +611,22 @@ async function getProcessBlob(blobId, key, options) {
return process;
}
ssb.addEventListener('message', function () {
/**
* SSB message added callback.
*/
ssb_internal.addEventListener('message', function () {
broadcastEvent('onMessage', [...arguments]);
});
ssb.addEventListener('broadcasts', function () {
ssb_internal.addEventListener('blob', function () {
broadcastEvent('onBlob', [...arguments]);
});
ssb_internal.addEventListener('broadcasts', function () {
broadcastEvent('onBroadcastsChanged', []);
});
ssb.addEventListener('connections', function () {
ssb_internal.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []);
});

View File

@@ -1,121 +0,0 @@
/**
* \file
* \defgroup tfhttp Tilde Friends HTTP Client JS
* Tilde Friends server-side HTTP client.
* @{
*/
/**
* Parse a URL into protocol, host, path, and port parts.
* @param url
* @return An object of the URL parts.
*/
function parseUrl(url) {
// XXX: Hack.
let match = url.match(new RegExp('(\\w+)://([^/:]+)(?::(\\d+))?(.*)'));
return {
protocol: match[1],
host: match[2],
path: match[4],
port: match[3] ? parseInt(match[3]) : match[1] == 'http' ? 80 : 443,
};
}
/**
* Parse an HTTP response into headers and body content.
* @param data The response data, headers and body included.
* @return headers and body data.
*/
function parseResponse(data) {
let firstLine;
let headers = {};
while (true) {
let endLine = data.indexOf('\r\n');
let line = data.substring(0, endLine);
data = data.substring(endLine + 2);
if (!line.length) {
break;
} else if (!firstLine) {
firstLine = line;
} else {
let colon = line.indexOf(':');
headers[line.substring(colon)] = line.substring(colon + 1);
}
}
return {headers: headers, body: data};
}
/**
* Make an HTTP request.
* @param url The URL.
* @param options Request options.
* @param allowed_hosts List of allowed hosts.
* @return A promise resolved with the response headers and body.
*/
export function fetch(url, options, allowed_hosts) {
let parsed = parseUrl(url);
return new Promise(function (resolve, reject) {
if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
}
let socket = new Socket();
let buffer = new Uint8Array(0);
return socket
.connect(parsed.host, parsed.port)
.then(function () {
socket.read(function (data) {
if (data && data.length) {
let newBuffer = new Uint8Array(buffer.length + data.length);
newBuffer.set(buffer, 0);
newBuffer.set(data, buffer.length);
buffer = newBuffer;
} else {
let result = parseHttpResponse(buffer);
if (!result) {
reject(new Exception('Parse failed.'));
}
if (typeof result == 'number') {
if (result == -2) {
reject('Incomplete request.');
} else {
reject('Bad request.');
}
} else if (typeof result == 'object') {
resolve({
body: buffer.slice(result.bytes_parsed),
status: result.status,
message: result.message,
headers: result.headers,
});
} else {
reject(new Exception('Unexpected parse result.'));
}
resolve(parseResponse(utf8Decode(buffer)));
}
});
if (parsed.port == 443) {
return socket.startTls();
}
})
.then(function () {
let body =
typeof options?.body == 'string'
? utf8Encode(options.body)
: options.body || new Uint8Array(0);
let headers = utf8Encode(
`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
);
let fullRequest = new Uint8Array(headers.length + body.length);
fullRequest.set(headers, 0);
fullRequest.set(body, headers.length);
socket.write(fullRequest);
})
.catch(function (error) {
reject(error);
});
});
}
/** @} */

View File

@@ -25,14 +25,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.33";
version = "0.2025.9";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-9D28gmaBTRVyXhY3zZd/W9PsXA1YZt/K69hz41aVP04=";
hash = "sha256-1nhsfhdOO5HIiiTMb+uROB8nDPL/UpOYm52hZ/OpPyk=";
fetchSubmodules = true;
};

File diff suppressed because one or more lines are too long

288
deps/codemirror_src/package-lock.json generated vendored
View File

@@ -19,9 +19,9 @@
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
"integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -31,9 +31,9 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
"integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -56,9 +56,9 @@
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@@ -69,7 +69,7 @@
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
"@lezer/html": "^1.3.12"
}
},
"node_modules/@codemirror/lang-javascript": {
@@ -112,9 +112,9 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz",
"integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -155,9 +155,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -206,9 +206,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -217,9 +217,9 @@
}
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
"license": "MIT"
},
"node_modules/@lezer/css": {
@@ -234,18 +234,18 @@
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.2.tgz",
"integrity": "sha512-z8TQwaBXXQIvG6i2g3e9cgMwUUXu9Ib7jo2qRRggdhwKpM56Dw3PM3wmexn+EGaaOZ7az0K7sjc3/gcGW7sz7A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz",
"integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -254,9 +254,9 @@
}
},
"node_modules/@lezer/javascript": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -338,9 +338,9 @@
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
"integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
@@ -360,9 +360,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.4.tgz",
"integrity": "sha512-B2wfzCJ+ps/OBzRjeds7DlJumCU3rXMxJJS1vzURyj7+KBHGONm7c9q1TfdBl4vCuNMkDvARn3PBl2wZzuR5mw==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"cpu": [
"arm"
],
@@ -373,9 +373,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.4.tgz",
"integrity": "sha512-FGJYXvYdn8Bs6lAlBZYT5n+4x0ciEp4cmttsvKAZc/c8/JiPaQK8u0c/86vKX8lA7OY/+37lIQSe0YoAImvBAA==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"cpu": [
"arm64"
],
@@ -386,9 +386,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.4.tgz",
"integrity": "sha512-/9qwE/BM7ATw/W/OFEMTm3dmywbJyLQb4f4v5nmOjgYxPIGpw7HaxRi6LnD4Pjn/q7k55FGeHe1/OD02w63apA==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"cpu": [
"arm64"
],
@@ -399,9 +399,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.4.tgz",
"integrity": "sha512-QkWfNbeRuzFnv2d0aPlrzcA3Ebq2mE8kX/5Pl7VdRShbPBjSnom7dbT8E3Jmhxo2RL784hyqGvR5KHavCJQciw==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"cpu": [
"x64"
],
@@ -412,9 +412,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.4.tgz",
"integrity": "sha512-+ToyOMYnSfV8D+ckxO6NthPln/PDNp1P6INcNypfZ7muLmEvPKXqduUiD8DlJpMMT8LxHcE5W0dK9kXfJke9Zw==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"cpu": [
"arm64"
],
@@ -425,9 +425,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.4.tgz",
"integrity": "sha512-cGT6ey/W+sje6zywbLiqmkfkO210FgRz7tepWAzzEVgQU8Hn91JJmQWNqs55IuglG8sJdzk7XfNgmGRtcYlo1w==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"cpu": [
"x64"
],
@@ -438,9 +438,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.4.tgz",
"integrity": "sha512-9fhTJyOb275w5RofPSl8lpr4jFowd+H4oQKJ9XTYzD1JWgxdZKE8bA6d4npuiMemkecQOcigX01FNZNCYnQBdA==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"cpu": [
"arm"
],
@@ -451,9 +451,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.4.tgz",
"integrity": "sha512-+6kCIM5Zjvz2HwPl/udgVs07tPMIp1VU2Y0c72ezjOvSvEfAIWsUgpcSDvnC7g9NrjYR6X9bZT92mZZ90TfvXw==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"cpu": [
"arm"
],
@@ -464,9 +464,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.4.tgz",
"integrity": "sha512-SWuXdnsayCZL4lXoo6jn0yyAj7TTjWE4NwDVt9s7cmu6poMhtiras5c8h6Ih6Y0Zk6Z+8t/mLumvpdSPTWub2Q==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"cpu": [
"arm64"
],
@@ -477,9 +477,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.4.tgz",
"integrity": "sha512-vDknMDqtMhrrroa5kyX6tuC0aRZZlQ+ipDfbXd2YGz5HeV2t8HOl/FDAd2ynhs7Ki5VooWiiZcCtxiZ4IjqZwQ==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"cpu": [
"arm64"
],
@@ -489,10 +489,10 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.4.tgz",
"integrity": "sha512-mCBkjRZWhvjtl/x+Bd4fQkWZT8canStKDxGrHlBiTnZmJnWygGcvBylzLVCZXka4dco5ymkWhZlLwKCGFF4ivw==",
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"cpu": [
"loong64"
],
@@ -503,9 +503,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.4.tgz",
"integrity": "sha512-YMdz2phOTFF+Z66dQfGf0gmeDSi5DJzY5bpZyeg9CPBkV9QDzJ1yFRlmi/j7WWRf3hYIWrOaJj5jsfwgc8GTHQ==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"cpu": [
"ppc64"
],
@@ -516,9 +516,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.4.tgz",
"integrity": "sha512-r0WKLSfFAK8ucG024v2yiLSJMedoWvk8yWqfNICX28NHDGeu3F/wBf8KG6mclghx4FsLePxJr/9N8rIj1PtCnw==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"cpu": [
"riscv64"
],
@@ -529,9 +529,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.4.tgz",
"integrity": "sha512-IaizpPP2UQU3MNyPH1u0Xxbm73D+4OupL0bjo4Hm0496e2wg3zuvoAIhubkD1NGy9fXILEExPQy87mweujEatA==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"cpu": [
"riscv64"
],
@@ -542,9 +542,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.4.tgz",
"integrity": "sha512-aCM29orANR0a8wk896p6UEgIfupReupnmISz6SUwMIwTGaTI8MuKdE0OD2LvEg8ondDyZdMvnaN3bW4nFbATPA==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"cpu": [
"s390x"
],
@@ -555,9 +555,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.4.tgz",
"integrity": "sha512-0Xj1vZE3cbr/wda8d/m+UeuSL+TDpuozzdD4QaSzu/xSOMK0Su5RhIkF7KVHFQsobemUNHPLEcYllL7ZTCP/Cg==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"cpu": [
"x64"
],
@@ -568,9 +568,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.4.tgz",
"integrity": "sha512-kM/orjpolfA5yxsx84kI6bnK47AAZuWxglGKcNmokw2yy9i5eHY5UAjcX45jemTJnfHAWo3/hOoRqEeeTdL5hw==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"cpu": [
"x64"
],
@@ -580,10 +580,23 @@
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.4.tgz",
"integrity": "sha512-cNLH4psMEsWKILW0isbpQA2OvjXLbKvnkcJFmqAptPQbtLrobiapBJVj6RoIvg6UXVp5w0wnIfd/Q56cNpF+Ew==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"cpu": [
"arm64"
],
@@ -594,9 +607,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.4.tgz",
"integrity": "sha512-OiEa5lRhiANpv4SfwYVgQ3opYWi/QmPDC5ve21m8G9pf6ZO+aX1g2EEF1/IFaM1xPSP7mK0msTRXlPs6mIagkg==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"cpu": [
"ia32"
],
@@ -606,10 +619,23 @@
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.4.tgz",
"integrity": "sha512-IKL9mewGZ5UuuX4NQlwOmxPyqielvkAPUS2s1cl6yWjjQvyN3h5JTdVFGD5Jr5xMjRC8setOfGQDVgX8V+dkjg==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"cpu": [
"x64"
],
@@ -779,12 +805,12 @@
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -799,9 +825,9 @@
}
},
"node_modules/rollup": {
"version": "4.46.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.4.tgz",
"integrity": "sha512-YbxoxvoqNg9zAmw4+vzh1FkGAiZRK+LhnSrbSrSXMdZYsRPDWoshcSd/pldKRO6lWzv/e9TiJAVQyirYIeSIPQ==",
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -814,26 +840,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.46.4",
"@rollup/rollup-android-arm64": "4.46.4",
"@rollup/rollup-darwin-arm64": "4.46.4",
"@rollup/rollup-darwin-x64": "4.46.4",
"@rollup/rollup-freebsd-arm64": "4.46.4",
"@rollup/rollup-freebsd-x64": "4.46.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.4",
"@rollup/rollup-linux-arm-musleabihf": "4.46.4",
"@rollup/rollup-linux-arm64-gnu": "4.46.4",
"@rollup/rollup-linux-arm64-musl": "4.46.4",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.4",
"@rollup/rollup-linux-ppc64-gnu": "4.46.4",
"@rollup/rollup-linux-riscv64-gnu": "4.46.4",
"@rollup/rollup-linux-riscv64-musl": "4.46.4",
"@rollup/rollup-linux-s390x-gnu": "4.46.4",
"@rollup/rollup-linux-x64-gnu": "4.46.4",
"@rollup/rollup-linux-x64-musl": "4.46.4",
"@rollup/rollup-win32-arm64-msvc": "4.46.4",
"@rollup/rollup-win32-ia32-msvc": "4.46.4",
"@rollup/rollup-win32-x64-msvc": "4.46.4",
"@rollup/rollup-android-arm-eabi": "4.52.5",
"@rollup/rollup-android-arm64": "4.52.5",
"@rollup/rollup-darwin-arm64": "4.52.5",
"@rollup/rollup-darwin-x64": "4.52.5",
"@rollup/rollup-freebsd-arm64": "4.52.5",
"@rollup/rollup-freebsd-x64": "4.52.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
"@rollup/rollup-linux-arm64-musl": "4.52.5",
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
"@rollup/rollup-linux-x64-gnu": "4.52.5",
"@rollup/rollup-linux-x64-musl": "4.52.5",
"@rollup/rollup-openharmony-arm64": "4.52.5",
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
"@rollup/rollup-win32-x64-gnu": "4.52.5",
"@rollup/rollup-win32-x64-msvc": "4.52.5",
"fsevents": "~2.3.2"
}
},
@@ -897,9 +925,9 @@
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/supports-preserve-symlinks-flag": {
@@ -915,14 +943,14 @@
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},

1
deps/openssl_src vendored

Submodule deps/openssl_src deleted from 0893a62353

2
deps/quickjs vendored

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head>
<body>
<script src="speedscope-HCR63FMT.js"></script>
<script src="speedscope-432XE7GS.js"></script>

View File

@@ -1,3 +1,3 @@
speedscope@1.23.1
Mon Aug 11 11:43:09 PDT 2025
0cec0f82c334aed6cf19d43cabeadcda0d95e0fc
speedscope@1.24.0
Mon Oct 20 18:11:29 PDT 2025
fc76932551754a442cd5c4f0afdba28032d14d8a

File diff suppressed because one or more lines are too long

View File

@@ -41,11 +41,9 @@ options:
ssb_port (default: 8008): Port on which to listen for SSB secure handshake connections.
http_local_only (default: false): Whether to bind http(s) to the loopback address. Otherwise any.
http_port (default: 12345): Port on which to listen for HTTP connections.
https_port (default: 0): Port on which to listen for secure HTTP connections.
out_http_port_file (default: ""): File to which to write bound HTTP port.
blob_fetch_age_seconds (default: -1): Only blobs mentioned more recently than this age will be automatically fetched.
blob_expire_age_seconds (default: -1): Blobs older than this will be automatically deleted.
fetch_hosts (default: ""): Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.
http_redirect (default: ""): If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "http://example.com")
index (default: "/~core/intro/"): Default path.
index_map (default: ""): Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1753749649,
"narHash": "sha256-+jkEZxs7bfOKfBIk430K+tK9IvXlwzqQQnppC2ZKFj4=",
"lastModified": 1758589230,
"narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a",
"rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0",
"type": "github"
},
"original": {

View File

@@ -0,0 +1,8 @@
* Fixed multiple issues with blob replication.
* Fixed some link encoding issues.
* Fixed some context menus being cut off.
* Minor Android fixes.
* Updates:
* CodeMirror
* OpenSSL 3.5.3
* QuickJS 2025-09-13

View File

@@ -0,0 +1,8 @@
* Remove OpenSSL.
* Exclude messages for subscribed channels from the general feed.
* Remove the query tab in favor of searching for "sql:SELECT ...".
* Faster load times.
* Fix contact groups expanding/collapsing.
* Give channel subscribe/unsubscribe similar grouping treatment to follows/blocks.
* Slightly improved following/blocking message display.
* Updates: CodeMirror, libbacktrace, and speedscope 1.24.0.

View File

@@ -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="42"
android:versionName="0.2025.8">
android:versionCode="44"
android:versionName="0.2025.10-wip">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application

View File

@@ -19,7 +19,6 @@ import android.os.RemoteException;
import android.os.StrictMode;
import android.text.InputType;
import android.util.Base64;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -44,6 +43,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.OutputStream;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class TildeFriendsActivity extends Activity {
@@ -53,29 +53,42 @@ public class TildeFriendsActivity extends Activity {
String port_file_path;
Thread create_thread;
Thread server_thread;
Thread log_thread;
ServiceConnection service_connection;
FileObserver observer;
LinkedBlockingQueue<String> log_queue = new LinkedBlockingQueue<String>();
private ValueCallback<Uri[]> upload_message;
private final static int FILECHOOSER_RESULT = 1;
private float touch_down_y;
private boolean ready = false;
private boolean loaded = false;
private boolean shutting_down = false;
static {
Log.w("tildefriends", "Calling system.loadLibrary().");
log("Calling system.loadLibrary().");
System.loadLibrary("tildefriends");
Log.w("tildefriends", "system.loadLibrary() completed.");
log("system.loadLibrary() completed.");
}
public static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path, ConnectivityManager connectivity_manager);
public static native int tf_sandbox_main(int pipe_fd);
public static void log(String message) {
if (s_activity != null && s_activity.log_queue != null && message != null) {
try {
s_activity.log_queue.put(message);
} catch (InterruptedException e) {
android.util.Log.w("tildefriends", message);
}
}
}
private void createThread() {
web_view = (TildeFriendsWebView)findViewById(R.id.web);
Log.w("tildefriends", String.format("getFilesDir() is %s", getFilesDir().toString()));
Log.w("tildefriends", String.format("getPackageResourcePath() is %s", getPackageResourcePath().toString()));
Log.w("tildefriends", String.format("nativeLibraryDir is %s", getApplicationInfo().nativeLibraryDir));
log(String.format("getFilesDir() is %s", getFilesDir().toString()));
log(String.format("getPackageResourcePath() is %s", getPackageResourcePath().toString()));
log(String.format("nativeLibraryDir is %s", getApplicationInfo().nativeLibraryDir));
port_file_path = getFilesDir().toString() + "/port.txt";
new File(port_file_path).delete();
@@ -86,17 +99,17 @@ public class TildeFriendsActivity extends Activity {
server_thread = new Thread(new Runnable() {
@Override
public void run() {
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
log("Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
Log.w("tildefriends", "Calling tf_server_main.");
log("Calling tf_server_main.");
int result = tf_server_main(
getFilesDir().toString(),
getPackageResourcePath().toString(),
port_file_path,
(ConnectivityManager)getApplicationContext().getSystemService(CONNECTIVITY_SERVICE));
Log.w("tildefriends", "tf_server_main returned " + result + ".");
log("tf_server_main returned " + result + ".");
}
});
server_thread.start();
@@ -110,17 +123,17 @@ public class TildeFriendsActivity extends Activity {
web_view.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String content_disposition, String mime_type, long content_length) {
Log.w("tildefriends", "Let's download: " + url + " (" + content_disposition + ")");
log("Let's download: " + url + " (" + content_disposition + ")");
String file_name = URLUtil.guessFileName(url, content_disposition, mime_type);
if (url.startsWith("data:") && url.indexOf(',') != -1) {
String b64 = url.substring(url.indexOf(',') + 1);
byte[] data = Base64.decode(b64, Base64.DEFAULT);
Log.w("tildefriends", "Downloaded " + data.length + " bytes.");
log("Downloaded " + data.length + " bytes.");
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
try (OutputStream stream = new FileOutputStream(new File(path, file_name))) {
stream.write(data);
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e.toString());
log("IOException: " + e.toString());
}
Toast.makeText(getApplicationContext(), "Downloaded File", Toast.LENGTH_LONG).show();
} else {
@@ -228,7 +241,7 @@ public class TildeFriendsActivity extends Activity {
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d("tildefriends", consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
log(consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
return true;
}
});
@@ -259,10 +272,27 @@ public class TildeFriendsActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
s_activity = this;
super.onCreate(savedInstanceState);
log_thread = new Thread(new Runnable() {
@Override
public void run() {
while (!s_activity.shutting_down) {
try {
String message = log_queue.take();
if (message != null) {
android.util.Log.w("tildefriends", message);
} else {
break;
}
} catch (InterruptedException e) {
}
}
}
});
log_thread.start();
StrictMode.setThreadPolicy(
new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDialog()
//.penaltyDialog()
.penaltyLog()
.build());
StrictMode.setVmPolicy(
@@ -305,8 +335,17 @@ public class TildeFriendsActivity extends Activity {
@Override
protected void onDestroy()
{
super.onDestroy();
try {
shutting_down = true;
if (log_queue != null) {
log_queue.put("Goodbye.");
}
log_thread.join();
} catch (InterruptedException e) {
}
log_thread = null;
s_activity = null;
super.onDestroy();
}
@Override
@@ -399,32 +438,32 @@ public class TildeFriendsActivity extends Activity {
}
public static void start_sandbox(int pipe_fd) {
Log.w("tildefriends", "starting service with fd: " + pipe_fd);
log("starting service with fd: " + pipe_fd);
Intent intent = new Intent(s_activity, TildeFriendsSandboxService.class);
s_activity.service_connection = new ServiceConnection() {
@Override
public void onBindingDied(ComponentName name) {
Log.w("tildefriends", "onBindingDied");
log("onBindingDied");
}
@Override
public void onNullBinding(ComponentName name) {
Log.w("tildefriends", "onNullBinding");
log("onNullBinding");
}
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
Log.w("tildefriends", "onServiceConnected");
log("onServiceConnected");
Parcel data = Parcel.obtain();
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(pipe_fd)) {
data.writeParcelable(pfd, 0);
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e);
log("IOException: " + e);
}
try {
binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException e) {
Log.w("tildefriends", "RemoteException");
log("RemoteException");
} finally {
data.recycle();
}
@@ -432,14 +471,14 @@ public class TildeFriendsActivity extends Activity {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.w("tildefriends", "onServiceDisconnected");
log("onServiceDisconnected");
}
};
s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND);
}
public static void stop_sandbox() {
Log.w("tildefriends", "stop_sandbox");
log("stop_sandbox");
if (s_activity.service_connection != null) {
s_activity.unbindService(s_activity.service_connection);
s_activity.service_connection = null;

View File

@@ -6,7 +6,6 @@ import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.util.Log;
public class TildeFriendsSandboxService extends Service {
public static final int START_CALL = IBinder.FIRST_CALL_TRANSACTION;
@@ -14,12 +13,12 @@ public class TildeFriendsSandboxService extends Service {
Thread thread;
public int onStartCommand(Intent intent, int flags, int start_id) {
Log.w("tildefriends", "TildeFriendsSandboxService: onStartCommand");
TildeFriendsActivity.log("TildeFriendsSandboxService: onStartCommand");
return super.onStartCommand(intent, flags, start_id);
}
public void onDestroy() {
Log.w("tildefriends", "TildeFriendsSandboxService: onDestroy");
TildeFriendsActivity.log("TildeFriendsSandboxService: onDestroy");
super.onDestroy();
}
@@ -27,9 +26,9 @@ public class TildeFriendsSandboxService extends Service {
thread = new Thread(new Runnable() {
@Override
public void run() {
Log.w("tildefriends", "Calling tf_sandbox_main.");
TildeFriendsActivity.log("Calling tf_sandbox_main.");
int result = TildeFriendsActivity.tf_sandbox_main(pipe_fd);
Log.w("tildefriends", "tf_sandbox_main returned " + result + ".");
TildeFriendsActivity.log("tf_sandbox_main returned " + result + ".");
}
});
thread.start();
@@ -43,7 +42,7 @@ public class TildeFriendsSandboxService extends Service {
if (code == START_CALL) {
ParcelFileDescriptor pfd = read_pfd(data);
if (pfd != null) {
Log.w("tildefriends", "fd is " + pfd.getFd());
TildeFriendsActivity.log("fd is " + pfd.getFd());
start_thread(pfd.detachFd());
try {
pfd.close();

View File

@@ -74,7 +74,7 @@ static void _tf_api_core_apps_after_work(tf_ssb_t* ssb, int status, void* user_d
JSValue result = JS_NewObject(context);
for (int i = 0; i < work->count; i++)
{
JS_SetPropertyStr(context, result, work->apps[i].app, JS_NewString(context, work->apps[i].path));
JS_SetPropertyStr(context, result, work->apps[i].app, JS_NewString(context, work->apps[i].path ? work->apps[i].path : ""));
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
@@ -147,12 +147,698 @@ static JSValue _tf_api_core_apps(JSContext* context, JSValueConst this_val, int
return result;
}
typedef struct _users_t
{
const char* users;
JSContext* context;
JSValue promise[2];
} users_t;
static void _tf_api_core_users_work(tf_ssb_t* ssb, void* user_data)
{
users_t* work = user_data;
work->users = tf_ssb_db_get_property(ssb, "auth", "users");
}
static void _tf_api_core_users_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
users_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_UNDEFINED;
if (work->users)
{
result = JS_ParseJSON(context, work->users, strlen(work->users), NULL);
tf_free((void*)work->users);
}
if (JS_IsUndefined(result))
{
result = JS_NewArray(context);
}
JSValue error = JS_Call(context, JS_IsArray(context, result) ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_api_core_users(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
users_t* work = tf_malloc(sizeof(users_t));
*work = (users_t) {
.context = context,
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_api_core_users_work, _tf_api_core_users_after_work, work);
return result;
}
static JSValue _tf_api_core_register(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue event_name = argv[0];
JSValue handler = argv[1];
JSValue process = data[0];
JSValue event_handlers = JS_GetPropertyStr(context, process, "eventHandlers");
JSAtom atom = JS_ValueToAtom(context, event_name);
JSValue array = JS_GetProperty(context, event_handlers, atom);
if (!JS_IsArray(context, array))
{
JS_FreeValue(context, array);
array = JS_NewArray(context);
JS_SetProperty(context, event_handlers, atom, JS_DupValue(context, array));
}
JS_SetPropertyUint32(context, array, tf_util_get_length(context, array), JS_DupValue(context, handler));
JS_FreeValue(context, array);
JS_FreeAtom(context, atom);
JS_FreeValue(context, event_handlers);
return JS_UNDEFINED;
}
static JSValue _tf_api_core_unregister(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue event_name = argv[0];
JSValue handler = argv[1];
JSValue process = data[0];
JSValue event_handlers = JS_GetPropertyStr(context, process, "eventHandlers");
JSAtom atom = JS_ValueToAtom(context, event_name);
JSValue array = JS_GetProperty(context, event_handlers, atom);
if (JS_IsArray(context, array))
{
JSValue index_of = JS_GetPropertyStr(context, array, "indexOf");
JSValue index = JS_Call(context, index_of, array, 1, &handler);
int int_index = -1;
JS_ToInt32(context, &int_index, index);
if (int_index != -1)
{
JSValue splice = JS_GetPropertyStr(context, array, "splice");
JSValue splice_args[] = {
index,
JS_NewInt32(context, 1),
};
JSValue result = JS_Call(context, splice, array, 2, splice_args);
JS_FreeValue(context, result);
JS_FreeValue(context, splice);
}
JS_FreeValue(context, index);
JS_FreeValue(context, index_of);
if (tf_util_get_length(context, array) == 0)
{
JS_DeleteProperty(context, event_handlers, atom, 0);
}
}
JS_FreeValue(context, array);
JS_FreeAtom(context, atom);
JS_FreeValue(context, event_handlers);
return JS_UNDEFINED;
}
typedef struct _permissions_for_user_t
{
const char* user;
const char* settings;
JSContext* context;
JSValue promise[2];
} permissions_for_user_t;
static void _tf_api_core_permissions_for_user_work(tf_ssb_t* ssb, void* user_data)
{
permissions_for_user_t* work = user_data;
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
static void _tf_api_core_permissions_for_user_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
permissions_for_user_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_UNDEFINED;
if (work->settings)
{
JSValue json = JS_ParseJSON(context, work->settings, strlen(work->settings), NULL);
if (JS_IsObject(json))
{
JSValue permissions = JS_GetPropertyStr(context, json, "permissions");
if (JS_IsObject(permissions))
{
result = JS_GetPropertyStr(context, permissions, work->user);
}
JS_FreeValue(context, permissions);
}
JS_FreeValue(context, json);
tf_free((void*)work->settings);
}
if (JS_IsUndefined(result))
{
result = JS_NewArray(context);
}
JSValue error = JS_Call(context, JS_IsArray(context, result) ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->user);
tf_free(work);
}
static JSValue _tf_api_core_permissionsForUser(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
permissions_for_user_t* work = tf_malloc(sizeof(permissions_for_user_t));
*work = (permissions_for_user_t) {
.context = context,
.user = JS_ToCString(context, argv[0]),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_api_core_permissions_for_user_work, _tf_api_core_permissions_for_user_after_work, work);
return result;
}
typedef struct _permissions_granted_t
{
JSContext* context;
const char* user;
const char* package_owner;
const char* package_name;
const char* settings;
JSValue promise[2];
} permissions_granted_t;
static void _tf_api_core_permissions_granted_work(tf_ssb_t* ssb, void* user_data)
{
permissions_granted_t* work = user_data;
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
static void _tf_api_core_permissions_granted_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
permissions_granted_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_UNDEFINED;
if (work->settings)
{
JSValue json = JS_ParseJSON(context, work->settings, strlen(work->settings), NULL);
if (JS_IsObject(json) &&
work->user &&
work->package_owner &&
work->package_name)
{
JSValue user_permissions = JS_GetPropertyStr(context, json, "userPermissions");
if (JS_IsObject(user_permissions))
{
JSValue user = JS_GetPropertyStr(context, user_permissions, work->user);
if (JS_IsObject(user))
{
JSValue package_owner = JS_GetPropertyStr(context, user, work->package_owner);
if (JS_IsObject(package_owner))
{
result = JS_GetPropertyStr(context, package_owner, work->package_name);
}
JS_FreeValue(context, package_owner);
}
JS_FreeValue(context, user);
}
JS_FreeValue(context, user_permissions);
}
JS_FreeValue(context, json);
tf_free((void*)work->settings);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((void*)work->user);
tf_free((void*)work->package_owner);
tf_free((void*)work->package_name);
tf_free(work);
}
static const char* _tf_ssb_get_process_credentials_session_name(JSContext* context, JSValue process)
{
JSValue credentials = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "credentials") : JS_UNDEFINED;
JSValue session = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "session") : JS_UNDEFINED;
JSValue name_value = JS_IsObject(session) ? JS_GetPropertyStr(context, session, "name") : JS_UNDEFINED;
const char* name = JS_IsString(name_value) ? JS_ToCString(context, name_value) : NULL;
const char* result = tf_strdup(name);
JS_FreeCString(context, name);
JS_FreeValue(context, name_value);
JS_FreeValue(context, session);
JS_FreeValue(context, credentials);
return result;
}
static JSValue _tf_api_core_permissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSValue process = data[0];
JSValue package_owner_value = JS_GetPropertyStr(context, process, "packageOwner");
JSValue package_name_value = JS_GetPropertyStr(context, process, "packageName");
const char* package_owner = JS_ToCString(context, package_owner_value);
const char* package_name = JS_ToCString(context, package_name_value);
permissions_granted_t* work = tf_malloc(sizeof(permissions_granted_t));
*work = (permissions_granted_t) {
.context = context,
.user = _tf_ssb_get_process_credentials_session_name(context, process),
.package_owner = tf_strdup(package_owner),
.package_name = tf_strdup(package_name),
};
JS_FreeCString(context, package_owner);
JS_FreeCString(context, package_name);
JS_FreeValue(context, package_owner_value);
JS_FreeValue(context, package_name_value);
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_api_core_permissions_granted_work, _tf_api_core_permissions_granted_after_work, work);
return result;
}
typedef struct _active_identity_work_t
{
JSContext* context;
const char* name;
const char* package_owner;
const char* package_name;
char identity[k_id_base64_len];
int result;
JSValue promise[2];
} active_identity_work_t;
static void _tf_ssb_getActiveIdentity_visit(const char* identity, void* user_data)
{
active_identity_work_t* request = user_data;
if (!*request->identity)
{
snprintf(request->identity, sizeof(request->identity), "@%s", identity);
}
}
static void _tf_ssb_getActiveIdentity_work(tf_ssb_t* ssb, void* user_data)
{
active_identity_work_t* request = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_identity_get_active(db, request->name, request->package_owner, request->package_name, request->identity, sizeof(request->identity));
tf_ssb_release_db_reader(ssb, db);
if (!*request->identity)
{
tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request);
}
if (!*request->identity && tf_ssb_db_user_has_permission(ssb, NULL, request->name, "administration"))
{
tf_ssb_whoami(ssb, request->identity, sizeof(request->identity));
}
}
static void _tf_ssb_getActiveIdentity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
active_identity_work_t* request = user_data;
JSContext* context = request->context;
if (request->result == 0)
{
JSValue identity = JS_NewString(context, request->identity);
JSValue error = JS_Call(context, request->promise[0], JS_UNDEFINED, 1, &identity);
JS_FreeValue(context, identity);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
}
else
{
JSValue error = JS_Call(context, request->promise[1], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
}
JS_FreeValue(context, request->promise[0]);
JS_FreeValue(context, request->promise[1]);
tf_free((void*)request->name);
tf_free((void*)request->package_owner);
tf_free((void*)request->package_name);
tf_free(request);
}
static JSValue _tf_ssb_getActiveIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSValue process = data[0];
JSValue package_owner_value = JS_GetPropertyStr(context, process, "packageOwner");
JSValue package_name_value = JS_GetPropertyStr(context, process, "packageName");
const char* name = _tf_ssb_get_process_credentials_session_name(context, process);
const char* package_owner = JS_ToCString(context, package_owner_value);
const char* package_name = JS_ToCString(context, package_name_value);
active_identity_work_t* work = tf_malloc(sizeof(active_identity_work_t));
*work = (active_identity_work_t) {
.context = context,
.name = tf_strdup(name),
.package_owner = tf_strdup(package_owner),
.package_name = tf_strdup(package_name),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_free((void*)name);
JS_FreeCString(context, package_owner);
JS_FreeCString(context, package_name);
JS_FreeValue(context, package_owner_value);
JS_FreeValue(context, package_name_value);
tf_ssb_run_work(ssb, _tf_ssb_getActiveIdentity_work, _tf_ssb_getActiveIdentity_after_work, work);
return result;
}
typedef struct _identities_visit_t
{
JSContext* context;
JSValue promise[2];
const char** identities;
int count;
char user[];
} identities_visit_t;
static void _tf_ssb_getIdentities_visit(const char* identity, void* user_data)
{
identities_visit_t* work = user_data;
work->identities = tf_resize_vec(work->identities, (work->count + 1) * sizeof(const char*));
char id[k_id_base64_len];
snprintf(id, sizeof(id), "@%s", identity);
work->identities[work->count++] = tf_strdup(id);
}
static void _tf_ssb_get_all_identities_work(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_db_identity_visit_all(ssb, _tf_ssb_getIdentities_visit, user_data);
}
static void _tf_ssb_get_identities_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
identities_visit_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->count; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->identities[i]));
tf_free((void*)work->identities[i]);
}
tf_free(work->identities);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_getAllIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (ssb)
{
identities_visit_t* work = tf_malloc(sizeof(identities_visit_t));
*work = (identities_visit_t) {
.context = context,
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_all_identities_work, _tf_ssb_get_identities_after_work, work);
}
return result;
}
static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data)
{
identities_visit_t* work = user_data;
if (tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{
char id[k_id_base64_len] = "";
if (tf_ssb_whoami(ssb, id, sizeof(id)))
{
_tf_ssb_getIdentities_visit(*id == '@' ? id + 1 : id, work);
}
}
tf_ssb_db_identity_visit(ssb, work->user, _tf_ssb_getIdentities_visit, user_data);
}
static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSValue process = data[0];
if (ssb)
{
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
if (user)
{
size_t user_length = user ? strlen(user) : 0;
identities_visit_t* work = tf_malloc(sizeof(identities_visit_t) + user_length + 1);
*work = (identities_visit_t) {
.context = context,
};
memcpy(work->user, user, user_length + 1);
tf_free((void*)user);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
}
}
return result;
}
static JSValue _tf_ssb_getOwnerIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSValue process = data[0];
if (ssb)
{
JSValue value = JS_GetPropertyStr(context, process, "packageOwner");
const char* user = JS_ToCString(context, value);
size_t user_length = user ? strlen(user) : 0;
identities_visit_t* work = tf_malloc(sizeof(identities_visit_t) + user_length + 1);
*work = (identities_visit_t) {
.context = context,
};
memcpy(work->user, user, user_length + 1);
JS_FreeCString(context, user);
JS_FreeValue(context, value);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
}
return result;
}
typedef struct _settings_descriptions_get_t
{
const char* settings;
JSContext* context;
JSValue promise[2];
} settings_descriptions_get_t;
static void _tf_ssb_get_settings_descriptions_work(tf_ssb_t* ssb, void* user_data)
{
settings_descriptions_get_t* work = user_data;
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
static void _tf_ssb_get_settings_descriptions_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
settings_descriptions_get_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_NewObject(context);
JSValue settings = JS_ParseJSON(context, work->settings ? work->settings : "", work->settings ? strlen(work->settings) : 0, NULL);
const char* name;
const char* type;
tf_setting_kind_t kind;
const char* description;
for (int i = 0; tf_util_get_global_setting_by_index(i, &name, &type, &kind, &description); i++)
{
JSValue entry = JS_NewObject(context);
JS_SetPropertyStr(context, entry, "type", JS_NewString(context, type));
JS_SetPropertyStr(context, entry, "description", JS_NewString(context, description));
switch (kind)
{
case k_kind_unknown:
break;
case k_kind_bool:
JS_SetPropertyStr(context, entry, "default_value", JS_NewBool(context, tf_util_get_default_global_setting_bool(name)));
break;
case k_kind_int:
JS_SetPropertyStr(context, entry, "default_value", JS_NewInt32(context, tf_util_get_default_global_setting_int(name)));
break;
case k_kind_string:
JS_SetPropertyStr(context, entry, "default_value", JS_NewString(context, tf_util_get_default_global_setting_string(name)));
break;
}
if (JS_IsObject(settings))
{
JS_SetPropertyStr(context, entry, "value", JS_GetPropertyStr(context, settings, name));
}
JS_SetPropertyStr(context, result, name, entry);
}
JS_FreeValue(context, settings);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((void*)work->settings);
tf_free(work);
}
static JSValue _tf_ssb_globalSettingsDescriptions(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
settings_descriptions_get_t* work = tf_malloc(sizeof(settings_descriptions_get_t));
*work = (settings_descriptions_get_t) { .context = context };
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_settings_descriptions_work, _tf_ssb_get_settings_descriptions_after_work, work);
return result;
}
typedef struct _settings_get_t
{
const char* key;
tf_setting_kind_t kind;
void* value;
JSContext* context;
JSValue promise[2];
} settings_get_t;
static void _tf_ssb_settings_get_work(tf_ssb_t* ssb, void* user_data)
{
settings_get_t* work = user_data;
work->kind = tf_util_get_global_setting_kind(work->key);
switch (work->kind)
{
case k_kind_unknown:
break;
case k_kind_bool:
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool value = false;
tf_ssb_db_get_global_setting_bool(db, work->key, &value);
work->value = (void*)(intptr_t)value;
tf_ssb_release_db_reader(ssb, db);
}
break;
case k_kind_int:
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
int64_t value = 0;
tf_ssb_db_get_global_setting_int64(db, work->key, &value);
work->value = (void*)(intptr_t)value;
tf_ssb_release_db_reader(ssb, db);
}
break;
case k_kind_string:
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
work->value = (void*)tf_ssb_db_get_global_setting_string_alloc(db, work->key);
tf_ssb_release_db_reader(ssb, db);
}
break;
}
}
static void _tf_ssb_settings_get_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
settings_get_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
switch (work->kind)
{
case k_kind_unknown:
break;
case k_kind_bool:
result = work->value ? JS_TRUE : JS_FALSE;
break;
case k_kind_int:
result = JS_NewInt64(context, (int64_t)(intptr_t)work->value);
break;
case k_kind_string:
result = JS_NewString(context, (const char*)work->value);
tf_free(work->value);
break;
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->key);
tf_free(work);
}
static JSValue _tf_ssb_globalSettingsGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
settings_get_t* work = tf_malloc(sizeof(settings_get_t));
*work = (settings_get_t) { .context = context, .key = JS_ToCString(context, argv[0]) };
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_settings_get_work, _tf_ssb_settings_get_after_work, work);
return result;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
JSValue process = argv[1];
JSValue core = JS_GetPropertyStr(context, imports, "core");
JS_SetPropertyStr(context, core, "apps", JS_NewCFunctionData(context, _tf_api_core_apps, 1, 0, 1, &process));
JS_SetPropertyStr(context, core, "register", JS_NewCFunctionData(context, _tf_api_core_register, 2, 0, 1, &process));
JS_SetPropertyStr(context, core, "unregister", JS_NewCFunctionData(context, _tf_api_core_unregister, 2, 0, 1, &process));
JS_SetPropertyStr(context, core, "users", JS_NewCFunctionData(context, _tf_api_core_users, 0, 0, 1, &process));
JS_SetPropertyStr(context, core, "permissionsForUser", JS_NewCFunctionData(context, _tf_api_core_permissionsForUser, 1, 0, 1, &process));
JS_SetPropertyStr(context, core, "permissionsGranted", JS_NewCFunctionData(context, _tf_api_core_permissionsGranted, 0, 0, 1, &process));
JSValue app = JS_NewObject(context);
JS_SetPropertyStr(context, app, "owner", JS_GetPropertyStr(context, process, "packageOwner"));
JS_SetPropertyStr(context, app, "name", JS_GetPropertyStr(context, process, "packageName"));
JS_SetPropertyStr(context, core, "app", app);
JS_SetPropertyStr(context, core, "url", JS_GetPropertyStr(context, process, "url"));
JSValue ssb = JS_GetPropertyStr(context, imports, "ssb");
JS_SetPropertyStr(context, ssb, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0));
JS_SetPropertyStr(context, ssb, "getActiveIdentity", JS_NewCFunctionData(context, _tf_ssb_getActiveIdentity, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getIdentities", JS_NewCFunctionData(context, _tf_ssb_getIdentities, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getOwnerIdentities", JS_NewCFunctionData(context, _tf_ssb_getOwnerIdentities, 0, 0, 1, &process));
JS_FreeValue(context, ssb);
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
JSValue permissions = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "permissions") : JS_UNDEFINED;
JSValue administration = JS_IsObject(permissions) ? JS_GetPropertyStr(context, permissions, "administration") : JS_UNDEFINED;
if (JS_ToBool(context, administration) > 0)
{
JS_SetPropertyStr(context, core, "globalSettingsDescriptions", JS_NewCFunction(context, _tf_ssb_globalSettingsDescriptions, "globalSettingsDescriptions", 0));
JS_SetPropertyStr(context, core, "globalSettingsGet", JS_NewCFunction(context, _tf_ssb_globalSettingsGet, "globalSettingsGet", 1));
}
JS_FreeValue(context, administration);
JS_FreeValue(context, permissions);
JS_FreeValue(context, credentials);
JS_FreeValue(context, core);
return JS_UNDEFINED;
}

View File

@@ -1,42 +0,0 @@
#include "bcrypt.js.h"
#include "task.h"
#include "ow-crypt.h"
#include "quickjs.h"
#include "uv.h"
static JSValue _crypt_hashpw(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
const char* key = JS_ToCString(context, argv[0]);
const char* salt = JS_ToCString(context, argv[1]);
char output[7 + 22 + 31 + 1];
char* hash = crypt_rn(key, salt, output, sizeof(output));
JSValue result = JS_NewString(context, hash);
JS_FreeCString(context, key);
JS_FreeCString(context, salt);
return result;
}
static JSValue _crypt_gensalt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
int length = 0;
JS_ToInt32(context, &length, argv[0]);
char buffer[16];
tf_task_t* task = tf_task_get(context);
size_t bytes = uv_random(tf_task_get_loop(task), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
char output[7 + 22 + 1];
char* salt = crypt_gensalt_rn("$2b$", length, buffer, bytes, output, sizeof(output));
JSValue result = JS_NewString(context, salt);
return result;
}
void tf_bcrypt_register(JSContext* context)
{
JSValue global = JS_GetGlobalObject(context);
JSValue bcrypt = JS_NewObject(context);
JS_SetPropertyStr(context, global, "bCrypt", bcrypt);
JS_SetPropertyStr(context, bcrypt, "hashpw", JS_NewCFunction(context, _crypt_hashpw, "hashpw", 2));
JS_SetPropertyStr(context, bcrypt, "gensalt", JS_NewCFunction(context, _crypt_gensalt, "gensalt", 1));
JS_FreeValue(context, global);
}

View File

@@ -1,19 +0,0 @@
#pragma once
/**
** \defgroup bcrypt_js bCrypt
** Exposes bcrypt to script, where it is used for hashing and verifying
** passwords.
** @{
*/
/** A JS context. */
typedef struct JSContext JSContext;
/**
** Register the bcrypt script interface.
** @param context The JS context.
*/
void tf_bcrypt_register(JSContext* context);
/** @} */

View File

@@ -2,7 +2,6 @@
#include "log.h"
#include "mem.h"
#include "tls.h"
#include "trace.h"
#include "util.js.h"
@@ -20,10 +19,12 @@
static const int k_timeout_ms = 60000;
static tf_http_t** s_http_instances;
int s_http_instance_count;
typedef struct _tf_http_connection_t
{
tf_http_t* http;
tf_tls_session_t* tls;
uv_tcp_t tcp;
uv_shutdown_t shutdown;
uv_timer_t timeout;
@@ -75,7 +76,6 @@ typedef struct _tf_http_handler_t
typedef struct _tf_http_listener_t
{
tf_http_t* http;
tf_tls_context_t* tls;
uv_tcp_t tcp;
tf_http_cleanup_t* cleanup;
void* user_data;
@@ -106,7 +106,6 @@ typedef struct _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 void _http_timer_reset(tf_http_connection_t* connection);
static void _http_tls_update(tf_http_connection_t* connection);
static void _http_builtin_404_handler(tf_http_request_t* request);
tf_http_t* tf_http_create(uv_loop_t* loop)
@@ -115,6 +114,8 @@ tf_http_t* tf_http_create(uv_loop_t* loop)
*http = (tf_http_t) {
.loop = loop,
};
s_http_instances = tf_resize_vec(s_http_instances, sizeof(tf_http_t*) * (s_http_instance_count + 1));
s_http_instances[s_http_instance_count++] = http;
return http;
}
@@ -255,11 +256,6 @@ static void _http_connection_destroy(tf_http_connection_t* connection, const cha
{
uv_close((uv_handle_t*)&connection->timeout, _http_connection_on_close);
}
if (connection->tls)
{
tf_tls_session_destroy(connection->tls);
connection->tls = NULL;
}
if (connection->ref_count == 0 && !connection->tcp.data && !connection->shutdown.data && !connection->timeout.data)
{
@@ -441,7 +437,6 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
*request = (tf_http_request_t) {
.http = connection->http,
.connection = connection,
.is_tls = connection->tls != NULL,
.method = connection->method,
.path = connection->path,
.query = connection->query,
@@ -582,21 +577,7 @@ static void _http_on_read(uv_stream_t* stream, ssize_t read_size, const uv_buf_t
_http_timer_reset(connection);
if (read_size > 0)
{
if (connection->tls)
{
if (tf_tls_session_write_encrypted(connection->tls, buffer->base, read_size) < 0)
{
_http_connection_destroy(connection, "tf_tls_session_write_encrypted");
}
else
{
_http_tls_update(connection);
}
}
else
{
_http_on_read_plain(connection, buffer->base, read_size);
}
_http_on_read_plain(connection, buffer->base, read_size);
}
else if (read_size < 0)
{
@@ -638,17 +619,6 @@ static void _http_on_connection(uv_stream_t* stream, int status)
tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t));
*connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection }, .is_receiving_headers = true };
if (listener->tls)
{
connection->tls = tf_tls_context_create_session(listener->tls);
if (!connection->tls)
{
_http_connection_destroy(connection, "tf_tls_context_create_session");
return;
}
tf_tls_session_start_accept(connection->tls);
connection->is_handshaking = true;
}
int r = uv_tcp_init(connection->http->loop, &connection->tcp);
if (r)
{
@@ -689,21 +659,15 @@ static void _http_on_connection(uv_stream_t* stream, int status)
return;
}
if (connection->tls)
{
_http_tls_update(connection);
}
http->connections = tf_resize_vec(http->connections, sizeof(tf_http_connection_t*) * (http->connections_count + 1));
http->connections[http->connections_count++] = connection;
}
int tf_http_listen(tf_http_t* http, int port, bool local_only, tf_tls_context_t* tls, tf_http_cleanup_t* cleanup, void* user_data)
int tf_http_listen(tf_http_t* http, int port, bool local_only, tf_http_cleanup_t* cleanup, void* user_data)
{
tf_http_listener_t* listener = tf_malloc(sizeof(tf_http_listener_t));
*listener = (tf_http_listener_t) {
.http = http,
.tls = tls,
.tcp = { .data = listener },
.cleanup = cleanup,
.user_data = user_data,
@@ -872,6 +836,21 @@ void tf_http_destroy(tf_http_t* http)
http->handlers_count = 0;
tf_free(http);
for (int i = 0; i < s_http_instance_count; i++)
{
if (s_http_instances[i] == http)
{
s_http_instances[i] = s_http_instances[--s_http_instance_count];
break;
}
}
if (s_http_instance_count == 0)
{
tf_free(s_http_instances);
s_http_instances = NULL;
}
tf_printf("http %p destroyed\n", http);
}
else
{
@@ -928,71 +907,10 @@ static void _http_write_internal(tf_http_connection_t* connection, const void* d
}
}
static void _http_tls_update(tf_http_connection_t* connection)
{
bool again = true;
while (again)
{
again = false;
if (connection->is_handshaking && connection->tls)
{
switch (tf_tls_session_handshake(connection->tls))
{
case k_tls_handshake_done:
connection->is_handshaking = false;
break;
case k_tls_handshake_more:
break;
case k_tls_handshake_failed:
_http_connection_destroy(connection, "tf_tls_session_handshake");
return;
}
}
/* Maybe we became disconnected and cleaned up our TLS session. */
if (connection->tls)
{
char buffer[8192];
int r = tf_tls_session_read_encrypted(connection->tls, buffer, sizeof(buffer));
if (r > 0)
{
_http_write_internal(connection, buffer, r);
again = true;
}
}
if (connection->tls)
{
char buffer[8192];
int r = tf_tls_session_read_plain(connection->tls, buffer, sizeof(buffer));
if (r > 0)
{
_http_on_read_plain(connection, buffer, r);
again = true;
}
}
}
}
static void _http_write(tf_http_connection_t* connection, const void* data, size_t size)
{
_http_timer_reset(connection);
if (connection->tls)
{
int r = tf_tls_session_write_plain(connection->tls, data, size);
if (r < (ssize_t)size)
{
char buffer[8192];
tf_tls_session_get_error(connection->tls, buffer, sizeof(buffer));
tf_printf("tf_tls_session_write_plain: %s\n", buffer);
}
_http_tls_update(connection);
}
else
{
_http_write_internal(connection, data, size);
}
_http_write_internal(connection, data, size);
}
void tf_http_request_websocket_send(tf_http_request_t* request, int op_code, const void* data, size_t size)
@@ -1215,3 +1133,19 @@ const char* tf_http_get_cookie(const char* cookie_header, const char* name)
}
return NULL;
}
void tf_http_debug_destroy()
{
for (int i = 0; i < s_http_instance_count; i++)
{
tf_http_t* http = s_http_instances[i];
tf_printf("http %p[%d]\n", http, i);
tf_printf(" connections = %d\n", http->connections_count);
for (int j = 0; j < http->connections_count; j++)
{
tf_http_connection_t* connection = http->connections[j];
tf_printf(" connection %p[%d] %s tcp=%p timeout=%p shutdown=%p rc=%d\n", connection, j, connection->trace_name, connection->tcp.data, connection->timeout.data,
connection->shutdown.data, connection->ref_count);
}
}
}

View File

@@ -23,9 +23,6 @@ typedef struct _tf_http_request_t tf_http_request_t;
/** An HTTP instance. */
typedef struct _tf_http_t tf_http_t;
/** A TLS context. */
typedef struct _tf_tls_context_t tf_tls_context_t;
/** A trace instance. */
typedef struct _tf_trace_t tf_trace_t;
@@ -68,8 +65,6 @@ typedef struct _tf_http_request_t
tf_http_t* http;
/** The HTTP connection associated with this request. */
tf_http_connection_t* connection;
/** True if this is an HTTPS session. */
bool is_tls;
/** The HTTP method of the request (GET/POST/...). */
const char* method;
/** The HTTP request path. */
@@ -117,12 +112,11 @@ void tf_http_set_trace(tf_http_t* http, tf_trace_t* trace);
** @param http The HTTP instance.
** @param port The port on which to listen, or 0 to assign a free port.
** @param local_only Only access connections on localhost, otherwise any address.
** @param tls An optional TLS context to use for HTTPS requests.
** @param cleanup A function called when the HTTP instance is being cleaned up.
** @param user_data User data passed to the cleanup callback.
** @return The port number on which the HTTP instance is now listening.
*/
int tf_http_listen(tf_http_t* http, int port, bool local_only, tf_tls_context_t* tls, tf_http_cleanup_t* cleanup, void* user_data);
int tf_http_listen(tf_http_t* http, int port, bool local_only, tf_http_cleanup_t* cleanup, void* user_data);
/**
** Add an HTTP request handler.
@@ -238,4 +232,9 @@ const char* tf_http_status_text(int status);
*/
bool tf_http_pattern_matches(const char* pattern, const char* path);
/**
** Log debug information to diagnose shutdown problems.
*/
void tf_http_debug_destroy();
/** @} */

View File

@@ -4,9 +4,9 @@
#include "http.h"
#include "log.h"
#include "mem.h"
#include "sha1.h"
#include "ssb.db.h"
#include "task.h"
#include "tls.h"
#include "trace.h"
#include "util.js.h"
#include "version.h"
@@ -14,8 +14,6 @@
#include "sodium/crypto_sign.h"
#include "sodium/utils.h"
#include <openssl/sha.h>
#define CYAN "\e[1;36m"
#define MAGENTA "\e[1;35m"
#define YELLOW "\e[1;33m"
@@ -169,8 +167,13 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
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);
SHA1_CTX sha1 = { 0 };
SHA1Init(&sha1);
SHA1Update(&sha1, key_magic, size);
SHA1Final(digest, &sha1);
char key[41] = { 0 };
tf_base64_encode(digest, sizeof(digest), key, sizeof(key));
@@ -253,11 +256,6 @@ JSValue tf_httpd_make_response_object(JSContext* context, tf_http_request_t* req
bool tf_httpd_redirect(tf_http_request_t* request)
{
if (request->is_tls)
{
return false;
}
http_user_data_t* user_data = tf_http_get_user_data(request->http);
if (!user_data || !*user_data->redirect)
{
@@ -272,16 +270,12 @@ bool tf_httpd_redirect(tf_http_request_t* request)
typedef struct _httpd_listener_t
{
tf_tls_context_t* tls;
int padding;
} httpd_listener_t;
static void _httpd_listener_cleanup(void* user_data)
{
httpd_listener_t* listener = user_data;
if (listener->tls)
{
tf_tls_context_destroy(listener->tls);
}
tf_free(listener);
}
@@ -570,7 +564,7 @@ static void _httpd_endpoint_add_slash(tf_http_request_t* request)
host = tf_http_request_get_header(request, "host");
}
char url[1024];
snprintf(url, sizeof(url), "%s%s%s/", request->is_tls ? "https://" : "http://", host, request->path);
snprintf(url, sizeof(url), "%s%s%s/", "http://", host, request->path);
const char* headers[] = {
"Location",
url,
@@ -856,31 +850,6 @@ bool tf_httpd_is_name_valid(const char* name)
return true;
}
static void _httpd_free_user_data(void* user_data)
{
tf_free(user_data);
}
static const char* _httpd_read_file(tf_task_t* task, const char* path)
{
const char* actual = tf_task_get_path_with_root(task, path);
const size_t k_max_read = 8 * 1024 * 1024;
char* result = NULL;
char* buffer = tf_malloc(k_max_read);
FILE* file = fopen(actual, "rb");
if (file)
{
size_t size = fread(buffer, 1, k_max_read, file);
result = tf_malloc(size + 1);
memcpy(result, buffer, size);
result[size] = '\0';
fclose(file);
}
tf_free(buffer);
tf_free((char*)actual);
return result;
}
void tf_httpd_register(JSContext* context)
{
JS_NewClassID(&_httpd_request_class_id);
@@ -909,36 +878,14 @@ tf_http_t* tf_httpd_create(JSContext* context)
tf_http_set_trace(http, tf_task_get_trace(task));
int64_t http_port = 0;
int64_t https_port = 0;
char out_http_port_file[512] = "";
bool local_only = false;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "http_port", &http_port);
tf_ssb_db_get_global_setting_int64(db, "https_port", &https_port);
tf_ssb_db_get_global_setting_string(db, "out_http_port_file", out_http_port_file, sizeof(out_http_port_file));
tf_ssb_db_get_global_setting_bool(db, "http_local_only", &local_only);
tf_ssb_release_db_reader(ssb, db);
if (https_port)
{
http_user_data_t* user_data = tf_http_get_user_data(http);
if (!user_data)
{
user_data = tf_malloc(sizeof(http_user_data_t));
memset(user_data, 0, sizeof(http_user_data_t));
tf_http_set_user_data(http, user_data, _httpd_free_user_data);
}
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_string(db, "http_redirect", user_data->redirect, sizeof(user_data->redirect));
tf_ssb_release_db_reader(ssb, db);
/* Workaround. */
if (strcmp(user_data->redirect, "0") == 0)
{
*user_data->redirect = '\0';
}
}
tf_http_add_handler(http, "/", _httpd_endpoint_root, NULL, task);
tf_http_add_handler(http, "/codemirror/*", tf_httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/lit/*", tf_httpd_endpoint_static, NULL, task);
@@ -973,7 +920,7 @@ tf_http_t* tf_httpd_create(JSContext* context)
{
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
*listener = (httpd_listener_t) { 0 };
int assigned_port = tf_http_listen(http, http_port, local_only, NULL, _httpd_listener_cleanup, listener);
int assigned_port = tf_http_listen(http, http_port, local_only, _httpd_listener_cleanup, listener);
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "http://127.0.0.1:%d/" RESET ".\n", assigned_port);
if (*out_http_port_file)
@@ -992,26 +939,6 @@ tf_http_t* tf_httpd_create(JSContext* context)
}
tf_free((char*)actual_http_port_file);
}
if (https_port)
{
const char* k_certificate = "data/httpd/certificate.pem";
const char* k_private_key = "data/httpd/privatekey.pem";
const char* certificate = _httpd_read_file(task, k_certificate);
const char* private_key = _httpd_read_file(task, k_private_key);
if (certificate && private_key)
{
tf_tls_context_t* tls = tf_tls_context_create();
tf_tls_context_set_certificate(tls, certificate);
tf_tls_context_set_private_key(tls, private_key);
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
*listener = (httpd_listener_t) { .tls = tls };
int assigned_port = tf_http_listen(http, https_port, local_only, tls, _httpd_listener_cleanup, listener);
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "https://127.0.0.1:%d/" RESET ".\n", assigned_port);
}
tf_free((char*)certificate);
tf_free((char*)private_key);
}
}
return http;
}

View File

@@ -37,11 +37,11 @@ typedef struct _login_request_t
const char* tf_httpd_make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie)
{
const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
int length = session_cookie ? snprintf(NULL, 0, k_pattern, session_cookie, k_httpd_auth_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
int length = session_cookie ? snprintf(NULL, 0, k_pattern, session_cookie, k_httpd_auth_refresh_interval, "") : 0;
char* cookie = length ? tf_malloc(length + 1) : NULL;
if (cookie)
{
snprintf(cookie, length + 1, k_pattern, session_cookie, k_httpd_auth_refresh_interval, request->is_tls ? "Secure; " : "");
snprintf(cookie, length + 1, k_pattern, session_cookie, k_httpd_auth_refresh_interval, "");
}
return cookie;
}
@@ -226,7 +226,7 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", "http://", tf_http_request_get_header(request, "host"));
}
goto done;
}
@@ -332,7 +332,7 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", "http://", tf_http_request_get_header(request, "host"));
}
login->set_cookie_header = tf_httpd_make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
@@ -416,8 +416,7 @@ void tf_httpd_endpoint_login(tf_http_request_t* request)
void tf_httpd_endpoint_logout(tf_http_request_t* request)
{
const char* k_set_cookie = request->is_tls ? "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly"
: "session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
const char* k_set_cookie = "session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
const char* k_location_format = "/login%s%s";
int length = snprintf(NULL, 0, k_location_format, request->query ? "?" : "", request->query);
char* location = alloca(length + 1);

View File

@@ -13,13 +13,13 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2025.8</string>
<string>0.2025.10</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>16</string>
<string>18</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>LSRequiresIPhoneOS</key>

BIN
src/ios/tildefriends512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1502,13 +1502,11 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
tf_ssb_t* ssb = tf_task_get_ssb(task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
int64_t http_port = 0;
int64_t https_port = 0;
char out_http_port_file[512] = "";
tf_ssb_db_get_global_setting_int64(db, "http_port", &http_port);
tf_ssb_db_get_global_setting_int64(db, "https_port", &https_port);
tf_ssb_db_get_global_setting_string(db, "out_http_port_file", out_http_port_file, sizeof(out_http_port_file));
tf_ssb_release_db_reader(ssb, db);
if (http_port || https_port || *out_http_port_file)
if (http_port || *out_http_port_file)
{
if (args->zip)
{
@@ -1578,28 +1576,28 @@ static void _shed_privileges()
if (setrlimit(RLIMIT_FSIZE, &zeroLimit) != 0)
{
perror("setrlimit(RLIMIT_FSIZE, {0, 0})");
exit(-1);
exit(EXIT_FAILURE);
}
if (setrlimit(RLIMIT_NOFILE, &zeroLimit) != 0)
{
perror("setrlimit(RLIMIT_NOFILE, {0, 0})");
exit(-1);
exit(EXIT_FAILURE);
}
if (setrlimit(RLIMIT_NPROC, &zeroLimit) != 0)
{
perror("setrlimit(RLIMIT_NPROC, {0, 0})");
exit(-1);
exit(EXIT_FAILURE);
}
#if !defined(__MACH__) && !defined(__OpenBSD__)
if (setrlimit(RLIMIT_LOCKS, &zeroLimit) != 0)
{
perror("setrlimit(RLIMIT_LOCKS, {0, 0})");
exit(-1);
exit(EXIT_FAILURE);
}
if (setrlimit(RLIMIT_MSGQUEUE, &zeroLimit) != 0)
{
perror("setrlimit(RLIMIT_MSGQUEUE, {0, 0})");
exit(-1);
exit(EXIT_FAILURE);
}
#endif
#endif
@@ -1609,12 +1607,12 @@ static void _shed_privileges()
if (unveil("/dev/null", "r") || unveil(NULL, NULL))
{
perror("unveil");
exit(-1);
exit(EXIT_FAILURE);
}
if (pledge("stdio unveil", NULL))
{
perror("pledge");
exit(-1);
exit(EXIT_FAILURE);
}
#endif
}
@@ -1831,7 +1829,7 @@ static void _error_handler(int sig)
const char* stack = tf_util_backtrace_string();
tf_printf("ERROR:\n%s\n", stack);
tf_free((void*)stack);
_exit(1);
_exit(EXIT_FAILURE);
}
#if defined(_WIN32)
@@ -1843,7 +1841,7 @@ static LONG WINAPI _win32_exception_handler(EXCEPTION_POINTERS* info)
const char* stack = tf_util_backtrace_string();
tf_printf("ERROR:\n%s\n", stack);
tf_free((void*)stack);
_exit(1);
_exit(EXIT_FAILURE);
}
return EXCEPTION_CONTINUE_SEARCH;
}
@@ -1869,7 +1867,6 @@ static void _startup(int argc, char* argv[])
prctl(PR_SET_PDEATHSIG, SIGKILL);
#endif
tf_mem_replace_uv_allocator();
tf_mem_replace_tls_allocator();
tf_mem_replace_sqlite_allocator();
uv_setup_args(argc, argv);
tf_taskstub_startup();

View File

@@ -7,8 +7,6 @@
#include "sqlite3.h"
#include "uv.h"
#include <openssl/crypto.h>
#include <stdbool.h>
#include <string.h>
@@ -19,7 +17,6 @@ static bool s_mem_tracking;
static tf_mem_node_t* s_mem_tracked;
static int64_t s_tf_malloc_size;
static int64_t s_uv_malloc_size;
static int64_t s_tls_malloc_size;
static int64_t s_js_malloc_size;
static int64_t s_sqlite_malloc_size;
@@ -387,31 +384,6 @@ size_t tf_mem_get_uv_malloc_size()
return s_uv_malloc_size;
}
static void* _tf_tls_alloc(size_t size, const char* file, int line)
{
return _tf_alloc(&s_tls_malloc_size, size);
}
static void* _tf_tls_realloc(void* ptr, size_t size, const char* file, int line)
{
return _tf_realloc(&s_tls_malloc_size, ptr, size);
}
static void _tf_tls_free(void* ptr, const char* file, int line)
{
_tf_free(&s_tls_malloc_size, ptr);
}
void tf_mem_replace_tls_allocator()
{
CRYPTO_set_mem_functions(_tf_tls_alloc, _tf_tls_realloc, _tf_tls_free);
}
size_t tf_mem_get_tls_malloc_size()
{
return s_tls_malloc_size;
}
void* tf_malloc(size_t size)
{
return _tf_alloc(&s_tf_malloc_size, size);

View File

@@ -3,7 +3,7 @@
/**
** \defgroup mem Memory management
** tf_malloc() and friends use malloc() behind the scenes but optionally
** track memory per system (OpenSSL, sqlite, libuv, ...) and store callstacks
** track memory per system (sqlite, libuv, ...) and store callstacks
** to help debug leaks.
** @{
*/
@@ -38,17 +38,6 @@ void tf_mem_replace_uv_allocator();
*/
size_t tf_mem_get_uv_malloc_size();
/**
** Register a custom allocator with OpenSSL.
*/
void tf_mem_replace_tls_allocator();
/**
** Get the number of bytes currently allocated by OpenSSL.
** @return The allocated size in bytes.
*/
size_t tf_mem_get_tls_malloc_size();
/**
** Register a custom allocator with SQLite.
*/

329
src/sha1.c Normal file
View File

@@ -0,0 +1,329 @@
/*
* SHA1 hash implementation and interface functions
* Copyright (c) 2003-2005, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
#include "sha1.h"
#include <stddef.h>
#include <string.h>
/* ===== start - public domain SHA1 implementation ===== */
/*
SHA-1 in C
By Steve Reid <sreid@sea-to-sky.net>
100% Public Domain
-----------------
Modified 7/98
By James H. Brown <jbrown@burgoyne.com>
Still 100% Public Domain
Corrected a problem which generated improper hash values on 16 bit machines
Routine SHA1Update changed from
void SHA1Update(SHA1_CTX* context, unsigned char* data, unsigned int
len)
to
void SHA1Update(SHA1_CTX* context, unsigned char* data, unsigned
long len)
The 'len' parameter was declared an int which works fine on 32 bit machines.
However, on 16 bit machines an int is too small for the shifts being done
against it. This caused the hash function to generate incorrect values if len
was greater than 8191 (8K - 1) due to the 'len << 3' on line 3 of SHA1Update().
Since the file IO in main() reads 16K at a time, any file 8K or larger would be
guaranteed to generate the wrong hash (e.g. Test Vector #3, a million "a"s).
I also changed the declaration of variables i & j in SHA1Update to unsigned
long from unsigned int for the same reason.
These changes should make no difference to any 32 bit implementations since an
int and a long are the same size in those environments.
--
I also corrected a few compiler warnings generated by Borland C.
1. Added #include <process.h> for exit() prototype
2. Removed unused variable 'j' in SHA1Final
3. Changed exit(0) to return(0) at end of main.
ALL changes I made can be located by searching for comments containing 'JHB'
-----------------
Modified 8/98
By Steve Reid <sreid@sea-to-sky.net>
Still 100% public domain
1- Removed #include <process.h> and used return() instead of exit()
2- Fixed overwriting of finalcount in SHA1Final() (discovered by Chris Hall)
3- Changed email address from steve@edmweb.com to sreid@sea-to-sky.net
-----------------
Modified 4/01
By Saul Kravitz <Saul.Kravitz@celera.com>
Still 100% PD
Modified to run on Compaq Alpha hardware.
-----------------
Modified 4/01
By Jouni Malinen <j@w1.fi>
Minor changes to match the coding style used in Dynamics.
Modified September 24, 2004
By Jouni Malinen <j@w1.fi>
Fixed alignment issue in SHA1Transform when SHA1HANDSOFF is defined.
-----------------
Modified September 29, 2025
By Cory McWilliams <cory@tildefriends.net>
Adapted from
https://web.mit.edu/freebsd/head/contrib/wpa/src/crypto/sha1-internal.c.
Modified to build outside of FreeBSD. Updated with clang-format.
*/
/*
Test Vectors (from FIPS PUB 180-1)
"abc"
A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
A million repetitions of "a"
34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
*/
#define SHA1HANDSOFF
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
/* blk0() and blk() perform the initial expand. */
/* I got the idea of expanding during the round function from SSLeay */
#ifndef WORDS_BIGENDIAN
#define blk0(i) (block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | (rol(block->l[i], 8) & 0x00FF00FF))
#else
#define blk0(i) block->l[i]
#endif
#define blk(i) (block->l[i & 15] = rol(block->l[(i + 13) & 15] ^ block->l[(i + 8) & 15] ^ block->l[(i + 2) & 15] ^ block->l[i & 15], 1))
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
#define R0(v, w, x, y, z, i) \
z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \
w = rol(w, 30);
#define R1(v, w, x, y, z, i) \
z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \
w = rol(w, 30);
#define R2(v, w, x, y, z, i) \
z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5); \
w = rol(w, 30);
#define R3(v, w, x, y, z, i) \
z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \
w = rol(w, 30);
#define R4(v, w, x, y, z, i) \
z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \
w = rol(w, 30);
#ifdef VERBOSE /* SAK */
void SHAPrintContext(SHA1_CTX* context, char* msg)
{
printf("%s (%d,%d) %x %x %x %x %x\n", msg, context->count[0], context->count[1], context->state[0], context->state[1], context->state[2], context->state[3], context->state[4]);
}
#endif
/* Hash a single 512-bit block. This is the core of the algorithm. */
void SHA1Transform(uint32_t state[5], const unsigned char buffer[64])
{
uint32_t a, b, c, d, e;
typedef union
{
unsigned char c[64];
uint32_t l[16];
} CHAR64LONG16;
CHAR64LONG16* block;
#ifdef SHA1HANDSOFF
CHAR64LONG16 workspace;
block = &workspace;
memcpy(block, buffer, 64);
#else
block = (CHAR64LONG16*)buffer;
#endif
/* Copy context->state[] to working vars */
a = state[0];
b = state[1];
c = state[2];
d = state[3];
e = state[4];
/* 4 rounds of 20 operations each. Loop unrolled. */
R0(a, b, c, d, e, 0);
R0(e, a, b, c, d, 1);
R0(d, e, a, b, c, 2);
R0(c, d, e, a, b, 3);
R0(b, c, d, e, a, 4);
R0(a, b, c, d, e, 5);
R0(e, a, b, c, d, 6);
R0(d, e, a, b, c, 7);
R0(c, d, e, a, b, 8);
R0(b, c, d, e, a, 9);
R0(a, b, c, d, e, 10);
R0(e, a, b, c, d, 11);
R0(d, e, a, b, c, 12);
R0(c, d, e, a, b, 13);
R0(b, c, d, e, a, 14);
R0(a, b, c, d, e, 15);
R1(e, a, b, c, d, 16);
R1(d, e, a, b, c, 17);
R1(c, d, e, a, b, 18);
R1(b, c, d, e, a, 19);
R2(a, b, c, d, e, 20);
R2(e, a, b, c, d, 21);
R2(d, e, a, b, c, 22);
R2(c, d, e, a, b, 23);
R2(b, c, d, e, a, 24);
R2(a, b, c, d, e, 25);
R2(e, a, b, c, d, 26);
R2(d, e, a, b, c, 27);
R2(c, d, e, a, b, 28);
R2(b, c, d, e, a, 29);
R2(a, b, c, d, e, 30);
R2(e, a, b, c, d, 31);
R2(d, e, a, b, c, 32);
R2(c, d, e, a, b, 33);
R2(b, c, d, e, a, 34);
R2(a, b, c, d, e, 35);
R2(e, a, b, c, d, 36);
R2(d, e, a, b, c, 37);
R2(c, d, e, a, b, 38);
R2(b, c, d, e, a, 39);
R3(a, b, c, d, e, 40);
R3(e, a, b, c, d, 41);
R3(d, e, a, b, c, 42);
R3(c, d, e, a, b, 43);
R3(b, c, d, e, a, 44);
R3(a, b, c, d, e, 45);
R3(e, a, b, c, d, 46);
R3(d, e, a, b, c, 47);
R3(c, d, e, a, b, 48);
R3(b, c, d, e, a, 49);
R3(a, b, c, d, e, 50);
R3(e, a, b, c, d, 51);
R3(d, e, a, b, c, 52);
R3(c, d, e, a, b, 53);
R3(b, c, d, e, a, 54);
R3(a, b, c, d, e, 55);
R3(e, a, b, c, d, 56);
R3(d, e, a, b, c, 57);
R3(c, d, e, a, b, 58);
R3(b, c, d, e, a, 59);
R4(a, b, c, d, e, 60);
R4(e, a, b, c, d, 61);
R4(d, e, a, b, c, 62);
R4(c, d, e, a, b, 63);
R4(b, c, d, e, a, 64);
R4(a, b, c, d, e, 65);
R4(e, a, b, c, d, 66);
R4(d, e, a, b, c, 67);
R4(c, d, e, a, b, 68);
R4(b, c, d, e, a, 69);
R4(a, b, c, d, e, 70);
R4(e, a, b, c, d, 71);
R4(d, e, a, b, c, 72);
R4(c, d, e, a, b, 73);
R4(b, c, d, e, a, 74);
R4(a, b, c, d, e, 75);
R4(e, a, b, c, d, 76);
R4(d, e, a, b, c, 77);
R4(c, d, e, a, b, 78);
R4(b, c, d, e, a, 79);
/* Add the working vars back into context.state[] */
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
state[4] += e;
/* Wipe variables */
a = b = c = d = e = 0;
#ifdef SHA1HANDSOFF
memset(block, 0, 64);
#endif
}
/* SHA1Init - Initialize new context */
void SHA1Init(SHA1_CTX* context)
{
/* SHA1 initialization constants */
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;
context->state[4] = 0xC3D2E1F0;
context->count[0] = context->count[1] = 0;
}
/* Run your data through this. */
void SHA1Update(SHA1_CTX* context, const void* _data, uint32_t len)
{
uint32_t i, j;
const unsigned char* data = _data;
#ifdef VERBOSE
SHAPrintContext(context, "before");
#endif
j = (context->count[0] >> 3) & 63;
if ((context->count[0] += len << 3) < (len << 3))
context->count[1]++;
context->count[1] += (len >> 29);
if ((j + len) > 63)
{
memcpy(&context->buffer[j], data, (i = 64 - j));
SHA1Transform(context->state, context->buffer);
for (; i + 63 < len; i += 64)
{
SHA1Transform(context->state, &data[i]);
}
j = 0;
}
else
i = 0;
memcpy(&context->buffer[j], &data[i], len - i);
#ifdef VERBOSE
SHAPrintContext(context, "after ");
#endif
}
/* Add padding and return the message digest. */
void SHA1Final(unsigned char digest[20], SHA1_CTX* context)
{
uint32_t i;
unsigned char finalcount[8];
for (i = 0; i < 8; i++)
{
/* Endian independent */
finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255);
}
SHA1Update(context, (unsigned char*)"\200", 1);
while ((context->count[0] & 504) != 448)
{
SHA1Update(context, (unsigned char*)"\0", 1);
}
/* Should cause a SHA1Transform() */
SHA1Update(context, finalcount, 8);
for (i = 0; i < 20; i++)
{
digest[i] = (unsigned char)((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
}
/* Wipe variables */
i = 0;
memset(context->buffer, 0, 64);
memset(context->state, 0, 20);
memset(context->count, 0, 8);
memset(finalcount, 0, 8);
}
/* ===== end - public domain SHA1 implementation ===== */

71
src/sha1.h Normal file
View File

@@ -0,0 +1,71 @@
/*
* SHA1 internal definitions
* Copyright (c) 2003-2005, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
/**
** \defgroup sha1 SHA1
** SHA1 API.
** Adapted from
** https://web.mit.edu/freebsd/head/contrib/wpa/src/crypto/sha1_i.h by Cory
** McWilliams 2025-09-28.
** @{
*/
#ifndef SHA1_I_H
#define SHA1_I_H
#include <inttypes.h>
/**
** SHA1 context struct.
*/
struct SHA1Context
{
/** SHA1 state. */
uint32_t state[5];
/** SHA1 count. */
uint32_t count[2];
/** SHA1 buffer. */
unsigned char buffer[64];
};
/**
** SHA1 context.
*/
typedef struct SHA1Context SHA1_CTX;
/**
** Initialize a SHA1 context.
** @param context The context.
*/
void SHA1Init(struct SHA1Context* context);
/**
** Calculate an ongoing hash for a block of data.
** @param context The SHA1 context.
** @param data The data to hash.
** @param len The length of data.
*/
void SHA1Update(struct SHA1Context* context, const void* data, uint32_t len);
/**
** Calculate the final hash digest.
** @param digest Populated with the digest.
** @param context The SHA1 context.
*/
void SHA1Final(unsigned char digest[20], struct SHA1Context* context);
/**
** Perform a SHA1 transformation.
** @param state The SHA1 state.
** @param buffer The data.
*/
void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]);
#endif /* SHA1_I_H */
/** @} */

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
#pragma once
/**
** \defgroup socket_js Socket Interface
** Exposes network sockets to script.
** @{
*/
#include "quickjs.h"
/**
** Register the socket script interface.
** @param context The JS context.
** @return The Socket constructor.
*/
JSValue tf_socket_register(JSContext* context);
/**
** Get the number of active socket objects.
** @return The count.
*/
int tf_socket_get_count();
/**
** Get the number of connected socket objects.
** @return the count.
*/
int tf_socket_get_open_count();
/** @} */

View File

@@ -1,5 +1,6 @@
#include "ssb.h"
#include "http.h"
#include "log.h"
#include "mem.h"
#include "ssb.connections.h"
@@ -2711,6 +2712,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_printf("--\n");
uv_print_all_handles(ssb->loop, stdout);
}
tf_http_debug_destroy();
uv_run(ssb->loop, UV_RUN_ONCE);
}

View File

@@ -421,6 +421,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
tf_printf("Done.\n");
}
_tf_ssb_db_exec(db, "DELETE FROM blob_wants_cache WHERE blob_wants_cache.id IN (SELECT blobs.id FROM blobs)");
if (!_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'index' AND name = 'blob_wants_cache_source_id_unique_index'"))
{
tf_printf("Creating blob_wants_cache UNIQUE constraint.\n");
@@ -436,8 +437,11 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ai_blob_wants_cache AFTER INSERT ON messages_refs BEGIN "
"INSERT INTO blob_wants_cache (source, id, timestamp) "
"SELECT messages.id, new.ref, messages.timestamp FROM messages WHERE messages.id = new.message AND "
"LENGTH(new.ref) = 52 AND new.ref LIKE '&%.sha256' "
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
"JOIN blobs ON new.ref = blobs.id "
"WHERE messages.id = new.message AND "
"LENGTH(new.ref) = 52 AND new.ref LIKE '&%.sha256' AND "
"blobs.content IS NULL "
"ON CONFLICT (source, id) DO NOTHING; END");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS blobs_refs_ai_blob_wants_cache");
_tf_ssb_db_exec(db,
@@ -445,6 +449,8 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"INSERT INTO blob_wants_cache (source, id, timestamp) "
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
"JOIN blob_wants_cache bwc ON bwc.source = messages.id AND bwc.id = new.blob "
"JOIN blobs ON bwc.id = blobs.id "
"WHERE blobs.content IS NULL "
"ON CONFLICT (source, id) DO NOTHING; END");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ad_blob_wants_cache AFTER DELETE ON messages BEGIN "
@@ -574,16 +580,15 @@ static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const c
return last_row_id;
}
static char* _tf_ssb_db_get_message_blob_wants(tf_ssb_t* ssb, int64_t rowid)
static char* _tf_ssb_db_get_message_blob_wants(sqlite3* db, int64_t rowid)
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement;
char* result = NULL;
size_t size = 0;
if (sqlite3_prepare_v2(db,
"SELECT DISTINCT json.value FROM messages, json_tree(messages.content) AS json LEFT OUTER JOIN blobs ON json.value = blobs.id WHERE messages.rowid = ?1 AND "
"json.value LIKE '&%.sha256' AND length(json.value) = ?2 AND blobs.content IS NULL",
"length(json.value) = ?2 AND json.value LIKE '&%.sha256' AND blobs.content IS NULL",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_int64(statement, 1, rowid) == SQLITE_OK && sqlite3_bind_int(statement, 2, k_blob_id_len - 1) == SQLITE_OK)
@@ -615,7 +620,6 @@ static char* _tf_ssb_db_get_message_blob_wants(tf_ssb_t* ssb, int64_t rowid)
result = tf_realloc(result, size + 1);
result[size] = '\0';
tf_ssb_release_db_reader(ssb, db);
return result;
}
@@ -653,7 +657,7 @@ static void _tf_ssb_db_store_message_work(tf_ssb_t* ssb, void* user_data)
if (last_row_id != -1)
{
store->out_stored = true;
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(ssb, last_row_id);
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
}
store = store->next;
}
@@ -972,7 +976,7 @@ static void _tf_ssb_db_blob_store_work(tf_ssb_t* ssb, void* user_data)
static void _tf_ssb_db_blob_store_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
blob_store_work_t* blob_work = user_data;
if (status == 0 && *blob_work->id)
if (status == 0 && *blob_work->id && blob_work->is_new)
{
tf_ssb_notify_blob_stored(ssb, blob_work->id);
}
@@ -2461,6 +2465,32 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou
return result;
}
const char* tf_ssb_db_get_global_setting_string_alloc(sqlite3* db, const char* name)
{
const char* result = NULL;
sqlite3_stmt* statement;
if (sqlite3_prepare_v2(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
if (!result)
{
result = tf_strdup(tf_util_get_default_global_setting_string(name));
}
return result;
}
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, char* value)
{
tf_setting_kind_t kind = tf_util_get_global_setting_kind(name);

View File

@@ -500,6 +500,14 @@ bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t*
*/
bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size);
/**
** Get a string global setting value.
** @param db The database.
** @param name The setting name.
** @return The setting if found.
*/
const char* tf_ssb_db_get_global_setting_string_alloc(sqlite3* db, const char* name);
/**
** Set a global setting from a string representation of its value.
** @param db The database.

View File

@@ -363,85 +363,6 @@ static JSValue _tf_ssb_swap_with_server_identity(JSContext* context, JSValueCons
return result;
}
typedef struct _identities_visit_t
{
JSContext* context;
JSValue promise[2];
const char** identities;
int count;
char user[];
} identities_visit_t;
static void _tf_ssb_getIdentities_visit(const char* identity, void* user_data)
{
identities_visit_t* work = user_data;
work->identities = tf_resize_vec(work->identities, (work->count + 1) * sizeof(const char*));
char id[k_id_base64_len];
snprintf(id, sizeof(id), "@%s", identity);
work->identities[work->count++] = tf_strdup(id);
}
static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data)
{
identities_visit_t* work = user_data;
if (tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{
char id[k_id_base64_len] = "";
if (tf_ssb_whoami(ssb, id, sizeof(id)))
{
_tf_ssb_getIdentities_visit(*id == '@' ? id + 1 : id, work);
}
}
tf_ssb_db_identity_visit(ssb, work->user, _tf_ssb_getIdentities_visit, user_data);
}
static void _tf_ssb_get_all_identities_work(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_db_identity_visit_all(ssb, _tf_ssb_getIdentities_visit, user_data);
}
static void _tf_ssb_get_identities_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
identities_visit_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->count; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->identities[i]));
tf_free((void*)work->identities[i]);
}
tf_free(work->identities);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
identities_visit_t* work = tf_malloc(sizeof(identities_visit_t) + user_length + 1);
*work = (identities_visit_t) {
.context = context,
};
memcpy(work->user, user, user_length + 1);
JS_FreeCString(context, user);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
}
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
@@ -513,109 +434,6 @@ static JSValue _tf_ssb_getServerIdentity(JSContext* context, JSValueConst this_v
return result;
}
static JSValue _tf_ssb_getAllIdentities(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb)
{
identities_visit_t* work = tf_malloc(sizeof(identities_visit_t));
*work = (identities_visit_t) {
.context = context,
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_all_identities_work, _tf_ssb_get_identities_after_work, work);
}
return result;
}
typedef struct _active_identity_work_t
{
JSContext* context;
const char* name;
const char* package_owner;
const char* package_name;
char identity[k_id_base64_len];
int result;
JSValue promise[2];
} active_identity_work_t;
static void _tf_ssb_getActiveIdentity_visit(const char* identity, void* user_data)
{
active_identity_work_t* request = user_data;
if (!*request->identity)
{
snprintf(request->identity, sizeof(request->identity), "@%s", identity);
}
}
static void _tf_ssb_getActiveIdentity_work(tf_ssb_t* ssb, void* user_data)
{
active_identity_work_t* request = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_identity_get_active(db, request->name, request->package_owner, request->package_name, request->identity, sizeof(request->identity));
tf_ssb_release_db_reader(ssb, db);
if (!*request->identity)
{
tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request);
}
if (!*request->identity && tf_ssb_db_user_has_permission(ssb, NULL, request->name, "administration"))
{
tf_ssb_whoami(ssb, request->identity, sizeof(request->identity));
}
}
static void _tf_ssb_getActiveIdentity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
active_identity_work_t* request = user_data;
JSContext* context = request->context;
if (request->result == 0)
{
JSValue identity = JS_NewString(context, request->identity);
JSValue error = JS_Call(context, request->promise[0], JS_UNDEFINED, 1, &identity);
JS_FreeValue(context, identity);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
}
else
{
JSValue error = JS_Call(context, request->promise[1], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
}
JS_FreeValue(context, request->promise[0]);
JS_FreeValue(context, request->promise[1]);
tf_free((void*)request->name);
tf_free((void*)request->package_owner);
tf_free((void*)request->package_name);
tf_free(request);
}
static JSValue _tf_ssb_getActiveIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* name = JS_ToCString(context, argv[0]);
const char* package_owner = JS_ToCString(context, argv[1]);
const char* package_name = JS_ToCString(context, argv[2]);
active_identity_work_t* work = tf_malloc(sizeof(active_identity_work_t));
*work = (active_identity_work_t) {
.context = context,
.name = tf_strdup(name),
.package_owner = tf_strdup(package_owner),
.package_name = tf_strdup(package_name),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, name);
JS_FreeCString(context, package_owner);
JS_FreeCString(context, package_name);
tf_ssb_run_work(ssb, _tf_ssb_getActiveIdentity_work, _tf_ssb_getActiveIdentity_after_work, work);
return result;
}
typedef struct _identity_info_work_t
{
JSContext* context;
@@ -2370,12 +2188,15 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, global, "ssb", object);
JS_SetOpaque(object, ssb);
JSValue object_internal = JS_NewObjectClass(context, _tf_ssb_classId);
JS_SetPropertyStr(context, global, "ssb_internal", object_internal);
JS_SetOpaque(object_internal, ssb);
/* Requires an identity. */
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2));
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
JS_SetPropertyStr(context, object, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2));
JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
JS_SetPropertyStr(context, object, "privateMessageEncrypt", JS_NewCFunction(context, _tf_ssb_private_message_encrypt, "privateMessageEncrypt", 4));
JS_SetPropertyStr(context, object, "privateMessageDecrypt", JS_NewCFunction(context, _tf_ssb_private_message_decrypt, "privateMessageDecrypt", 3));
@@ -2385,9 +2206,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
/* Does not require an identity. */
JS_SetPropertyStr(context, object, "getServerIdentity", JS_NewCFunction(context, _tf_ssb_getServerIdentity, "getServerIdentity", 0));
JS_SetPropertyStr(context, object, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0));
JS_SetPropertyStr(context, object, "getActiveIdentity", JS_NewCFunction(context, _tf_ssb_getActiveIdentity, "getActiveIdentity", 3));
JS_SetPropertyStr(context, object, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object, "blobGet", JS_NewCFunction(context, _tf_ssb_blobGet, "blobGet", 1));
JS_SetPropertyStr(context, object, "connections", JS_NewCFunction(context, _tf_ssb_connections, "connections", 0));
JS_SetPropertyStr(context, object, "storedConnections", JS_NewCFunction(context, _tf_ssb_storedConnections, "storedConnections", 0));
@@ -2406,8 +2224,9 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "blobStore", JS_NewCFunction(context, _tf_ssb_blobStore, "blobStore", 1));
/* Trusted only. */
JS_SetPropertyStr(context, object, "addEventListener", JS_NewCFunction(context, _tf_ssb_add_event_listener, "addEventListener", 2));
JS_SetPropertyStr(context, object, "removeEventListener", JS_NewCFunction(context, _tf_ssb_remove_event_listener, "removeEventListener", 2));
JS_SetPropertyStr(context, object_internal, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object_internal, "addEventListener", JS_NewCFunction(context, _tf_ssb_add_event_listener, "addEventListener", 2));
JS_SetPropertyStr(context, object_internal, "removeEventListener", JS_NewCFunction(context, _tf_ssb_remove_event_listener, "removeEventListener", 2));
JS_FreeValue(context, global);
}

View File

@@ -923,7 +923,7 @@ static void _write_file(const char* path, const char* contents)
fclose(file);
}
#define TEST_ARGS " --args=ssb_port=0,http_port=0,https_port=0"
#define TEST_ARGS " --args=ssb_port=0,http_port=0"
void tf_ssb_test_encrypt(const tf_test_options_t* options)
{
@@ -1128,6 +1128,11 @@ void tf_ssb_test_replicate(const tf_test_options_t* options)
tf_printf("%s user %d = %s private=%s\n", added ? "added" : "failed", i, public[i], private[i]);
}
char blob_id[k_id_base64_len] = { 0 };
const char* k_blob = "Hello, new blob!";
b = tf_ssb_db_blob_store(ssb0, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL);
assert(b);
JSContext* context0 = tf_ssb_get_context(ssb0);
for (int i = 0; i < k_key_count - 1; i++)
{
@@ -1152,6 +1157,7 @@ void tf_ssb_test_replicate(const tf_test_options_t* options)
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
JS_SetPropertyStr(context0, obj, "text", JS_NewString(context0, "Hello, world!"));
JS_SetPropertyStr(context0, obj, "arbitrary_reference", JS_NewString(context0, blob_id));
stored = false;
signed_message = tf_ssb_sign_message(ssb0, self, private_bin, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
@@ -1213,6 +1219,189 @@ void tf_ssb_test_replicate(const tf_test_options_t* options)
tf_ssb_remove_message_added_callback(ssb1, _message_added, &count1);
tf_printf("done\n");
tf_printf("Waiting for blob.\n");
while (!tf_ssb_db_blob_get(ssb1, blob_id, NULL, NULL))
{
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_ONCE);
tf_ssb_set_main_thread(ssb1, false);
}
tf_printf("done\n");
tf_ssb_send_close(ssb1);
uv_close((uv_handle_t*)&idle0, NULL);
uv_close((uv_handle_t*)&idle1, NULL);
tf_printf("final run\n");
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_DEFAULT);
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);
tf_printf("done\n");
tf_printf("destroy 0\n");
tf_ssb_destroy(ssb0);
tf_printf("destroy 1\n");
tf_ssb_destroy(ssb1);
tf_printf("close\n");
uv_loop_close(&loop);
}
void tf_ssb_test_replicate_blob(const tf_test_options_t* options)
{
tf_printf("Testing blob replication.\n");
uv_loop_t loop = { 0 };
uv_loop_init(&loop);
unlink("out/test_db0.sqlite");
tf_ssb_t* ssb0 = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL);
tf_ssb_register(tf_ssb_get_context(ssb0), ssb0);
unlink("out/test_db1.sqlite");
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, "file:out/test_db1.sqlite", NULL);
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
uv_idle_t idle0 = { .data = ssb0 };
uv_idle_init(&loop, &idle0);
uv_idle_start(&idle0, _ssb_test_idle);
uv_idle_t idle1 = { .data = ssb1 };
uv_idle_init(&loop, &idle1);
uv_idle_start(&idle1, _ssb_test_idle);
test_t test = {
.ssb0 = ssb0,
.ssb1 = ssb1,
};
tf_ssb_add_connections_changed_callback(ssb0, _ssb_test_connections_changed, NULL, &test);
tf_ssb_add_connections_changed_callback(ssb1, _ssb_test_connections_changed, NULL, &test);
tf_ssb_generate_keys(ssb0);
tf_ssb_generate_keys(ssb1);
uint8_t priv0[crypto_sign_SECRETKEYBYTES] = { 0 };
uint8_t priv1[crypto_sign_SECRETKEYBYTES] = { 0 };
tf_ssb_get_private_key(ssb0, priv0, sizeof(priv0));
tf_ssb_get_private_key(ssb1, priv1, sizeof(priv1));
char id0[k_id_base64_len] = { 0 };
char id1[k_id_base64_len] = { 0 };
bool b = tf_ssb_whoami(ssb0, id0, sizeof(id0));
(void)b;
assert(b);
b = tf_ssb_whoami(ssb1, id1, sizeof(id1));
assert(b);
tf_printf("ID %s and %s\n", id0, id1);
char priv0_str[512] = { 0 };
char priv1_str[512] = { 0 };
tf_base64_encode(priv0, sizeof(priv0), priv0_str, sizeof(priv0_str));
tf_base64_encode(priv1, sizeof(priv0), priv1_str, sizeof(priv1_str));
tf_ssb_db_identity_add(ssb0, "test", id0 + 1, priv0_str);
tf_ssb_db_identity_add(ssb1, "test", id1 + 1, priv1_str);
static const int k_key_count = 5;
char public[k_key_count][k_id_base64_len - 1];
char private[k_key_count][512];
for (int i = 0; i < k_key_count; i++)
{
tf_ssb_generate_keys_buffer(public[i], sizeof(public[i]), private[i], sizeof(private[i]));
bool added = tf_ssb_db_identity_add(ssb0, "test", public[i], private[i]);
tf_printf("%s user %d = %s private=%s\n", added ? "added" : "failed", i, public[i], private[i]);
}
tf_printf("ssb0\n");
tf_ssb_db_identity_visit_all(ssb0, _test_print_identity, ssb0);
tf_printf("ssb1\n");
tf_ssb_db_identity_visit_all(ssb1, _test_print_identity, ssb1);
tf_ssb_server_open(ssb0, 12347);
uint8_t id0bin[k_id_bin_len];
tf_ssb_id_str_to_bin(id0bin, id0);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
tf_printf("Waiting for connection.\n");
while (test.connection_count0 != 1 || test.connection_count1 != 1)
{
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_ONCE);
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);
}
tf_ssb_server_close(ssb0);
char blob_id[k_id_base64_len] = { 0 };
const char* k_blob = "Hello, new blob!";
b = tf_ssb_db_blob_store(ssb0, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL);
assert(b);
JSContext* context0 = tf_ssb_get_context(ssb0);
for (int i = 0; i < k_key_count - 1; i++)
{
JSValue obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "contact"));
char self[k_id_base64_len];
snprintf(self, sizeof(self), "@%s", public[i]);
char contact[k_id_base64_len];
snprintf(contact, sizeof(contact), "@%s", public[i + 1]);
JS_SetPropertyStr(context0, obj, "contact", JS_NewString(context0, contact));
JS_SetPropertyStr(context0, obj, "following", JS_TRUE);
bool stored = false;
uint8_t private_bin[512] = { 0 };
tf_base64_decode(private[i], strlen(private[i]) - strlen(".ed25519"), private_bin, sizeof(private_bin));
tf_printf("ssb0 %s following %s\n", self, contact);
JSValue signed_message = tf_ssb_sign_message(ssb0, self, private_bin, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
JS_SetPropertyStr(context0, obj, "text", JS_NewString(context0, "Hello, world!"));
JS_SetPropertyStr(context0, obj, "arbitrary_reference", JS_NewString(context0, blob_id));
stored = false;
signed_message = tf_ssb_sign_message(ssb0, self, private_bin, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
}
JSContext* context1 = tf_ssb_get_context(ssb1);
{
JSValue obj = JS_NewObject(context1);
JS_SetPropertyStr(context1, obj, "type", JS_NewString(context1, "contact"));
char self[k_id_base64_len];
tf_string_set(self, sizeof(self), id1);
char contact[k_id_base64_len];
snprintf(contact, sizeof(contact), "@%s", public[0]);
JS_SetPropertyStr(context1, obj, "contact", JS_NewString(context1, contact));
JS_SetPropertyStr(context1, obj, "following", JS_TRUE);
bool stored = false;
tf_printf("ssb1 %s following %s\n", self, contact);
JSValue signed_message = tf_ssb_sign_message(ssb1, self, priv1, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb1, signed_message, _message_stored, &stored);
JS_FreeValue(context1, signed_message);
_wait_stored(ssb1, &stored);
JS_FreeValue(context1, obj);
}
tf_printf("Waiting for blob.\n");
while (!tf_ssb_db_blob_get(ssb1, blob_id, NULL, NULL))
{
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_ONCE);
tf_ssb_set_main_thread(ssb1, false);
}
tf_printf("done\n");
tf_ssb_send_close(ssb1);
uv_close((uv_handle_t*)&idle0, NULL);
@@ -1364,12 +1553,6 @@ void tf_ssb_test_invite(const tf_test_options_t* options)
tf_ssb_release_db_writer(ssb0, writer);
tf_printf("invite: %s\n", invite);
int count0 = 0;
int count1 = 0;
tf_ssb_add_message_added_callback(ssb0, _message_added, NULL, &count0);
tf_ssb_add_message_added_callback(ssb1, _message_added, NULL, &count1);
tf_ssb_connect_str(ssb1, invite, 0, NULL, NULL);
tf_printf("Waiting for connection.\n");
@@ -1385,11 +1568,19 @@ void tf_ssb_test_invite(const tf_test_options_t* options)
tf_printf("waiting for messages\n");
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
while (count0 != 3 || count1 != 3)
int32_t sequence0 = 0;
int32_t sequence1 = 0;
while (sequence0 != 1 || sequence1 != 2)
{
uv_run(&loop, UV_RUN_ONCE);
tf_printf("count0=%d count1=%d\n", count0, count1);
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);
tf_ssb_db_get_latest_message_by_author(ssb0, id0, &sequence0, NULL, 0);
tf_ssb_db_get_latest_message_by_author(ssb1, id1, &sequence1, NULL, 0);
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
tf_printf("sequence0=%d sequence1=%d\n", sequence0, sequence1);
}
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);

View File

@@ -66,13 +66,19 @@ void tf_ssb_test_peer_exchange(const tf_test_options_t* options);
void tf_ssb_test_publish(const tf_test_options_t* options);
/**
** Test connecting by string.
** Test message and replication.
** @param options The test options.
*/
void tf_ssb_test_replicate(const tf_test_options_t* options);
/**
** Test invites.
** Test blob replication for a message received while already connected.
** @param options The test options.
*/
void tf_ssb_test_replicate_blob(const tf_test_options_t* options);
/**
** Test connecting by string.
** @param options The test options.
*/
void tf_ssb_test_connect_str(const tf_test_options_t* options);

View File

@@ -1,20 +1,18 @@
#include "task.h"
#include "api.js.h"
#include "bcrypt.js.h"
#include "database.js.h"
#include "file.js.h"
#include "http.h"
#include "httpd.js.h"
#include "log.h"
#include "mem.h"
#include "packetstream.h"
#include "serialize.h"
#include "socket.js.h"
#include "ssb.db.h"
#include "ssb.h"
#include "ssb.js.h"
#include "taskstub.js.h"
#include "tlscontext.js.h"
#include "trace.h"
#include "util.js.h"
#include "version.h"
@@ -28,8 +26,6 @@
#include "uv.h"
#include "zlib.h"
#include <openssl/crypto.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
@@ -709,11 +705,6 @@ static JSValue _tf_task_version(JSContext* context, JSValueConst this_val, int a
JS_SetPropertyStr(context, version, "name", JS_NewString(context, VERSION_NAME));
JS_SetPropertyStr(context, version, "libuv", JS_NewString(context, uv_version_string()));
JS_SetPropertyStr(context, version, "sqlite", JS_NewString(context, sqlite3_libversion()));
#if defined(OPENSSL_VERSION_STRING)
JS_SetPropertyStr(context, version, "openssl", JS_NewString(context, OpenSSL_version(OPENSSL_VERSION_STRING)));
#else
JS_SetPropertyStr(context, version, "openssl", JS_NewString(context, OpenSSL_version(OPENSSL_VERSION)));
#endif
const char* sodium_version_string();
JS_SetPropertyStr(context, version, "c-ares", JS_NewString(context, ares_version(NULL)));
JS_SetPropertyStr(context, version, "libsodium", JS_NewString(context, sodium_version_string()));
@@ -824,12 +815,8 @@ static JSValue _tf_task_getStats(JSContext* context, JSValueConst this_val, int
JS_SetPropertyStr(context, result, "sqlite3_memory_percent", JS_NewFloat64(context, 100.0 * tf_mem_get_sqlite_malloc_size() / total_memory));
JS_SetPropertyStr(context, result, "js_malloc_percent", JS_NewFloat64(context, 100.0 * tf_mem_get_js_malloc_size() / total_memory));
JS_SetPropertyStr(context, result, "uv_malloc_percent", JS_NewFloat64(context, 100.0 * tf_mem_get_uv_malloc_size() / total_memory));
JS_SetPropertyStr(context, result, "tls_malloc_percent", JS_NewFloat64(context, 100.0 * tf_mem_get_tls_malloc_size() / total_memory));
JS_SetPropertyStr(context, result, "tf_malloc_percent", JS_NewFloat64(context, 100.0 * tf_mem_get_tf_malloc_size() / total_memory));
JS_SetPropertyStr(context, result, "socket_count", JS_NewInt32(context, tf_socket_get_count()));
JS_SetPropertyStr(context, result, "socket_open_count", JS_NewInt32(context, tf_socket_get_open_count()));
if (task->_ssb)
{
tf_ssb_stats_t ssb_stats = { 0 };
@@ -1101,7 +1088,7 @@ void tf_task_on_receive_packet(int packetType, const char* begin, size_t length,
}
else
{
exit(1);
exit(EXIT_FAILURE);
}
break;
case kSetImports:
@@ -1666,8 +1653,6 @@ void tf_task_activate(tf_task_t* task)
sqlite3_open(task->_db_path, &task->_db);
JS_SetPropertyStr(context, global, "Task", tf_taskstub_register(context));
JS_SetPropertyStr(context, global, "Socket", tf_socket_register(context));
JS_SetPropertyStr(context, global, "TlsContext", tf_tls_context_register(context));
tf_file_register(context);
tf_database_register(context);
@@ -1699,7 +1684,7 @@ void tf_task_activate(tf_task_t* task)
else
{
tf_printf("Assignment missing '=': %s.\n", assignment);
exit(1);
exit(EXIT_FAILURE);
}
}
tf_free(copy);
@@ -1728,7 +1713,6 @@ void tf_task_activate(tf_task_t* task)
tf_trace_set_write_callback(task->_trace, _tf_task_trace_to_parent, task);
}
tf_bcrypt_register(context);
tf_util_register(context);
JS_SetPropertyStr(context, global, "exit", JS_NewCFunction(context, _tf_task_exit, "exit", 1));
JS_SetPropertyStr(context, global, "version", JS_NewCFunction(context, _tf_task_version, "version", 0));
@@ -1884,6 +1868,7 @@ void tf_task_destroy(tf_task_t* task)
tf_printf("--\n");
uv_print_all_handles(&task->_loop, stdout);
}
tf_http_debug_destroy();
uv_run(&task->_loop, UV_RUN_ONCE);
}
if (task->_trace)

View File

@@ -32,7 +32,7 @@
#include <TargetConditionals.h>
#endif
#define TEST_ARGS " --args=ssb_port=0,http_port=0,https_port=0"
#define TEST_ARGS " --args=ssb_port=0,http_port=0"
#if !TARGET_OS_IPHONE
static void _write_file(const char* path, const char* contents)
@@ -549,93 +549,6 @@ static void _test_float(const tf_test_options_t* options)
unlink("out/child.js");
}
static void _test_socket(const tf_test_options_t* options)
{
_write_file("out/test.js",
"'use strict';\n"
"\n"
"var s = new Socket();\n"
"print('connecting');\n"
"print('before connect', s.isConnected);\n"
"s.onError(function(e) {\n"
" print(e);\n"
"});\n"
"print('noDelay', s.noDelay);\n"
"s.noDelay = true;\n"
"s.connect('www.unprompted.com', 80).then(function() {\n"
" print('connected', 'www.unprompted.com', 80, s.isConnected);\n"
" print(s.peerName);\n"
" s.read(function(data) {\n"
" print('read', data ? data.length : null);\n"
" });\n"
" s.write('GET / HTTP/1.0\\r\\n\\r\\n');\n"
"}).then(function(e) {\n"
" print('closed 1');\n"
"});\n"
"\n"
"var s2 = new Socket();\n"
"print('connecting');\n"
"print('before connect', s2.isConnected);\n"
"s2.onError(function(e) {\n"
" print('error');\n"
" print(e);\n"
"});\n"
"print('noDelay', s2.noDelay);\n"
"s2.noDelay = true;\n"
"s2.connect('www.unprompted.com', 443).then(function() {\n"
" print('connected', 'www.unprompted.com', 443);\n"
" s2.read(function(data) {\n"
" print('read', data ? data.length : null);\n"
" });\n"
" return s2.startTls();\n"
"}).then(function() {\n"
" print('ready');\n"
" print(s2.peerName);\n"
" s2.write('GET / HTTP/1.0\\r\\nConnection: close\\r\\n\\r\\n').then(function() {\n"
" s2.shutdown();\n"
" });\n"
"}).catch(function(e) {\n"
" print('caught');\n"
" print(e);\n"
"});\n"
"var s3 = new Socket();\n"
"print('connecting s3');\n"
"print('before connect', s3.isConnected);\n"
"s3.onError(function(e) {\n"
" print('error');\n"
" print(e);\n"
"});\n"
"print('noDelay', s3.noDelay);\n"
"s3.noDelay = true;\n"
"s3.connect('0.0.0.0', 443).then(function() {\n"
" print('connected', '0.0.0.0', 443);\n"
" s3.read(function(data) {\n"
" print('read', data ? data.length : null);\n"
" });\n"
" return s3.startTls();\n"
"}).then(function() {\n"
" print('ready');\n"
" print(s3.peerName);\n"
" s3.write('GET / HTTP/1.0\\r\\nConnection: close\\r\\n\\r\\n').then(function() {\n"
" s3.shutdown();\n"
" });\n"
"}).catch(function(e) {\n"
" print('caught');\n"
" print(e);\n"
"});\n");
char command[256];
unlink("out/test_db0.sqlite");
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command);
int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WIFEXITED(result));
assert(WEXITSTATUS(result) == 0);
unlink("out/test.js");
}
static void _test_file(const tf_test_options_t* options)
{
_write_file("out/test.js",
@@ -781,7 +694,7 @@ static void _test_http(const tf_test_options_t* options)
tf_http_t* http = tf_http_create(&loop);
tf_http_add_handler(http, "/hello", _test_http_handler, NULL, NULL);
tf_http_add_handler(http, "/post", _test_http_handler_post, NULL, NULL);
tf_http_listen(http, 23456, true, NULL, NULL, NULL);
tf_http_listen(http, 23456, true, NULL, NULL);
test_http_t test = { .loop = &loop };
uv_async_init(&loop, &test.async, _test_http_async);
@@ -873,7 +786,7 @@ static void _test_httpd(const tf_test_options_t* options)
uv_spawn(&loop, &process,
&(uv_process_options_t) {
.file = options->exe_path,
.args = (char*[]) { (char*)options->exe_path, "run", "--db-path=out/test_db0.sqlite", "--args=ssb_port=0,http_port=8080,https_port=0", NULL },
.args = (char*[]) { (char*)options->exe_path, "run", "--db-path=out/test_db0.sqlite", "--args=ssb_port=0,http_port=8080", NULL },
.stdio_count = sizeof(stdio) / sizeof(*stdio),
.stdio = stdio,
});
@@ -1065,7 +978,6 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "icu", _test_icu, false);
_tf_test_run(options, "uint8array", _test_uint8array, false);
_tf_test_run(options, "float", _test_float, false);
_tf_test_run(options, "socket", _test_socket, false);
_tf_test_run(options, "file", _test_file, false);
_tf_test_run(options, "b64", _test_b64, false);
_tf_test_run(options, "rooms", tf_ssb_test_rooms, false);
@@ -1076,6 +988,7 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false);
_tf_test_run(options, "publish", tf_ssb_test_publish, false);
_tf_test_run(options, "replicate", tf_ssb_test_replicate, false);
_tf_test_run(options, "replicate_blob", tf_ssb_test_replicate_blob, false);
_tf_test_run(options, "connect_str", tf_ssb_test_connect_str, false);
_tf_test_run(options, "invite", tf_ssb_test_invite, false);
_tf_test_run(options, "triggers", tf_ssb_test_triggers, false);

384
src/tls.c
View File

@@ -1,384 +0,0 @@
#include "tls.h"
#include "mem.h"
#include <string.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>
typedef enum _direction_t
{
k_direction_undetermined,
k_direction_accept,
k_direction_connect,
} direction_t;
typedef struct _tf_tls_context_t
{
SSL_CTX* context;
} tf_tls_context_t;
typedef struct _tf_tls_session_t
{
tf_tls_context_t* context;
BIO* bio_in;
BIO* bio_out;
SSL* ssl;
const char* hostname;
direction_t direction;
} tf_tls_session_t;
tf_tls_context_t* tf_tls_context_create()
{
tf_tls_context_t* context = tf_malloc(sizeof(tf_tls_context_t));
memset(context, 0, sizeof(*context));
OPENSSL_init_ssl(0, NULL);
context->context = SSL_CTX_new(SSLv23_method());
SSL_CTX_set_default_verify_paths(context->context);
return context;
}
bool tf_tls_context_set_certificate(tf_tls_context_t* context, const char* certificate)
{
int result = 0;
BIO* bio = BIO_new(BIO_s_mem());
BIO_puts(bio, certificate);
X509* x509 = PEM_read_bio_X509(bio, 0, 0, 0);
result = SSL_CTX_use_certificate(context->context, x509);
X509_free(x509);
while (true)
{
x509 = PEM_read_bio_X509(bio, 0, 0, 0);
if (x509)
{
SSL_CTX_add_extra_chain_cert(context->context, x509);
/* Docs say don't x509_free: https://www.openssl.org/docs/man3.2/man3/SSL_CTX_add_extra_chain_cert.html. */
}
else
{
break;
}
}
BIO_free(bio);
return result == 1;
}
bool tf_tls_context_set_private_key(tf_tls_context_t* context, const char* private_key)
{
int result = 0;
BIO* bio = BIO_new(BIO_s_mem());
BIO_puts(bio, private_key);
EVP_PKEY* key = PEM_read_bio_PrivateKey(bio, 0, 0, 0);
result = SSL_CTX_use_PrivateKey(context->context, key);
EVP_PKEY_free(key);
BIO_free(bio);
return result == 1;
}
bool tf_tls_context_add_trusted_certificate(tf_tls_context_t* context, const char* certificate)
{
bool result = false;
BIO* bio = BIO_new_mem_buf(certificate, -1);
X509* x509 = PEM_read_bio_X509(bio, 0, 0, 0);
BIO_free(bio);
if (x509)
{
X509_STORE* store = SSL_CTX_get_cert_store(context->context);
if (store && X509_STORE_add_cert(store, x509) == 1)
{
result = true;
}
X509_free(x509);
}
return result;
}
tf_tls_session_t* tf_tls_context_create_session(tf_tls_context_t* context)
{
tf_tls_session_t* session = tf_malloc(sizeof(tf_tls_session_t));
memset(session, 0, sizeof(*session));
session->context = context;
session->bio_in = BIO_new(BIO_s_mem());
session->bio_out = BIO_new(BIO_s_mem());
return session;
}
void tf_tls_context_destroy(tf_tls_context_t* context)
{
SSL_CTX_free(context->context);
OPENSSL_cleanup();
tf_free(context);
}
void tf_tls_session_destroy(tf_tls_session_t* session)
{
if (session->ssl)
{
SSL_free(session->ssl);
}
if (session->hostname)
{
tf_free((void*)session->hostname);
}
tf_free(session);
}
void tf_tls_session_set_hostname(tf_tls_session_t* session, const char* hostname)
{
if (session->hostname)
{
tf_free((void*)session->hostname);
session->hostname = NULL;
}
if (hostname)
{
session->hostname = tf_strdup(hostname);
}
}
void tf_tls_session_start_accept(tf_tls_session_t* session)
{
session->direction = k_direction_accept;
session->ssl = SSL_new(session->context->context);
SSL_set_bio(session->ssl, session->bio_in, session->bio_out);
SSL_accept(session->ssl);
tf_tls_session_handshake(session);
}
void tf_tls_session_start_connect(tf_tls_session_t* session)
{
session->direction = k_direction_connect;
session->ssl = SSL_new(session->context->context);
X509_VERIFY_PARAM* param = SSL_get0_param(session->ssl);
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
X509_VERIFY_PARAM_set1_host(param, session->hostname, 0);
SSL_set_tlsext_host_name(session->ssl, session->hostname);
SSL_set_bio(session->ssl, session->bio_in, session->bio_out);
SSL_connect(session->ssl);
tf_tls_session_handshake(session);
}
void tf_tls_session_shutdown(tf_tls_session_t* session)
{
SSL_shutdown(session->ssl);
}
int tf_tls_session_get_peer_certificate(tf_tls_session_t* session, char* buffer, size_t bytes)
{
int result = -1;
#if OPENSSL_VERSION_NUMBER < 0x30000000L
X509* certificate = SSL_get_peer_certificate(session->ssl);
#else
X509* certificate = SSL_get1_peer_certificate(session->ssl);
#endif
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_X509(bio, certificate);
X509_free(certificate);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
if (mem->length <= bytes)
{
memcpy(buffer, mem->data, mem->length);
result = mem->length;
}
BIO_free(bio);
return result;
}
#if OPENSSL_VERSION_NUMBER < 0x10100000L
static bool _tls_session_wildcard_match(const char* pattern, size_t pattern_length, const char* name)
{
const char* it = pattern;
while (it - pattern < pattern_length && *name)
{
if (*it == '*')
{
for (const char* p = name; *p; ++p)
{
if (_tls_session_wildcard_match(it + 1, pattern_length - 1, p))
{
return true;
}
}
return false;
}
else if (tolower(*it) == tolower(*name))
{
++it;
++name;
}
else
{
break;
}
}
return it - pattern <= pattern_length && *name == 0;
}
static bool _tls_session_verify_hostname(X509* certificate, const char* hostname)
{
bool verified = false;
void* names = X509_get_ext_d2i(certificate, NID_subject_alt_name, 0, 0);
if (names)
{
int count = sk_GENERAL_NAME_num(names);
for (int i = 0; i < count; ++i)
{
const GENERAL_NAME* check = sk_GENERAL_NAME_value(names, i);
if (!verified)
{
#if OPENSSL_VERSION_NUMBER <= 0x1000211fL
const unsigned char* name = ASN1_STRING_data(check->d.ia5);
#else
const char* name = ASN1_STRING_get0_data(check->d.ia5);
#endif
size_t length = ASN1_STRING_length(check->d.ia5);
if (_tls_session_wildcard_match((const char*)name, length, hostname))
{
verified = true;
}
}
}
sk_GENERAL_NAMES_free(names);
}
if (!verified)
{
int index = X509_NAME_get_index_by_NID(X509_get_subject_name(certificate), NID_commonName, -1);
if (index >= 0)
{
X509_NAME_ENTRY* entry = X509_NAME_get_entry(X509_get_subject_name(certificate), index);
if (entry)
{
ASN1_STRING* asn1 = X509_NAME_ENTRY_get_data(entry);
if (asn1)
{
#if OPENSSL_VERSION_NUMBER <= 0x1000211fL
const unsigned char* commonName = ASN1_STRING_data(asn1);
#else
const char* commonName = ASN1_STRING_get0_data(asn1);
#endif
if ((size_t)(ASN1_STRING_length(asn1)) == strlen((const char*)commonName))
{
verified = _tls_session_wildcard_match((const char*)commonName, ASN1_STRING_length(asn1), hostname);
}
}
}
}
}
return verified;
}
#endif
static bool _tls_session_verify_peer_certificate(tf_tls_session_t* session)
{
bool verified = false;
#if OPENSSL_VERSION_NUMBER < 0x30000000L
X509* certificate = SSL_get_peer_certificate(session->ssl);
#else
X509* certificate = SSL_get1_peer_certificate(session->ssl);
#endif
if (certificate)
{
if (SSL_get_verify_result(session->ssl) == X509_V_OK)
{
#if OPENSSL_VERSION_NUMBER < 0x10100000L
if (_tls_session_verify_hostname(certificate, session->hostname))
{
verified = true;
}
#else
verified = true;
#endif
}
X509_free(certificate);
}
return verified;
}
tf_tls_handshake_t tf_tls_session_handshake(tf_tls_session_t* session)
{
tf_tls_handshake_t result = k_tls_handshake_done;
if (!SSL_is_init_finished(session->ssl))
{
int value = SSL_do_handshake(session->ssl);
if (value <= 0)
{
int error = SSL_get_error(session->ssl, value);
if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE)
{
result = k_tls_handshake_failed;
}
else
{
result = k_tls_handshake_more;
}
}
}
if (result == k_tls_handshake_done && session->direction == k_direction_connect && !_tls_session_verify_peer_certificate(session))
{
result = k_tls_handshake_failed;
}
return result;
}
int tf_tls_session_read_plain(tf_tls_session_t* session, char* buffer, size_t bytes)
{
int result = SSL_read(session->ssl, buffer, bytes);
if (result <= 0)
{
int error = SSL_get_error(session->ssl, result);
if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)
{
result = 0;
}
else if (error == SSL_ERROR_ZERO_RETURN)
{
if ((SSL_get_shutdown(session->ssl) & SSL_RECEIVED_SHUTDOWN) != 0)
{
result = k_tls_read_zero;
}
else
{
result = 0;
}
}
else
{
result = k_tls_read_failed;
}
}
return result;
}
int tf_tls_session_write_plain(tf_tls_session_t* session, const char* buffer, size_t bytes)
{
return SSL_write(session->ssl, buffer, bytes);
}
int tf_tls_session_read_encrypted(tf_tls_session_t* session, char* buffer, size_t bytes)
{
return BIO_read(session->bio_out, buffer, bytes);
}
int tf_tls_session_write_encrypted(tf_tls_session_t* session, const char* buffer, size_t bytes)
{
return BIO_write(session->bio_in, buffer, bytes);
}
bool tf_tls_session_get_error(tf_tls_session_t* session, char* buffer, size_t bytes)
{
unsigned long error = ERR_get_error();
if (error != 0)
{
ERR_error_string_n(error, buffer, bytes);
}
return error != 0;
}

177
src/tls.h
View File

@@ -1,177 +0,0 @@
#pragma once
/**
** \defgroup tls TLS
** A minimal wrapper around OpenSSL.
** @{
*/
#include <stdbool.h>
#include <stddef.h>
/**
** A TLS context. May have many tf_tls_session_t instances.
*/
typedef struct _tf_tls_context_t tf_tls_context_t;
/**
** A TLS session. Belongs to one tf_tls_context_t and represents a single connection.
*/
typedef struct _tf_tls_session_t tf_tls_session_t;
/**
** The state of a TLS handshake.
*/
typedef enum _tf_tls_handshake_t
{
k_tls_handshake_done,
k_tls_handshake_more,
k_tls_handshake_failed,
} tf_tls_handshake_t;
/**
** Possible error statuses from tf_tls_session_read_plain.
*/
typedef enum _tf_tls_read_t
{
k_tls_read_zero = -1,
k_tls_read_failed = -2,
} tf_tls_read_t;
/**
** Create a TLS context. Clean up with tf_tls_context_destroy().
** @return A new TLS context.
*/
tf_tls_context_t* tf_tls_context_create();
/**
** Set the TLS context's server certificate.
** @param context The TLS context.
** @param certificate The certificate in PEM format.
** @return true if set successfully.
*/
bool tf_tls_context_set_certificate(tf_tls_context_t* context, const char* certificate);
/**
** Set the TLS context's server certificate's private key.
** @param context The TLS context.
** @param private_key The private key in PEM format.
** @return true if set successfully.
*/
bool tf_tls_context_set_private_key(tf_tls_context_t* context, const char* private_key);
/**
** Add a trusted certificate.
** @param context The TLS context.
** @param certificate The certificate in PEM format.
** @return true if the certificate was added to the trusted list successfully.
*/
bool tf_tls_context_add_trusted_certificate(tf_tls_context_t* context, const char* certificate);
/**
** Create a TLS session from a context. Once created, call
** tf_tls_session_handshake() until it returns k_tls_handshake_done. Call
** tf_tls_session_[read/write]_[plain/encrypted]() as data is available.
** @param context The TLS context. Clean up with tf_tls_session_destroy().
** @return A new TLS session.
*/
tf_tls_session_t* tf_tls_context_create_session(tf_tls_context_t* context);
/**
** Destroy a TLS context.
** @param context The TLS contextx created by tf_tls_context_create().
*/
void tf_tls_context_destroy(tf_tls_context_t* context);
/**
** Destroy a TLS session.
** @param session A TLS sesssion created by tf_tls_context_create_session().
*/
void tf_tls_session_destroy(tf_tls_session_t* session);
/**
** Set the remote hostname for a session.
** @param session The TLS session.
** @param hostname The hostname.
*/
void tf_tls_session_set_hostname(tf_tls_session_t* session, const char* hostname);
/**
** Begin an outgoing TLS session.
** @param session The TLS session.
*/
void tf_tls_session_start_accept(tf_tls_session_t* session);
/**
** Begin an incoming TLS session.
** @param session The TLS session.
*/
void tf_tls_session_start_connect(tf_tls_session_t* session);
/**
** Begin the clean shutdown process for a TLS session.
** @param session The TLS session.
*/
void tf_tls_session_shutdown(tf_tls_session_t* session);
/**
** Get the certificate from the remote end of a TLS session if available.
** @param session The TLS session.
** @param buffer A buffer to receive the certificate.
** @param bytes The size of the buffer.
** @return The size of the returned certificate, or -1 on failure.
*/
int tf_tls_session_get_peer_certificate(tf_tls_session_t* session, char* buffer, size_t bytes);
/**
** Update the TLS handshake. Call repeatedly as new data is available until it returns done.
** @param session The TLS session.
** @return The current state of the handshake process.
*/
tf_tls_handshake_t tf_tls_session_handshake(tf_tls_session_t* session);
/**
** Read decrypted data from the TLS session.
** @param session The TLS session.
** @param buffer A buffer to receive the data.
** @param bytes The size of the buffer.
** @return The number of bytes returned.
*/
int tf_tls_session_read_plain(tf_tls_session_t* session, char* buffer, size_t bytes);
/**
** Write unencrypted data to the TLS session.
** @param session The TLS session.
** @param buffer The data to encrypt.
** @param bytes The size of the data.
** @return 1 on success, 0 on failure.
*/
int tf_tls_session_write_plain(tf_tls_session_t* session, const char* buffer, size_t bytes);
/**
** Read encrypted data from the TLS session that needs to be sent.
** @param session The TLS session.
** @param buffer A buffer to receive the data.
** @param bytes The size of the buffer.
** @return The number of bytes returned.
*/
int tf_tls_session_read_encrypted(tf_tls_session_t* session, char* buffer, size_t bytes);
/**
** Write encrypted data to the TLS session.
** @param session The TLS session.
** @param buffer The encrypted data.
** @param bytes The number of bytes written.
*/
int tf_tls_session_write_encrypted(tf_tls_session_t* session, const char* buffer, size_t bytes);
/**
** Retrieve the last error from a TLS session.
** @param session The TLS session.
** @param buffer A buffer to receive the error text.
** @param bytes The size of the buffer.
** @return true if an error was retrieved.
*/
bool tf_tls_session_get_error(tf_tls_session_t* session, char* buffer, size_t bytes);
/** @} */

View File

@@ -1,105 +0,0 @@
#include "tlscontext.js.h"
#include "log.h"
#include "mem.h"
#include "task.h"
#include "tls.h"
#include <stdlib.h>
#include <string.h>
static JSClassID _classId;
static int _count;
typedef struct _tf_tls_context_t
{
tf_tls_context_t* context;
tf_task_t* task;
JSValue object;
} tf_tls_context_t;
static JSValue _tls_context_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static void _tls_context_finalizer(JSRuntime* runtime, JSValue value);
static JSValue _tls_context_set_certificate(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_tls_context_t* tls = JS_GetOpaque(this_val, _classId);
const char* value = JS_ToCString(context, argv[0]);
tf_tls_context_set_certificate(tls->context, value);
JS_FreeCString(context, value);
return JS_UNDEFINED;
}
static JSValue _tls_context_set_private_key(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_tls_context_t* tls = JS_GetOpaque(this_val, _classId);
const char* value = JS_ToCString(context, argv[0]);
tf_tls_context_set_private_key(tls->context, value);
JS_FreeCString(context, value);
return JS_UNDEFINED;
}
static JSValue _tls_context_add_trusted_certificate(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_tls_context_t* tls = JS_GetOpaque(this_val, _classId);
const char* value = JS_ToCString(context, argv[0]);
tf_tls_context_add_trusted_certificate(tls->context, value);
JS_FreeCString(context, value);
return JS_UNDEFINED;
}
JSValue tf_tls_context_register(JSContext* context)
{
JS_NewClassID(&_classId);
JSClassDef def = {
.class_name = "TlsContext",
.finalizer = _tls_context_finalizer,
};
if (JS_NewClass(JS_GetRuntime(context), _classId, &def) != 0)
{
fprintf(stderr, "Failed to register TlsContext.\n");
}
return JS_NewCFunction2(context, _tls_context_create, "TlsContext", 0, JS_CFUNC_constructor, 0);
}
tf_tls_context_t* tf_tls_context_get(JSValue value)
{
tf_tls_context_t* tls = JS_GetOpaque(value, _classId);
return tls ? tls->context : NULL;
}
int tf_tls_context_get_count()
{
return _count;
}
static JSValue _tls_context_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_tls_context_t* tls = tf_malloc(sizeof(tf_tls_context_t));
memset(tls, 0, sizeof(*tls));
++_count;
tls->object = JS_NewObjectClass(context, _classId);
JS_SetOpaque(tls->object, tls);
JS_SetPropertyStr(context, tls->object, "setCertificate", JS_NewCFunction(context, _tls_context_set_certificate, "setCertificate", 1));
JS_SetPropertyStr(context, tls->object, "setPrivateKey", JS_NewCFunction(context, _tls_context_set_private_key, "setPrivateKey", 1));
JS_SetPropertyStr(context, tls->object, "addTrustedCertificate", JS_NewCFunction(context, _tls_context_add_trusted_certificate, "addTrustedCertificate", 1));
tls->context = tf_tls_context_create();
tls->task = tf_task_get(context);
return tls->object;
}
static void _tls_context_finalizer(JSRuntime* runtime, JSValue value)
{
tf_tls_context_t* tls = JS_GetOpaque(value, _classId);
if (tls->context)
{
tf_tls_context_destroy(tls->context);
tls->context = NULL;
}
--_count;
tf_free(tls);
}

View File

@@ -1,37 +0,0 @@
#pragma once
/**
** \defgroup tls_js TLS Interface
** Exposes \ref tls to JS.
** @{
*/
#include "quickjs.h"
/**
** A TLS context instance.
*/
typedef struct _tf_tls_context_t tf_tls_context_t;
/**
** Register TLS script interface.
** @param context The TLS context.
** @return the TlsContext constructor.
*/
JSValue tf_tls_context_register(JSContext* context);
/**
** Get a TLS context instance from its JS object.
** @param value A TlsContext JS object.
** @return The corresponding instance.
*/
tf_tls_context_t* tf_tls_context_get(JSValue value);
/**
** Get the number of active TLS context instances.
** @return The number of TlsContext objects created that have not been
** finalized.
*/
int tf_tls_context_get_count();
/** @} */

View File

@@ -253,66 +253,6 @@ bool tf_util_report_error(JSContext* context, JSValue value)
return is_error;
}
static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
int status = 0;
int minor_version = 0;
const char* message = NULL;
size_t message_length = 0;
struct phr_header headers[100];
size_t header_count = sizeof(headers) / sizeof(*headers);
int previous_length = 0;
JS_ToInt32(context, &previous_length, argv[1]);
JSValue buffer = JS_UNDEFINED;
size_t length;
uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[0]);
if (!array)
{
size_t offset;
size_t element_size;
buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &length, &element_size);
if (!JS_IsException(buffer))
{
array = tf_util_try_get_array_buffer(context, &length, buffer);
}
}
if (array)
{
int parse_result = phr_parse_response((const char*)array, length, &minor_version, &status, &message, &message_length, headers, &header_count, previous_length);
if (parse_result > 0)
{
result = JS_NewObject(context);
JS_SetPropertyStr(context, result, "bytes_parsed", JS_NewInt32(context, parse_result));
JS_SetPropertyStr(context, result, "minor_version", JS_NewInt32(context, minor_version));
JS_SetPropertyStr(context, result, "status", JS_NewInt32(context, status));
JS_SetPropertyStr(context, result, "message", JS_NewStringLen(context, message, message_length));
JSValue header_object = JS_NewObject(context);
for (int i = 0; i < (int)header_count; i++)
{
char name[256];
snprintf(name, sizeof(name), "%.*s", (int)headers[i].name_len, headers[i].name);
JS_SetPropertyStr(context, header_object, name, JS_NewStringLen(context, headers[i].value, headers[i].value_len));
}
JS_SetPropertyStr(context, result, "headers", header_object);
}
else
{
result = JS_NewInt32(context, parse_result);
}
}
else
{
result = JS_ThrowTypeError(context, "Could not convert argument to array.");
}
JS_FreeValue(context, buffer);
return result;
}
static const char* k_kind_name[] = {
[k_kind_bool] = "bool",
[k_kind_int] = "int",
@@ -349,7 +289,6 @@ static const setting_t k_settings[] = {
.description = "Whether to bind http(s) to the loopback address. Otherwise any.",
.default_value = { .kind = k_kind_bool, .bool_value = TF_IS_MOBILE ? true : false } },
{ .name = "http_port", .type = "integer", .description = "Port on which to listen for HTTP connections.", .default_value = { .kind = k_kind_int, .int_value = 12345 } },
{ .name = "https_port", .type = "integer", .description = "Port on which to listen for secure HTTP connections.", .default_value = { .kind = k_kind_int, .int_value = 0 } },
{ .name = "out_http_port_file", .type = "hidden", .description = "File to which to write bound HTTP port.", .default_value = { .kind = k_kind_string, .string_value = NULL } },
{ .name = "blob_fetch_age_seconds",
.type = "integer",
@@ -359,10 +298,6 @@ static const setting_t k_settings[] = {
.type = "integer",
.description = "Blobs older than this will be automatically deleted.",
.default_value = { .kind = k_kind_int, .int_value = TF_IS_MOBILE ? (int)(1.0f * 365 * 24 * 60 * 60) : -1 } },
{ .name = "fetch_hosts",
.type = "string",
.description = "Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.",
.default_value = { .kind = k_kind_string, .string_value = NULL } },
{ .name = "http_redirect",
.type = "string",
.description = "If connecting by HTTP and HTTPS is configured, Location header prefix (ie, \"http://example.com\")",
@@ -445,6 +380,31 @@ const char* tf_util_get_default_global_setting_string(const char* name)
return setting && setting->default_value.string_value ? setting->default_value.string_value : "";
}
bool tf_util_get_global_setting_by_index(int index, const char** out_name, const char** out_type, tf_setting_kind_t* out_kind, const char** out_description)
{
if (index >= 0 && index < tf_countof(k_settings))
{
if (out_name)
{
*out_name = k_settings[index].name;
}
if (out_type)
{
*out_type = k_settings[index].type;
}
if (out_kind)
{
*out_kind = k_settings[index].default_value.kind;
}
if (out_description)
{
*out_description = k_settings[index].description;
}
return true;
}
return false;
}
static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue settings = JS_NewObject(context);
@@ -523,7 +483,6 @@ void tf_util_register(JSContext* context)
JS_SetPropertyStr(context, global, "bip39Words", JS_NewCFunction(context, _util_bip39_words, "bip39Words", 1));
JS_SetPropertyStr(context, global, "bip39Bytes", JS_NewCFunction(context, _util_bip39_bytes, "bip39Bytes", 1));
JS_SetPropertyStr(context, global, "print", JS_NewCFunction(context, _util_print, "print", 1));
JS_SetPropertyStr(context, global, "parseHttpResponse", JS_NewCFunction(context, _util_parseHttpResponse, "parseHttpResponse", 2));
JS_SetPropertyStr(context, global, "defaultGlobalSettings", JS_NewCFunction(context, _util_defaultGlobalSettings, "defaultGlobalSettings", 2));
JS_FreeValue(context, global);
}

View File

@@ -212,6 +212,16 @@ const char* tf_util_get_default_global_setting_string(const char* name);
*/
tf_setting_kind_t tf_util_get_global_setting_kind(const char* name);
/**
** Get the index-th global setting.
** @param index The index.
** @param out_name Populated with the setting name.
** @param out_type Populated with the setting type.
** @param out_kind Populated with the setting kind.
** @param out_description Populated with the setting description.
*/
bool tf_util_get_global_setting_by_index(int index, const char** out_name, const char** out_type, tf_setting_kind_t* out_kind, const char** out_description);
/**
** Log documentation for the available settings.
** @param line_prefix Text to prefix each line with."

View File

@@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.2025.8"
#define VERSION_NUMBER "0.2025.10-wip"
#define VERSION_NAME "This program kills fascists."

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env bash
if [[ -z $BUILD_PLATFORM ]]; then
BUILD_PLATFORM=$(uname -s)
fi
if [[ -z $BUILD_TARGET ]]; then
BUILD_TARGET=$(uname -m)
WORK_DIR=out/openssl-local
else
WORK_DIR=out/openssl-$BUILD_PLATFORM-$BUILD_TARGET
if [[ -z $SSL_TARGET ]]; then
SSL_TARGET=linux-$BUILD_PLATFORM-$BUILD_TARGET
fi
fi
rm -rf $WORK_DIR
mkdir -p out/
cp -aRf deps/openssl_src/ $WORK_DIR
echo "Building"
pwd
pushd $WORK_DIR || exit 128
rm -rf $DESTDIR
echo $PATH
export GLOBAL_OPTIONS=" \
no-apps \
no-asm \
no-async \
no-autoerrinit \
no-autoload-config \
no-cmp \
no-cms \
no-comp \
no-deprecated \
no-dgram \
no-docs \
no-dsa \
no-dso \
no-dtls \
no-dtls1 \
no-dtls1-method \
no-dynamic-engine \
no-ec2m \
no-egd \
no-engine \
no-err \
no-filenames \
no-gost \
no-http \
no-idea \
no-legacy \
no-md2 \
no-md4 \
no-module \
no-multiblock \
no-nextprotoneg \
no-ocsp \
no-psk \
no-shared \
no-sock \
no-srp \
no-ssl \
no-ssl3 \
no-ssl-trace \
no-stdio \
no-tests \
no-thread-pool \
no-threads \
no-tls1 \
no-tls1-method \
no-trace \
no-ui-console \
no-uplink \
no-weak-ssl-ciphers \
no-whirlpool \
no-zlib \
-Os \
-DOPENSSL_SMALL_FOOTPRINT \
-Wno-error \
-ffunction-sections \
-fdata-sections \
--static \
-static \
"
pwd
echo "./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS" && \
./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS && \
make -s clean && \
make -s build_generated && \
make -s libcrypto.a libssl.a || exit 128
popd
echo WORK_DIR=$WORK_DIR
rm -rf out/openssl/$BUILD_PLATFORM/$BUILD_TARGET/
mkdir -p out/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/include/
mkdir -p out/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/lib/
cp -R $WORK_DIR/include/* out/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/include/
cp $WORK_DIR/*.a out/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/lib/
echo Success