Mercurial Hosting > luan
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
