From b50f79fa9f1ad0925be65363b9a528fb986852c4 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Tue, 13 Oct 2026 09:30:31 +0000 Subject: [PATCH] . --- .../server/{server.js => RT-Style-server.js} | 0 .../server/RT-Style-server.service | 52 ++++ .../Manuscript.copy/server/style-server | 258 ------------------ .../server/style-server.service | 39 --- 4 files changed, 52 insertions(+), 297 deletions(-) rename developer/authored/Manuscript.copy/server/{server.js => RT-Style-server.js} (100%) create mode 100644 developer/authored/Manuscript.copy/server/RT-Style-server.service delete mode 100644 developer/authored/Manuscript.copy/server/style-server delete mode 100644 developer/authored/Manuscript.copy/server/style-server.service diff --git a/developer/authored/Manuscript.copy/server/server.js b/developer/authored/Manuscript.copy/server/RT-Style-server.js similarity index 100% rename from developer/authored/Manuscript.copy/server/server.js rename to developer/authored/Manuscript.copy/server/RT-Style-server.js diff --git a/developer/authored/Manuscript.copy/server/RT-Style-server.service b/developer/authored/Manuscript.copy/server/RT-Style-server.service new file mode 100644 index 0000000..a499440 --- /dev/null +++ b/developer/authored/Manuscript.copy/server/RT-Style-server.service @@ -0,0 +1,52 @@ +# /etc/systemd/system/RT-Style-server.service +# +# Serves the RT-Style library at style.reasoningtechnology.com. +# +# /srv/style.reasoningtechnology.com is a read-only bindfs view of +# ~Thomas_developer/project/RT-Style/consumer +# made by /etc/fstab, in which files owned by Thomas_developer appear owned by +# web. The library and the server's own code both arrive through that view: +# /srv/style.reasoningtechnology.com/Manuscript/... the library +# /srv/style.reasoningtechnology.com/Manuscript/server/server.js this server +# A promote replaces the files on disk; node reads server.js only when the +# service starts, so new server code runs only after a restart by root. + +[Unit] +Description=RT-Style HTTPS server (style.reasoningtechnology.com) +After=network-online.target +Wants=network-online.target +# Start only after the bindfs view is mounted, and stop if it goes away. +RequiresMountsFor=/srv/style.reasoningtechnology.com + +[Service] +User=web +Group=web + +# systemd copies these into a directory only this service can read, named by +# %d below. The key itself stays root-only in /etc/ssl/private. +LoadCredential=cert:/etc/ssl/certs/style.reasoningtechnology.com.pem +LoadCredential=key:/etc/ssl/private/style.reasoningtechnology.com.key + +ExecStart=/usr/bin/node /srv/style.reasoningtechnology.com/Manuscript/server/server.js serve /srv/style.reasoningtechnology.com %d/cert %d/key 443 +Restart=on-failure +RestartSec=5 + +# The one privilege it needs: binding to a port below 1024. +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +NoNewPrivileges=yes + +# The whole file system is read-only to it, and /home, /root and /run/user +# are hidden; it writes nothing. +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +UMask=0077 + +[Install] +WantedBy=multi-user.target diff --git a/developer/authored/Manuscript.copy/server/style-server b/developer/authored/Manuscript.copy/server/style-server deleted file mode 100644 index 83f1959..0000000 --- a/developer/authored/Manuscript.copy/server/style-server +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env node -// -*- mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- -/* -style-server - -Interim HTTPS server for the RT-Style library at style.reasoningtechnology.com. -Serves static files from one directory. It is meant to sit behind Cloudflare, -which connects to it with Full (strict) TLS using the Cloudflare origin -certificate. It is to be replaced by a server built on the TM library. - -Usage: style-server ... - - help | -h | --help print this message (also printed when given no arguments) - check load the certificate and key, check the root directory, serve nothing - serve [port] serve over HTTPS on [port], 443 when not given - - directory whose files are served; a request for /a/b.js is answered from /a/b.js - PEM certificate file - PEM private key file - -What it does for each request: - only GET and HEAD are answered; other methods get 405 - the path is decoded and resolved inside ; any path, or symbolic link, - that would lead outside gets 404 - a directory is answered with its index.html, after a redirect that adds a - trailing slash to the address - files are sent with a content type chosen by extension, Cache-Control, - Access-Control-Allow-Origin "*", and Last-Modified; a request carrying - If-Modified-Since for an unchanged file gets 304 and no body - -One log line per request goes to standard output: client address, method, -path, status, bytes. Behind Cloudflare the client address is taken from the -CF-Connecting-IP header. -*/ - -const https = require("https"); -const fs = require("fs"); -const fsp = require("fs/promises"); -const path = require("path"); - -const CACHE_CONTROL = "public, max-age=3600"; - -const CONTENT_TYPE = { - ".html": "text/html; charset=utf-8" - ,".js": "text/javascript; charset=utf-8" - ,".mjs": "text/javascript; charset=utf-8" - ,".css": "text/css; charset=utf-8" - ,".json": "application/json; charset=utf-8" - ,".txt": "text/plain; charset=utf-8" - ,".svg": "image/svg+xml" - ,".png": "image/png" - ,".jpg": "image/jpeg" - ,".jpeg": "image/jpeg" - ,".gif": "image/gif" - ,".ico": "image/x-icon" - ,".woff": "font/woff" - ,".woff2": "font/woff2" - ,".ttf": "font/ttf" - ,".otf": "font/otf" - ,".pdf": "application/pdf" -}; - -// ---------------------------------------------------------------- work - -// Resolves a URL path to a real file path inside root_real, or null. -// root_real is the root directory with its own symbolic links resolved. -async function resolve_inside(root_real ,url_path){ - let decoded; - try{ decoded = decodeURIComponent(url_path); } - catch(e){ return null; } // malformed %-encoding - if(decoded.includes("\0")) return null; - - // Resolve lexically first: "." + "/a/../b" stays under root_real. - const candidate = path.resolve(root_real ,"." + path.posix.normalize("/" + decoded)); - if(!is_inside(root_real ,candidate)) return null; - - // Then follow symbolic links, and check again. - let real; - try{ real = await fsp.realpath(candidate); } - catch(e){ return null; } // does not exist - if(!is_inside(root_real ,real)) return null; - return real; -} - -function is_inside(root ,p){ - return p === root || p.startsWith(root + path.sep); -} - -// Read when the request arrives: by the time a response is logged, the -// connection may already be gone, and its address with it. -function client_address(request){ - return request.headers["cf-connecting-ip"] || request.socket.remoteAddress || "-"; -} - -function log_line(request ,status ,bytes){ - const when = new Date().toISOString(); - console.log(`${when} ${request.rt_client} ${request.method} ${request.url} ${status} ${bytes}`); -} - -function send_status(request ,response ,status ,extra_headers = {}){ - const body = status + "\n"; - response.writeHead(status ,{ - "Content-Type": "text/plain; charset=utf-8" - ,"Content-Length": Buffer.byteLength(body) - ,"Cache-Control": "no-store" - ,...extra_headers - }); - response.end(request.method === "HEAD" ? undefined : body); - log_line(request ,status ,0); -} - -async function handle(root_real ,request ,response){ - request.rt_client = client_address(request); - if(request.method !== "GET" && request.method !== "HEAD"){ - send_status(request ,response ,405 ,{"Allow": "GET, HEAD"}); - return; - } - - // Only the path part of the address is used; any ?query is ignored. - const url_path = (request.url || "/").split("?")[0].split("#")[0]; - let file = await resolve_inside(root_real ,url_path); - if(file === null){ send_status(request ,response ,404); return; } - - let stat = await fsp.stat(file); - if(stat.isDirectory()){ - if(!url_path.endsWith("/")){ - send_status(request ,response ,301 ,{"Location": url_path + "/"}); - return; - } - file = await resolve_inside(root_real ,url_path + "index.html"); - if(file === null){ send_status(request ,response ,404); return; } - stat = await fsp.stat(file); - } - if(!stat.isFile()){ send_status(request ,response ,404); return; } - - const headers = { - "Content-Type": CONTENT_TYPE[path.extname(file).toLowerCase()] || "application/octet-stream" - ,"Cache-Control": CACHE_CONTROL - ,"Access-Control-Allow-Origin": "*" - ,"X-Content-Type-Options": "nosniff" - ,"Last-Modified": stat.mtime.toUTCString() - }; - - // HTTP dates have whole-second precision, so compare whole seconds. - const since = Date.parse(request.headers["if-modified-since"] || ""); - if(!Number.isNaN(since) && Math.floor(stat.mtimeMs / 1000) * 1000 <= since){ - response.writeHead(304 ,headers); - response.end(); - log_line(request ,304 ,0); - return; - } - - headers["Content-Length"] = stat.size; - response.writeHead(200 ,headers); - if(request.method === "HEAD"){ - response.end(); - log_line(request ,200 ,0); - return; - } - // Log once, when the response finishes or the connection closes, whichever - // comes first. A client that has received all Content-Length bytes may hang - // up before the response formally ends, so a response counts as complete - // when the whole file was read and passed on, and as aborted otherwise. - const stream = fs.createReadStream(file); - let logged = false; - const log_once = () => { - if(logged) return; - logged = true; - log_line(request ,200 ,stream.bytesRead === stat.size ? stat.size : `aborted@${stream.bytesRead}`); - }; - response.on("finish" ,log_once); - response.on("close" ,log_once); - stream.on("error" ,() => response.destroy()); - stream.pipe(response); -} - -function load_tls(cert_path ,key_path){ - return { - cert: fs.readFileSync(cert_path) - ,key: fs.readFileSync(key_path) - ,minVersion: "TLSv1.2" - }; -} - -async function check(root ,cert_path ,key_path){ - const root_real = await fsp.realpath(root); - if(!(await fsp.stat(root_real)).isDirectory()) throw new Error(`${root} is not a directory`); - const tls = load_tls(cert_path ,key_path); - // Building a secure context fails if the key does not match the certificate. - require("tls").createSecureContext(tls); - return root_real; -} - -async function serve(root ,cert_path ,key_path ,port){ - const root_real = await check(root ,cert_path ,key_path); - const server = https.createServer(load_tls(cert_path ,key_path) ,(request ,response) => { - handle(root_real ,request ,response).catch(e => { - console.error(`error: ${request.url}: ${e.message}`); - if(!response.headersSent) send_status(request ,response ,500); - else response.destroy(); - }); - }); - server.on("tlsClientError" ,() => {}); // scanners and broken clients; not worth a log line - await new Promise((ok ,fail) => { server.once("error" ,fail); server.listen(port ,ok); }); - console.log(`style-server: serving ${root_real} on port ${port}`); - - const stop = () => { - console.log("style-server: stopping"); - server.close(() => process.exit(0)); - setTimeout(() => process.exit(0) ,5000).unref(); - }; - process.on("SIGTERM" ,stop); - process.on("SIGINT" ,stop); -} - -// ---------------------------------------------------------------- command line - -function print_help(){ - const source = fs.readFileSync(__filename ,"utf8"); - const start = source.indexOf("/*\n") + 3; - console.log(source.slice(start ,source.indexOf("*/" ,start)).trim()); - return 0; -} - -async function CLI(argv){ - if(argv.length === 0 || ["help" ,"-h" ,"--help"].includes(argv[0])) return print_help(); - const [command ,...rest] = argv; - - if(command === "check"){ - if(rest.length !== 3){ console.error("Error: check needs ."); return 1; } - const root_real = await check(...rest); - console.log(`style-server: ok: root ${root_real}, certificate and key load and match`); - return 0; - } - - if(command === "serve"){ - if(rest.length !== 3 && rest.length !== 4){ - console.error("Error: serve needs [port]."); - return 1; - } - const port = rest.length === 4 ? Number(rest[3]) : 443; - if(!Number.isInteger(port) || port < 1 || port > 65535){ - console.error(`Error: bad port '${rest[3]}'.`); - return 1; - } - await serve(rest[0] ,rest[1] ,rest[2] ,port); - return null; // keep running - } - - console.error(`Error: unknown command '${command}'.\n`); - print_help(); - return 1; -} - -CLI(process.argv.slice(2)).then( - code => { if(code !== null) process.exit(code); } - ,e => { console.error(`Error: ${e.message}`); process.exit(1); } -); diff --git a/developer/authored/Manuscript.copy/server/style-server.service b/developer/authored/Manuscript.copy/server/style-server.service deleted file mode 100644 index c944792..0000000 --- a/developer/authored/Manuscript.copy/server/style-server.service +++ /dev/null @@ -1,39 +0,0 @@ -# /etc/systemd/system/style-server.service -# -# Runs style-server as an unprivileged, throwaway user, with only the one -# privilege it needs: binding to port 443. - -[Unit] -Description=RT-Style interim HTTPS server (style.reasoningtechnology.com) -After=network-online.target -Wants=network-online.target - -[Service] -# systemd copies these into a private directory that only this service can -# read, named by %d below. The key itself stays root-only in /etc/ssl/private. -LoadCredential=cert:/etc/ssl/certs/style.reasoningtechnology.com.pem -LoadCredential=key:/etc/ssl/private/style.reasoningtechnology.com.key - -ExecStart=/usr/local/bin/style-server serve /srv/style %d/cert %d/key 443 -Restart=on-failure -RestartSec=5 - -# A user made up for this service, existing only while it runs. -DynamicUser=yes -# The one privilege it needs: binding to a port below 1024. -AmbientCapabilities=CAP_NET_BIND_SERVICE -CapabilityBoundingSet=CAP_NET_BIND_SERVICE -NoNewPrivileges=yes - -# It reads /srv/style and nothing else of note; it writes nothing. -ProtectSystem=strict -ProtectHome=yes -PrivateTmp=yes -PrivateDevices=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictAddressFamilies=AF_INET AF_INET6 - -[Install] -WantedBy=multi-user.target -- 2.20.1