changeset 2122:ce75c0136e28 default tip

merge
author Franklin Schmidt <fschmidt@gmail.com>
date Wed, 07 Jan 2026 14:00:43 -0700
parents 454bc5a2ba10 (current diff) 8311ddedf344 (diff)
children
files
diffstat 17 files changed, 416 insertions(+), 363 deletions(-) [+]
line wrap: on
line diff
--- a/host/doc/install.txt	Thu Dec 11 13:17:11 2025 -0700
+++ b/host/doc/install.txt	Wed Jan 07 14:00:43 2026 -0700
@@ -25,6 +25,7 @@
 6) Make sudo nginx without password
   add string to /etc/sudoers
   %admin ALL=(ALL) NOPASSWD: /usr/local/bin/nginx
+  (requires user to be part of group "admin")
 
 7) compile
   ./update.sh
--- a/host/local_https.sh	Thu Dec 11 13:17:11 2025 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-#!/bin/bash
-set -e
-
-DOMAIN=$1
-
-cd sites/$DOMAIN
-
-openssl req -x509 -newkey rsa:2048 -nodes -keyout "$DOMAIN.key" -out fullchain.cer -days 365 \
-  -subj "/CN=$DOMAIN" \
-  -addext "subjectAltName=DNS:$DOMAIN,IP:127.0.0.1"
--- a/host/macos/renewSsl.plist	Thu Dec 11 13:17:11 2025 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-    <dict>
-        <key>Label</key>
-            <string>com.luanhost.renewSsl</string>
-        <key>ProgramArguments</key>
-        <array>
-            <string>ROOT/renewSsl.sh</string>
-            <string>ROOT</string>
-        </array>
-        <key>StartCalendarInterval</key>
-        <array>
-            <dict>
-                <key>Day</key>
-                <integer>1</integer>
-                <key>Hour</key>
-                <integer>00</integer>
-                <key>Minute</key>
-                <integer>00</integer>
-            </dict>
-        </array>
-        <key>AbandonProcessGroup</key>
-            <true/>
-        <key>UserName</key>
-            <string>USER</string>
-        <key>StandardErrorPath</key>
-            <string>LOG</string>
-        <key>StandardOutPath</key>
-            <string>LOG</string>
-        <key>RunAtLoad</key><false/>
-        <key>KeepAlive</key><false/>
-    </dict>
-</plist>
--- a/host/renewSsl.sh	Thu Dec 11 13:17:11 2025 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-#!/bin/bash
-set -e
-
-# for now - fschmidt
-echo 'totally fucked up'
-exit 1
-
-cd "$1" || exit 1
-
-ROOTPWD=$(pwd)
-KEYFILE="$ROOTPWD/local/tiny_account.key"
-for SITEROOT in "$ROOTPWD"/sites/*; do
-  {
-    # Skip if not a directory
-    [ -d "$SITEROOT" ] || continue
-
-    DOMAIN=$(basename "$SITEROOT")
-    CSRFILE="$SITEROOT/$DOMAIN.csr"
-    FULLCHAIN="$SITEROOT/fullchain.cer"
-    CHALLENGEDIR="$SITEROOT/site/.well-known/acme-challenge"
-    TMPOUT="/tmp/$DOMAIN.crt"
-    echo "Processing domain: $DOMAIN"
-
-    # local_https.sh does not create a csr file, assume
-    # it is a self-signed local cert if it doesn't exist
-    if [ ! -f "$CSRFILE" ]; then
-      echo "CSR file not found, assuming self-signed and skipping."
-      continue
-    fi
-
-    mkdir -p "$CHALLENGEDIR"
-
-    "$ROOTPWD/acme_tiny" \
-      --account-key "$KEYFILE" \
-      --csr "$CSRFILE" \
-      --acme-dir "$CHALLENGEDIR" \
-      > "$TMPOUT"
-
-    # check if exists
-    if [ -f "$FULLCHAIN" ]; then
-      mv $FULLCHAIN "$FULLCHAIN.old"
-    fi
-
-    mv "$TMPOUT" "$FULLCHAIN"
-
-    echo "Renewed certificate for $DOMAIN"
-  } || {
-    echo "Error processing $SITEROOT — skipping."
-  }
-done
-
-sudo /usr/local/bin/nginx -s reload -c "$(pwd)/local/nginx.conf"
-echo "Nginx reloaded."
--- a/host/startup/nginx/nginx.acme_setup.conf.luan	Thu Dec 11 13:17:11 2025 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-local rootDir, domain = ...
-
-%>
-	# This config exists to serve up acme challenges on
-	# .well-known for initial domain verification by letsencrypt.
-	# see set_https in luan/src/luan/host/https.luan for more.
-	server {
-		server_name <%=domain%>;
-		listen 80;
-		listen [::]:80;
-
-		error_log <%=rootDir%>/error.log;
-		access_log <%=rootDir%>/access.log;
-
-		root <%=rootDir%>;
-		index index.html;
-
-		location / {
-				try_files $uri $uri/ =404;
-		}
-	}
-
-<%
-
--- a/host/startup/nginx/nginx.conf.luan	Thu Dec 11 13:17:11 2025 -0700
+++ b/host/startup/nginx/nginx.conf.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -1,12 +1,12 @@
 local rootDir, user, group = ...
 
 %>
-worker_processes  4;
+worker_processes	4;
 user <%=user%> <%=group%>;
-pid <%=rootDir%>/local/nginx.pid;
+pid /var/run/luanhost_nginx.pid;
 
 events {
-	worker_connections  4096;
+	worker_connections	4096;
 }
 
 http { 
@@ -25,9 +25,17 @@
 		listen 80 default_server;
 		listen [::]:80 default_server;
 		include nginx.default.conf;
+
+		location /.well-known/acme-challenge/ {
+			# $host/ssl does not exist for non-ssl sites and requests to here
+			# will fail with 404 for those sites, which is what we want
+			alias <%=rootDir%>/sites/$host/ssl/acme-challenge/;
+			autoindex on;
+		}
 	}
 
-	include <%=rootDir%>/sites/*/nginx.ssl.conf;
-	include /tmp/acme_setup/*/nginx.acme_setup.conf;
+	# glob pattern returns no results for site dirs that don't have 
+	# the ssl/ subdir, so this is ok
+	include <%=rootDir%>/sites/*/ssl/nginx.ssl.conf;
 }
 <%
