view src/luan/host/https.luan @ 2045:265dc9af6a49 acme-tiny

fix guard_uri initiation
author Violet7
date Sun, 09 Nov 2025 02:31:57 -0800
parents d8550e64d613
children e0896f65c847
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 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 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"

logger.info("Hello test")

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 nginx_file = site_dir.child("nginx.ssl.conf")
	local key_file = site_dir.child(domain..".key")
	local key_file_str = key_file.canonical().to_string()
	local csr_file = site_dir.child(domain..".csr")
	local csr_file_str = csr_file.canonical().to_string()
	local local_cer_file = site_dir.child("fullchain.cer")
	local local_cer_file_str = local_cer_file.canonical().to_string()
	local local_ca_file = site_dir.child("ca.cer")
	-- luan/host
	local luanhost_dir = uri("file:.").canonical().to_string()
	local changed = false
	-- use for testing, so as to not hit rate limits
	-- on the real letsencrypt servers
	local dry_run = false
	local dry_run_dir_url = "https://acme-staging-v02.api.letsencrypt.org/directory"

	-- declare these so they are visible in the catch and finally blocks
  local guard_file = "/tmp/acme_setup_locks/"..domain..".lock"
  local guard_uri = uri("file:"..guard_file)

	if is_https then	-- https
		if not key_file.exists() then
			local is_local = ip(domain) == "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
				-- set up a temporary barebones nginx conf
				-- to serve acme challenges on the domain
				try
					local temp_dir_string = "/tmp/acme_setup/"..domain

					-- recursion guard, must have this to prevent
					-- the http request from invoking this code
					-- and causing an infinite recursion.
					local cmd = "mkdir -p /tmp/acme_setup_locks/"
					local s = uri("bash:"..cmd).read_text()
					if guard_uri.exists() then
							logger.info("set_https already running for "..domain..", skipping")
							return
					end

					-- Clean out old temp files
					local cmd = "rm -rf "..temp_dir_string
					local s = uri("bash:"..cmd).read_text()

					-- create all needed dirs at once by using
					-- mkdir -p on the deepest nested dir (acme-challenge)
					local webroot = temp_dir_string.."/webroot"
					local acme_challenges = webroot.."/.well-known/acme-challenge"
					local cmd = "mkdir -p "..acme_challenges
					local s = uri("bash:"..cmd).read_text()

					guard_uri.write("this is a recursion guard, see https.luan")


					-- Create the nginx config from the template
					local temp_dir = uri("file:"..temp_dir_string)
					-- The *output* file, where the generated config is stored
					local acme_nginx_file = temp_dir.child("nginx.acme_setup.conf")
					local conf = load_file "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 'hi, testing' > "..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..[[/local/nginx.conf" && sudo $(which nginx) -s reload;
					]]
					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 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 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 = [[
						python3 acme_tiny.py --account-key ./local/tiny_account.key \
						--csr ]]..csr_file_str..[[ \
						--acme-dir ]]..acme_challenges..[[ \
					]]
					if dry_run == true then
						cmd = cmd.." --directory-url "..dry_run_dir_url
					end
					cmd = cmd.."> "..local_cer_file_str

					local s = uri("bash:"..cmd).read_text()
					logger.info("get cert signed by letsencrypt\n"..s)

					-- The above http requests made by acme_tiny are the only thing
					-- that could cause a recursion so it is safe to delete the guard here.

				catch e
					logger.error("Error setting up ACME: "..e.to_string())
				finally
					if guard_uri and guard_uri.exists() then
						guard_uri.delete()
					end
					local cmd = "rm -rf "..temp_dir_string
					local s = uri("bash:"..cmd).read_text()
				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.

			end
			if key_file.exists() and local_cer_file.exists() then
				changed = true
				-- the nginx config only requires 2 files:
				-- fullchain.cer and DOMAIN.key
				local conf = load_file "file:startup/nginx/nginx.ssl.conf.luan"
				local nginx = ` conf(luanhost_dir,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..[[/local/nginx.conf" && sudo $(which nginx) -s reload;
]]
		local s = uri("bash:"..cmd).read_text()
		logger.info("reload_nginx "..s)
	end
	--logger.info "done"
end
Hosted.set_https = Boot.no_security(Hosted.set_https)