From: Thomas Walker Lynch Date: Tue, 13 Oct 2026 09:30:31 +0000 (+0000) Subject: . X-Git-Url: https://git.reasoningtechnology.com/%27%20%20%20window.RT.dirpr_library%20%20%20%27/%28%5B%5E?a=commitdiff_plain;h=b50f79fa9f1ad0925be65363b9a528fb986852c4;p=RT-Style . --- diff --git a/developer/authored/Manuscript.copy/server/RT-Style-server.js b/developer/authored/Manuscript.copy/server/RT-Style-server.js new file mode 100644 index 0000000..f2b38d8 --- /dev/null +++ b/developer/authored/Manuscript.copy/server/RT-Style-server.js @@ -0,0 +1,258 @@ +#!/usr/bin/env node +// -*- mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- +/* +RT-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: node RT-Style-server.js ... + + 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(`RT-Style-server: serving ${root_real} on port ${port}`); + + const stop = () => { + console.log("RT-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(`RT-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/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/server.js b/developer/authored/Manuscript.copy/server/server.js deleted file mode 100644 index f2b38d8..0000000 --- a/developer/authored/Manuscript.copy/server/server.js +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env node -// -*- mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- -/* -RT-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: node RT-Style-server.js ... - - 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(`RT-Style-server: serving ${root_real} on port ${port}`); - - const stop = () => { - console.log("RT-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(`RT-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 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