--- a/host/startup/nginx/nginx.ssl.conf.luan	Thu Dec 11 13:17:11 2025 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-local rootDir, domain = ...
-
-%>
-	server {
-		server_name <%=domain%>;
-		listen 80;
-		listen [::]:80;
-		return 301 https://$http_host$request_uri;
-	}
-
-	server {
-		server_name <%=domain%>;
-		listen 443 ssl;
-		listen [::]:443 ssl;
-
-		if ($host != $server_name) {
-			return 301 http://$http_host$request_uri;
-		}
-
-		ssl_certificate <%=rootDir%>/sites/<%=domain%>/fullchain.cer;
-		ssl_certificate_key <%=rootDir%>/sites/<%=domain%>/<%=domain%>.key;
-		include <%=rootDir%>/sites/<%=domain%>/site/nginx.*.conf;
-		include <%=rootDir%>/local/nginx.default.conf;
-	}
-<%
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/host/test/test_delete_junk.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -0,0 +1,16 @@
+local Luan = require("luan:Luan.luan")
+local error = Luan.error
+local Io = require("luan:Io.luan")
+local uri = Io.uri or error()
+local Logging = require("luan:logging/Logging.luan")
+local logger = Logging.logger("test_delete_junk")
+
+local Https = require("classpath:luan/host/Https.luan")
+
+local domain = "https.me.luan.software"
+local site_dir = uri("file:../sites/" .. domain)
+
+local junk = site_dir.child("this_is_a_junk_dir/")
+junk.mkdir()
+
+Https.delete_junk(domain, site_dir)
--- a/host/test/test_https.luan	Thu Dec 11 13:17:11 2025 -0700
+++ b/host/test/test_https.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -3,19 +3,18 @@
 local do_file = Luan.do_file or error()
 local Io = require "luan:Io.luan"
 local uri = Io.uri or error()
