Mercurial Hosting > luan
view src/luan/host/Https.luan @ 2134:de3107eb911f
improve previous
| author | Violet7 |
|---|---|
| date | Fri, 16 Jan 2026 17:59:40 -0800 |
| parents | c3b4c19f2d8a |
| children |
line wrap: on
line source
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() %>/; try_files $uri $uri/ =404; } } 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 master nginx conf include <%=luanhost_dir.canonical().to_string()%>/local/luanhost.default.conf; } <% end local function reload_nginx(luanhost_dir_str) local cmd = "sudo nginx -t && sudo nginx -s reload;" 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
