summaryrefslogtreecommitdiffstats
path: root/wk.fo
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-16 23:01:43 +0300
committerAhmed <git@gumx.cc>2026-06-16 23:01:43 +0300
commit9c634e4f26b06cc9bfdb755b0c6084c937d8bed0 (patch)
treef8402bde9e0baf931056d0721a9a2c4d44f37b65 /wk.fo
parentb20ad8a1cfadfc0abe0e12f0848f74b712c2ef17 (diff)
add: lots of vibed edits again
Diffstat (limited to 'wk.fo')
-rw-r--r--wk.fo/body.html8
-rw-r--r--wk.fo/index.html10
-rw-r--r--wk.fo/pgp/cgi-bin/pgp-hkp.sh138
-rw-r--r--wk.fo/pgp/index.html61
4 files changed, 210 insertions, 7 deletions
diff --git a/wk.fo/body.html b/wk.fo/body.html
index 40ac3f3..158927c 100644
--- a/wk.fo/body.html
+++ b/wk.fo/body.html
@@ -1,14 +1,16 @@
-<p>File sharing, IRC bouncer, and VPN for friends. All services require an invitation.</p>
+<p>File sharing, IRC, VPN, and tools for friends. Invite required for most services.</p>
<h2>file sharing</h2>
<p>Upload via HTTPS (token required):</p>
<pre><code>curl -F "file=@photo.jpg" -H "Authorization: YOUR_TOKEN" https://wk.fo/</code></pre>
<p>Files expire after 24 hours by default (max 48h with <code>expires</code> param). Max 256 MB.</p>
<h2>irc</h2>
+<p><a href="/chat">web client</a> — no account needed. Or connect directly:</p>
<table>
<tr><th>server</th><td>wk.fo:6697 (TLS)</td></tr>
-<tr><th>type</th><td>Soju bouncer</td></tr>
+<tr><th>auth</th><td>SASL PLAIN (invite only)</td></tr>
</table>
-<p>Use the credentials provided to you on invite. Works with any IRC client that supports SASL PLAIN.</p>
+<h2>pgp</h2>
+<p>Curated keyserver — keys added after in-person verification. <a href="/pgp">search or browse</a>.</p>
<h2>vpn</h2>
<table>
<tr><th>server</th><td>wk.fo:51820 (UDP)</td></tr>
diff --git a/wk.fo/index.html b/wk.fo/index.html
index fbb74cb..af02a48 100644
--- a/wk.fo/index.html
+++ b/wk.fo/index.html
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
-<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAiIGhlaWdodD0iMTQwIiBzdHJva2U9IiMwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iMTIyIiBzdHJva2UtZGFzaGFycmF5PSIyLDM4IiBkPSJtOSw3MGgxMjJNNzAsOXYxMjIiLz48cGF0aCBzdHJva2Utd2lkdGg9IjMzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGQ9Im03MCwzMGgwbTQwLDQwaDBtLTgwLDQwdjBtNDAsMGgwbTQwLDBoMCIvPjwvc3ZnPg==">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>wk.fo</title>
<style>
@@ -36,17 +36,19 @@ a { color: inherit; }
<h1><a href="https://gumx.cc">gumx</a> / wk.fo</h1>
</header>
<main>
-<p>File sharing, IRC bouncer, and VPN for friends. All services require an invitation.</p>
+<p>File sharing, IRC, VPN, and tools for friends. Invite required for most services.</p>
<h2>file sharing</h2>
<p>Upload via HTTPS (token required):</p>
<pre><code>curl -F "file=@photo.jpg" -H "Authorization: YOUR_TOKEN" https://wk.fo/</code></pre>
<p>Files expire after 24 hours by default (max 48h with <code>expires</code> param). Max 256 MB.</p>
<h2>irc</h2>
+<p><a href="/chat">web client</a> — no account needed. Or connect directly:</p>
<table>
<tr><th>server</th><td>wk.fo:6697 (TLS)</td></tr>
-<tr><th>type</th><td>Soju bouncer</td></tr>
+<tr><th>auth</th><td>SASL PLAIN (invite only)</td></tr>
</table>
-<p>Use the credentials provided to you on invite. Works with any IRC client that supports SASL PLAIN.</p>
+<h2>pgp</h2>
+<p>Curated keyserver — keys added after in-person verification. <a href="/pgp">search or browse</a>.</p>
<h2>vpn</h2>
<table>
<tr><th>server</th><td>wk.fo:51820 (UDP)</td></tr>
diff --git a/wk.fo/pgp/cgi-bin/pgp-hkp.sh b/wk.fo/pgp/cgi-bin/pgp-hkp.sh
new file mode 100644
index 0000000..155c820
--- /dev/null
+++ b/wk.fo/pgp/cgi-bin/pgp-hkp.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+# HKP keyserver CGI for wk.fo/pks
+# Implements: GET /pks/lookup?op=get|index|vindex and POST /pks/add
+# Keys live in /var/pgp/keys/<FINGERPRINT_UPPERCASE>.asc
+
+KEYS_DIR=/var/pgp/keys
+
+FAVICON="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAiIGhlaWdodD0iMTQwIiBzdHJva2U9IiMwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iMTIyIiBzdHJva2UtZGFzaGFycmF5PSIyLDM4IiBkPSJtOSw3MGgxMjJNNzAsOXYxMjIiLz48cGF0aCBzdHJva2Utd2lkdGg9IjMzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGQ9Im03MCwzMGgwbTQwLDQwaDBtLTgwLDQwdjBtNDAsMGgwbTQwLDBoMCIvPjwvc3ZnPg=="
+
+CSS='@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; }
+@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; }
+* { unicode-bidi: plaintext; box-sizing: border-box; }
+html { color: black; background-color: white; }
+body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; }
+main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
+h1, header, footer { text-align: center; }
+main { text-align: justify; }
+p, h2, h3, h4 { margin: 1em 0 0 0; }
+table { margin: 1em auto; border-collapse: collapse; }
+th, td { border: 1px solid; padding: 0.3em 0.8em; }
+code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
+header { margin-bottom: 1em; }
+footer { margin-top: 3em; }
+a { color: inherit; }
+@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
+@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
+@media (prefers-color-scheme: dark) { html { filter: invert(1); } }'
+
+FOOTER='<footer>
+<hr>
+<a href="https://twt.gumx.cc">twt</a> /
+<a href="https://git.gumx.cc">git</a> /
+<a href="https://mail.gumx.cc">mail</a> /
+<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
+<a href="https://vpn.gumx.cc">vpn</a> /
+<a href="/pgp">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
+<a href="https://wk.fo">wk.fo</a>
+</footer>'
+
+qs_get() {
+ echo "$QUERY_STRING" \
+ | tr '&' '\n' \
+ | grep -E "^$1=" \
+ | head -1 \
+ | cut -d= -f2- \
+ | sed 's/+/ /g; s/%20/ /g'
+}
+
+op=$(qs_get op)
+search=$(qs_get search | sed 's/^0x//i; s/^0X//i')
+
+case "$REQUEST_METHOD" in
+ GET)
+ case "$op" in
+ get)
+ printf "Content-Type: text/plain\r\n\r\n"
+ FP="${search^^}"
+ keyfile="$KEYS_DIR/$FP.asc"
+ if [[ -f "$keyfile" ]]; then
+ cat "$keyfile"
+ else
+ printf "Error: No key found for %s\n" "$search"
+ fi
+ ;;
+ index)
+ printf "Content-Type: text/plain\r\n\r\n"
+ total=$(find "$KEYS_DIR" -name '*.asc' 2>/dev/null | wc -l)
+ echo "info:1:$total"
+ for f in "$KEYS_DIR"/*.asc; do
+ [[ -f "$f" ]] || continue
+ gpg --with-colons --import-options show-only \
+ --import < "$f" 2>/dev/null \
+ | awk -F: '/^pub/{print "pub:" $5 ":" $6 ":::" $10 "::"}'
+ done
+ ;;
+ vindex)
+ printf "Content-Type: text/html\r\n\r\n"
+ printf '<!DOCTYPE html>\n<html lang="en">\n<head>\n'
+ printf '<meta charset="utf-8">\n'
+ printf '<link rel="icon" type="image/svg+xml" href="%s">\n' "$FAVICON"
+ printf '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
+ printf '<title>wk.fo / pgp / keys</title>\n'
+ printf '<style>\n%s\n</style>\n' "$CSS"
+ printf '</head>\n<body>\n'
+ printf '<header>\n<h1><a href="https://wk.fo">wk.fo</a> / <a href="/pgp">pgp</a> / keys</h1>\n</header>\n'
+ printf '<main>\n<table>\n<tr><th>fingerprint</th><th>uid</th></tr>\n'
+ found=0
+ for f in "$KEYS_DIR"/*.asc; do
+ [[ -f "$f" ]] || continue
+ found=1
+ FP=$(basename "$f" .asc)
+ KEY_UID=$(gpg --with-colons --import-options show-only --import < "$f" 2>/dev/null \
+ | awk -F: '/^uid/{print $10; exit}')
+ printf '<tr><td><a href="/pks/lookup?op=get&amp;search=0x%s"><code>%s</code></a></td><td>%s</td></tr>\n' \
+ "$FP" "$FP" "$KEY_UID"
+ done
+ [[ $found -eq 0 ]] && printf '<tr><td colspan="2">no keys</td></tr>\n'
+ printf '</table>\n</main>\n'
+ printf '%s\n' "$FOOTER"
+ printf '</body>\n</html>\n'
+ ;;
+ *)
+ printf "Content-Type: text/plain\r\n\r\n"
+ echo "Error: unknown op"
+ ;;
+ esac
+ ;;
+
+ POST)
+ printf "Content-Type: text/plain\r\n\r\n"
+ read -r -d '' -n "${CONTENT_LENGTH:-4096}" SUBMITTED_KEY
+
+ TMPDIR_GPG=$(mktemp -d)
+ FINGERPRINT=$(echo "$SUBMITTED_KEY" \
+ | gpg --homedir "$TMPDIR_GPG" \
+ --with-colons --import-options show-only \
+ --import 2>/dev/null \
+ | awk -F: '/^fpr/{print $10; exit}')
+ rm -rf "$TMPDIR_GPG"
+
+ if [[ -z "$FINGERPRINT" ]]; then
+ echo "Error: Could not parse submitted key"
+ exit 0
+ fi
+
+ KEYFILE="$KEYS_DIR/${FINGERPRINT^^}.asc"
+ if [[ -f "$KEYFILE" ]]; then
+ echo "$SUBMITTED_KEY" > "$KEYFILE"
+ echo "Key updated successfully"
+ else
+ echo "Error: Fingerprint not registered."
+ echo "Contact hi@gumx.cc to arrange a key signing meeting."
+ fi
+ ;;
+esac
diff --git a/wk.fo/pgp/index.html b/wk.fo/pgp/index.html
new file mode 100644
index 0000000..2847b9f
--- /dev/null
+++ b/wk.fo/pgp/index.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAiIGhlaWdodD0iMTQwIiBzdHJva2U9IiMwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iMTIyIiBzdHJva2UtZGFzaGFycmF5PSIyLDM4IiBkPSJtOSw3MGgxMjJNNzAsOXYxMjIiLz48cGF0aCBzdHJva2Utd2lkdGg9IjMzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGQ9Im03MCwzMGgwbTQwLDQwaDBtLTgwLDQwdjBtNDAsMGgwbTQwLDBoMCIvPjwvc3ZnPg==">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>wk.fo / pgp</title>
+<style>
+@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; }
+@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; }
+* { unicode-bidi: plaintext; box-sizing: border-box; }
+html { color: black; background-color: white; }
+body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; }
+main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
+h1, header, footer { text-align: center; }
+main { text-align: justify; }
+p, h2, h3, h4 { margin: 1em 0 0 0; }
+pre { margin: 1em 0; }
+pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
+code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
+header { margin-bottom: 1em; }
+footer { margin-top: 3em; }
+a { color: inherit; }
+input[type="text"], button { font-family: inherit; font-size: inherit; border: thin solid; padding: 0.3em 0.6em; background: transparent; color: inherit; }
+button { cursor: pointer; }
+@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
+@media (prefers-color-scheme: dark) { html { filter: invert(1); } }
+</style>
+</head>
+<body>
+<header>
+<h1><a href="https://wk.fo">wk.fo</a> / pgp</h1>
+</header>
+<main>
+<p>Curated HKP keyserver. Keys are added after in-person verification only.</p>
+<h2>search</h2>
+<form action="/pks/lookup" method="get">
+<input type="hidden" name="op" value="get">
+<p><input type="text" name="search" placeholder="0x fingerprint"> <button type="submit">lookup</button></p>
+</form>
+<p><a href="/pks/lookup?op=vindex&amp;search=">browse all keys</a></p>
+<h2>via gpg</h2>
+<pre><code>gpg --keyserver hkps://wk.fo --recv-keys &lt;fingerprint&gt;</code></pre>
+<h2>submit</h2>
+<p>Keys are not accepted without prior arrangement. Contact <a href="mailto:hi@gumx.cc">hi@gumx.cc</a>.</p>
+</main>
+<footer>
+<hr>
+<a href="https://twt.gumx.cc">twt</a> /
+<a href="https://git.gumx.cc">git</a> /
+<a href="https://mail.gumx.cc">mail</a> /
+<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
+<a href="https://vpn.gumx.cc">vpn</a> /
+<a href="/pgp">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
+<a href="https://wk.fo">wk.fo</a>
+</footer>
+</body>
+</html>