-local Hosted = require "luan:host/Hosted.luan"
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "test_https"
 
 
-do_file "classpath:luan/host/https.luan"
+local Https = require "classpath:luan/host/Https.luan"
 
 local is_https = true
-local domain = "https.s3.luan.software"
+local domain = "https.me.luan.software"
 local site_dir = uri("file:local")
 local luanhost_dir = uri("file:..")
 local dry_run = true
 
 site_dir.mkdir()
 
-Hosted.do_set_https(is_https,domain,site_dir,luanhost_dir,dry_run)
+Https.do_set_https(is_https,domain,site_dir,luanhost_dir,dry_run)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/host/test/test_renew_ssl.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -0,0 +1,20 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local do_file = Luan.do_file or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "test_https"
+
+local Https = require "classpath:luan/host/Https.luan"
+
+local domain = "https.s3.luan.software"
+local site_dir = uri("file:../sites/https.s3.luan.software/")
+local luanhost_dir = uri("file:..")
+local dry_run = true
+local files = Https.get_files(domain, site_dir)
+logger.info(files.csr_file.canonical().to_string())
+
+site_dir.exists() or error()
+
+Https.renew_ssl(files, 0,luanhost_dir,dry_run)
--- a/host/update.sh	Thu Dec 11 13:17:11 2025 -0700
+++ b/host/update.sh	Wed Jan 07 14:00:43 2026 -0700
@@ -1,5 +1,4 @@
 #!/bin/bash
-
 set -e
 
 ./stop.sh
@@ -7,22 +6,4 @@
 echo Updating hg
 hg pull -u
 
-../scripts/build-luan.sh
-
-mkdir -p local
-mkdir -p logs
-rm -f logs/*
-hg identify >logs/changeset.txt
-
-if [ ! -f local/tiny_account.key ]; then
-  echo "Register letsencrypt (tiny-acme)"
-  openssl genrsa 4096 > local/tiny_account.key
-fi
-
-cp startup/nginx/mime.types local/mime.types
-# id -gn gets the name of the primary group of the current user (staff)
-luan startup/nginx/nginx.conf.luan $(pwd) $(whoami) $(id -gn) >local/nginx.conf
-luan startup/nginx/nginx.default.conf.luan $(pwd) >local/nginx.default.conf
-
-echo Starting...
-./start.sh
+./update2.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/host/update2.sh	Wed Jan 07 14:00:43 2026 -0700
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+
+../scripts/build-luan.sh
+
+mkdir -p local
+mkdir -p logs
+rm -f logs/*
+hg identify >logs/changeset.txt
+
+if [ ! -f local/tiny_account.key ]; then
+	echo "Register letsencrypt (tiny-acme)"
+	openssl genrsa 4096 >local/tiny_account.key
+fi
+
+cp startup/nginx/mime.types local/mime.types
+# id -gn gets the name of the primary group of the current user (staff)
+luan startup/nginx/nginx.conf.luan $(pwd) $(whoami) $(id -gn) >local/nginx.conf
+luan startup/nginx/nginx.default.conf.luan $(pwd) >local/nginx.default.conf
+
+# this is done because the nginx conf uses absolute paths
+# and this breaks sites when the luan/host directory is moved
+luan classpath:luan/host/update.luan
+
+echo Starting...
+./start.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/host/Https.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -0,0 +1,304 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local new_error = Luan.new_error or error()
+local load_file = Luan.load_file or error()
+local pairs = Luan.pairs or error()
+local ipairs = Luan.ipairs or error()
+local Io = require "luan:Io.luan"
+local ip = Io.ip or error()
+local uri = Io.uri or error()
+local String = require "luan:String.luan"
+local starts_with = String.starts_with or error()
+local Thread = require "luan:Thread.luan"
+local try_synchronized = Thread.try_synchronized or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Https"
+local sys_logging = require "classpath:luan/host/sys_logging.luan"
+local sys_logger = sys_logging "sys-Https"
+
+
+local Https = {}
+
+local my_ips = Io.my_ips()
+
+local function get_files(domain,site_dir)
+	local ssl_files_dir = site_dir.child("ssl/")
+	ssl_files_dir.mkdir()
+
+	return {
+		ssl_files_dir = ssl_files_dir
+		nginx_file = ssl_files_dir.child("nginx.ssl.conf")
+		key_file = ssl_files_dir.child(domain..".key")
+		local_cer_file = ssl_files_dir.child("fullchain.cer")
+		csr_file = ssl_files_dir.child(domain..".csr")
+		tmp_cert_out = ssl_files_dir.child(domain..".crt.tmp")
+		acme_challenges = ssl_files_dir.child("acme-challenge/")
+	}
+end
+
+-- for testing
+Https.get_files = get_files
+
+local function do_delete_junk(file,canonicals)
+	if canonicals[file.canonical().to_string()] == nil then
+		sys_logger.info("delete "..file.to_string())
+		file.delete()
+	elseif file.is_directory() then
+		for _, child in ipairs(file.children()) do
+			do_delete_junk(child,canonicals)
+		end
+	end
+end
+
+local function delete_junk(domain,site_dir)
+	local files = get_files(domain,site_dir)
+	files.info_luan = site_dir.child("info.luan")
+	local canonicals = {}
+	for _, file in pairs(files) do
+		canonicals[file.canonical().to_string()] = true
+	end
+	for _, child in ipairs(site_dir.children()) do
+		if child.name() == "site" then
+			continue
+		end
+		do_delete_junk(child,canonicals)
+	end
+end
+
+-- for testing
+Https.delete_junk = delete_junk
+
+local function nginx_ssl_conf(domain,files,luanhost_dir)
+%>
+	server {
+		server_name <%=domain%>;
+		listen 80;
+		listen [::]:80;
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+
+		location /.well-known/acme-challenge/ {
+			alias <%= files.acme_challenges.to_string() %>/;
+			autoindex on;
+		}
+	}
+
+	server {
+		server_name <%=domain%>;
+		listen 443 ssl;
+		listen [::]:443 ssl;
+
+		if ($host != $server_name) {
+			return 301 http://$http_host$request_uri;
+		}
+
+		ssl_certificate <%= files.local_cer_file.to_string() %>;
+		ssl_certificate_key <%= files.key_file.to_string() %>;
+		# path is relative to the dir of the conf this comment is found in.
+		include nginx.default.conf;
+	}
+<%
+end
+
+local function reload_nginx(luanhost_dir_str)
+	local cmd = `%>
+sudo $(which nginx) -t -c "<%=luanhost_dir_str%>/local/nginx.conf" && \
+sudo $(which nginx) -s reload -c "<%=luanhost_dir_str%>/local/nginx.conf";
+<%`
+	local s = uri("bash:"..cmd).read_text()
+	logger.info("reload_nginx "..s)
+end
+
+local function issue_cert(files, luanhost_dir, dry_run)
+	local luanhost_dir_str = luanhost_dir.canonical().to_string()
+	local csr_file_str = files.csr_file.canonical().to_string()
+
+	-- Finally, get our cert from letsencrypt.
+	local cmd = luanhost_dir_str..[[/acme_tiny \
+		--account-key ]]..luanhost_dir_str..[[/local/tiny_account.key \
+		--csr ]]..csr_file_str..[[ \
+		--acme-dir ]]..files.acme_challenges.canonical().to_string()..[[ \
+	]]
+
+	-- Problems here are probably from letsencrypt
+	-- leaving this comment here in case its not
+	if dry_run then
+		local dry_run_dir_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
+		cmd = cmd.." --directory-url "..dry_run_dir_url
+	end
+
+	local tmp_out_str = files.tmp_cert_out.canonical().to_string()
+	cmd = cmd.." > "..tmp_out_str
+	logger.info("acme-tiny commandline:\n"..cmd)
+
+	local s = uri("bash:"..cmd).read_text()
+	logger.info("get cert signed by letsencrypt\n"..s)
+
+	-- Empty stdout from acme-tiny is a failure.
+	if files.tmp_cert_out.length() == 0 then
+		logger.error("FAILED getting cert from letsencrypt.\nSee previous output.\nNot writing to fullchain.cer")
+		error("FAILED getting cert from letsencrypt.\nSee log output.\nNot writing to fullchain.cer")
+	else
+		-- Success! Move the temp output to the real fullchain.
+		local local_cer_file_str = files.local_cer_file.canonical().to_string()
+		if files.local_cer_file.exists() then
+			local cmd = "mv "..local_cer_file_str.." "..local_cer_file_str..".old"
+			local s = uri("bash:"..cmd).read_text()
+			logger.info("moving old fullchain to fullchain.cer.old\n"..s)
+		end
+
+		local cmd = "mv "..tmp_out_str.." "..local_cer_file_str
+		local s = uri("bash:"..cmd).read_text()
+		logger.info("move temp output to fullchain.cer\n"..s)
+	end
+
+	reload_nginx(luanhost_dir_str)
+end
+
+
+local function renew_ssl(files,renewal_period,luanhost_dir,dry_run)
+	files.csr_file.exists() or error "no CSR file, assuming local https cert"
+	if time_now() - files.local_cer_file.last_modified() > renewal_period then
+		issue_cert(files, luanhost_dir, dry_run)
+		return
+	end
+end
+Https.renew_ssl = renew_ssl
+
+local ssl_renewal_period = Time.period{days=30}
+
+function Https.update(domain,site_dir,luanhost_dir)
+	local files = get_files(domain,site_dir)
+	if files.nginx_file.exists() then
+		-- sys_logger.info("update "..domain)
+		local nginx = ` nginx_ssl_conf(domain,files,luanhost_dir) `
+		files.nginx_file.write(nginx)
+	end
+	delete_junk(domain,site_dir)
+end
+
+local function do_set_https(is_https,domain,site_dir,luanhost_dir,dry_run)
+	local files = get_files(domain,site_dir)
+
+	-- luan/host
+	local luanhost_file = "file:"..luanhost_dir.to_string().."/"
+	local luanhost_dir_str = luanhost_dir.canonical().to_string()
+
+	if is_https then -- https
+		local domain_ip = ip(domain)
+		local is_local = domain_ip == "127.0.0.1"
+		if not files.key_file.exists() \
+			or not files.local_cer_file.exists() or files.local_cer_file.length()==0 \
+			or not files.nginx_file.exists() \
+		then
+			logger.info("is_local "..is_local)
+
+			-- Use openssl directly to make a self-signed cert,
+			-- no external cert authority involved
+			if is_local then
+				local cmd = `%>
+					openssl req -x509 -newkey rsa:2048 -nodes \
+						-keyout <%= files.key_file.to_string() %> \
+						-out <%= files.local_cer_file.to_string() %> -days 365 \
+						-subj "/CN=<%=domain%>" \
+						-addext "subjectAltName=DNS:<%=domain%>,IP:127.0.0.1"
+				<%`
+				sys_logger.info("local ssl commandline:\n"..cmd)
+				local s = uri("bash:"..cmd).read_text()
+				logger.info("issue local certificate")
+			else
+				if my_ips[domain_ip] ~= true then
+					logger.error("the domain "..domain.." doesn't map to this machine")
+					return
+				end
+				try
+					-- uncomment for testing
+					-- dry_run = true
+
+					-- make the challenge dir. note that this is
+					-- directly under sites/DOMAIN, and *not* under
+					-- sites/DOMAIN/site.
+					files.acme_challenges.mkdir()
+
+					-- Create a domain key to sign the certificate signing request (csr).
+					local key_file_str = files.key_file.canonical().to_string()
+					local cmd = "openssl genrsa 4096 > "..key_file_str
+					local s = uri("bash:"..cmd).read_text()
+					logger.info("create domain key\n"..s)
+
+					-- Create the csr.
+					local csr_file_str = files.csr_file.canonical().to_string()
+					local cmd = 'openssl req -new -sha256 -key '..key_file_str..' -subj "/CN='..domain..'" > '..csr_file_str
+					local s = uri("bash:"..cmd).read_text()
+					logger.info("create csr\n"..s)
+
+					issue_cert(files, luanhost_dir, dry_run)
+
+
+				catch e
+					logger.error("Error setting up ACME: "..e)
+				end_try
+
+			end
+				-- We now have our certificate!
+				-- Now we just need to generate the nginx config
+				-- that uses it, place it in luan/host/sites/DOMAIN/ssl/nginx.ssl.conf
+				-- and tell luanhost to reload nginx.
+
+			if files.key_file.exists() and files.local_cer_file.exists() and files.local_cer_file.length() > 0 then
+				-- the nginx config only requires 2 files:
+				-- fullchain.cer and DOMAIN.key
+				logger.info("writing nginx conf to "..files.nginx_file.canonical().to_string())
+				local nginx = ` nginx_ssl_conf(domain,files,luanhost_dir) `
+				files.nginx_file.write(nginx)
+				reload_nginx(luanhost_dir_str)
+			end
+		else
+			if not is_local then
+				renew_ssl(files,ssl_renewal_period,luanhost_dir,dry_run)
+			end
+		end
+		if not is_local then
+			local function fn()
+				renew_ssl(files,ssl_renewal_period,luanhost_dir,dry_run)
+			end
+			Thread.schedule(fn,{repeating_delay=ssl_renewal_period})
+		end
+	else -- http
+		if files.key_file.exists() or files.nginx_file.exists() then
+			for _, file in pairs(files) do
+				file.delete()
+			end
+			reload_nginx(luanhost_dir_str)
+		end
+	end
+	--logger.info "done"
+end
+Https.do_set_https = do_set_https -- for testing
+
+function Https.set_https(is_https)
+	if Http.did_init() then
+		logger.error(new_error("set_https called outside of init.luan"))
+		return
+	end
+	local domain = Http.domain
+	local site_dir = uri("site:").parent()
+	local luanhost_dir = uri("file:.")
+
+	-- use for testing, so as to not hit rate limits
+	-- on the real letsencrypt servers
+	local dry_run = false
+
+	if not try_synchronized( function()
+		do_set_https(is_https,domain,site_dir,luanhost_dir,dry_run)
+	end, domain..".lock", 0 )() then
+		logger.info("set_https already running for "..domain..", skipping")
+	end
+end
+
+return Https
--- a/src/luan/host/https.luan	Thu Dec 11 13:17:11 2025 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,187 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local new_error = Luan.new_error or error()
-local load_file = Luan.load_file or error()
-local ipairs = Luan.ipairs or error()
-local Boot = require "luan:Boot.luan"
-local Io = require "luan:Io.luan"
-local ip = Io.ip or error()
-local uri = Io.uri or error()
-local String = require "luan:String.luan"
-local starts_with = String.starts_with or error()
-local Thread = require "luan:Thread.luan"
-local try_synchronized = Thread.try_synchronized or error()
-local Http = require "luan:http/Http.luan"
-local Hosted = require "luan:host/Hosted.luan"
-local Logging = require "luan:logging/Logging.luan"
-local logger = Logging.logger "https"
-
-
-local my_ips = Io.my_ips()
-
-local function do_set_https(is_https,domain,site_dir,luanhost_dir,dry_run)
-	local nginx_file = site_dir.child("nginx.ssl.conf")
-	local key_file = site_dir.child(domain..".key")
-	local csr_file = site_dir.child(domain..".csr")
-	local local_cer_file = site_dir.child("fullchain.cer")
-	local local_ca_file = site_dir.child("ca.cer")
-	-- luan/host
-	local luanhost_file = "file:"..luanhost_dir.to_string().."/"
-	local luanhost_dir_str = luanhost_dir.canonical().to_string()
-	local changed = false
-
-	if is_https then -- https
-		if not key_file.exists() \
-			or not local_cer_file.exists() or local_cer_file.length()==0 \
-			or not nginx_file.exists() \
-		then
-			local domain_ip = ip(domain)
-			local is_local = domain_ip == "127.0.0.1"
-			logger.info("is_local "..is_local)
-
-			-- Use openssl directly to make a self-signed cert,
-			-- no external cert authority involved
-			if is_local then
-				local cmd = [[
-					./local_https.sh "]]..domain..[["
-				]]
-				local s = uri("bash:"..cmd).read_text()
-				logger.info("issue local certificate")
-			else
-				if my_ips[domain_ip] ~= true then
-					logger.error("the domain "..domain.." doesn't map to this machine")
-					return
-				end
-				-- set up a temporary barebones nginx conf
-				-- to serve acme challenges on the domain
-				local temp_dir = uri("file:/tmp/acme_setup/"..domain)
-				try
-					-- Clean out old temp files
-					temp_dir.delete()
-
-					-- create all needed dirs at once by using
-					-- mkdir -p on the deepest nested dir (acme-challenge)
-					local webroot = temp_dir.to_string().."/webroot"
-					local acme_challenges = webroot.."/.well-known/acme-challenge"
-					uri("file:"..acme_challenges).mkdir()
-
-					-- Create the nginx config from the template
-					-- The *output* file, where the generated config is stored
-					local acme_nginx_file = temp_dir.child("nginx.acme_setup.conf")
-					local conf = load_file(luanhost_file.."startup/nginx/nginx.acme_setup.conf.luan")
-					local acme_nginx = ` conf(webroot,domain) `
-					acme_nginx_file.write(acme_nginx)
-
-					-- Create an index.html to search for in the logs
-					-- to verify everything is working
-					local index_file = webroot.."/index.html"
-					local cmd = "echo 'if you are seeing this, ssl setup has failed. please check the logs.' > "..index_file
-					local s = uri("bash:"..cmd).read_text()
-
-					-- The config in ./local/nginx.conf has a directive to
-					-- glob include confs in /tmp/acme_setup/*/nginx.acme_setup.conf
-					-- so we just need to reload it so it can find the one we just made
-					local cmd = [[
-						sudo $(which nginx) -t -c "]]..luanhost_dir_str..[[/local/nginx.conf" && \ 
-						sudo $(which nginx) -s reload -c "]]..luanhost_dir_str..[[/local/nginx.conf";
-					]]
-					local s = uri("bash:"..cmd).read_text()
-					logger.info("reload_nginx "..s)
-
-					-- We've set up nginx to serve from our temp root, now we need to
-					-- create a *domain key*, which we then use to sign our cert.
-					local key_file_str = key_file.canonical().to_string()
-					local cmd = "openssl genrsa 4096 > "..key_file_str
-					local s = uri("bash:"..cmd).read_text()
-					logger.info("create domain key\n"..s)
-
-					-- create the cert, signed with the key we just made
-					local csr_file_str = csr_file.canonical().to_string()
-					local cmd = 'openssl req -new -sha256 -key '..key_file_str..' -subj "/CN='..domain..'" > '..csr_file_str
-					local s = uri("bash:"..cmd).read_text()
-					logger.info("create cert\n"..s)
-
-					-- Finally, get our cert signed by letsencrypt.
-					local cmd = [[
-						]]..luanhost_dir_str..[[/acme_tiny --account-key ]]..luanhost_dir_str..[[/local/tiny_account.key \
-						--csr ]]..csr_file_str..[[ \
-						--acme-dir ]]..acme_challenges..[[ \
-					]]
-					if dry_run then
-						local dry_run_dir_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
-						cmd = cmd.." --directory-url "..dry_run_dir_url
-					end
-					local local_cer_file_str = local_cer_file.canonical().to_string()
-					cmd = cmd.."> "..local_cer_file_str
-
-					local s = uri("bash:"..cmd).read_text()
-					logger.info("get cert signed by letsencrypt\n"..s)
-
-				catch e
-					logger.error("Error setting up ACME: "..e)
-				finally
-					temp_dir.delete()
-				end_try
-
-			end
-				-- We now have our certificate!
-				-- Now we just need to generate the nginx config
-				-- that uses it, place it in luan/host/sites/*/nginx.ssl.conf
-				-- and tell luan-host to reload nginx.
-
-			if key_file.exists() and local_cer_file.exists() and local_cer_file.length() > 0 then
-				changed = true
-				-- the nginx config only requires 2 files:
-				-- fullchain.cer and DOMAIN.key
-				local conf = load_file(luanhost_file.."startup/nginx/nginx.ssl.conf.luan")
-				local nginx = ` conf(luanhost_dir_str,domain) `
-				nginx_file.write(nginx)
-			end
-		end
-	else -- http
-		if key_file.exists() or nginx_file.exists() then
-			changed = true
-			nginx_file.delete()
-			local_cer_file.delete()
-			local_ca_file.delete()
-			local ptn = domain.."."
-			for _, file in ipairs(site_dir.children()) do
-				if starts_with(file.name(),ptn) then
-					file.delete()
-				end
-			end
-		end
-	end
-	if changed then
-		local cmd = [[
-sudo $(which nginx) -t -c "]]..luanhost_dir_str..[[/local/nginx.conf" && \
-sudo $(which nginx) -s reload -c "]]..luanhost_dir_str..[[/local/nginx.conf";
-]]
-		local s = uri("bash:"..cmd).read_text()
-		logger.info("reload_nginx "..s)
-	end
-	--logger.info "done"
-end
-Hosted.do_set_https = do_set_https  -- for testing
-
-function Hosted.set_https(is_https)
-	if Http.did_init() then
-		logger.error(new_error("set_https called outside of init.luan"))
-		return
-	end
-	local domain = Http.domain
-	local site_dir = uri("site:").parent()
-	local luanhost_dir = uri("file:.")
-
-	-- use for testing, so as to not hit rate limits
-	-- on the real letsencrypt servers
-	local dry_run = false
-
-	if not try_synchronized( function()
-		do_set_https(is_https,domain,site_dir,luanhost_dir,dry_run)
-	end, domain..".lock", 0 )() then
-		logger.info("set_https already running for "..domain..", skipping")
-	end
-end
-
-Hosted.set_https = Boot.no_security(Hosted.set_https)
--- a/src/luan/host/init.luan	Thu Dec 11 13:17:11 2025 -0700
+++ b/src/luan/host/init.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -4,6 +4,7 @@
 local Package = require "luan:Package.luan"
 local Number = require "luan:Number.luan"
 local long = Number.long or error()
+local Boot = require "luan:Boot.luan"
 
 
 local dir, domain = ...
@@ -99,7 +100,8 @@
 end
 
 
-do_file "classpath:luan/host/https.luan"
+local Https = require "classpath:luan/host/Https.luan"
+Hosted.set_https = Boot.no_security(Https.set_https)
 
 
 local LuanJava = require "java:luan.Luan"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/host/sys_logging.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -0,0 +1,13 @@
+local Logging = require "luan:logging/Logging.luan"
+require "java"
+local ThreadLocalAppender = require "java:goodjava.logger.ThreadLocalAppender"
+
+
+return function(name)
+	local logger = Logging.logger(name)
+	local jlogger = logger.java.logger
+	if jlogger.appender.instanceof(ThreadLocalAppender) then
+		jlogger.appender = jlogger.appender.defaultAppender
+	end
+	return logger
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/host/update.luan	Wed Jan 07 14:00:43 2026 -0700
@@ -0,0 +1,16 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Io = require "luan:Io.luan"
+local Https = require "classpath:luan/host/Https.luan"
+
+
+local luanhost_dir = Io.schemes.file(".").canonical()
+local sites_dir = luanhost_dir.child("sites")
+sites_dir.mkdir()
+
+local children = sites_dir.children()
+for _, site_dir in ipairs(children) do
+	local domain = site_dir.name()
+	Https.update(domain, site_dir, luanhost_dir)
+end