#!/bin/sh /etc/rc.common
# Copyright (C) 2024 ImmortalWrt

. /lib/functions.sh
. /lib/functions/procd.sh

USE_PROCD=1

START=90
STOP=10

PROG=/usr/bin/gecoosac
DEFAULT_DB_DIR=/etc/gecoosac/
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload/
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
DEFAULT_PID_DIR=/var/run/
DEFAULT_LANG=zh
CERT_TIMEOUT=60

init_conf() {
	config_load "gecoosac"
	config_get "db_dir" "config" "db_dir" "$DEFAULT_DB_DIR"
	config_get "upload_dir" "config" "upload_dir" "$DEFAULT_UPLOAD_DIR"
	config_get "enabled" "config" "enabled" "0"
	config_get "port" "config" "port" "60650"
	config_get "isonlyoneprot" "config" "isonlyoneprot" "1"
	config_get "m_port" "config" "m_port" "8080"
	config_get "https" "config" "https" "0"
	config_get "crt_file" "config" "crt_file" "$DEFAULT_CRT_FILE"
	config_get "key_file" "config" "key_file" "$DEFAULT_KEY_FILE"
	config_get "piddir" "config" "piddir" "$DEFAULT_PID_DIR"
	config_get "lang" "config" "lang" "$DEFAULT_LANG"
	config_get "debug" "config" "debug" "0"
	config_get "showtip" "config" "showtip" "0"
	config_get "log" "config" "log" "0"
}

is_abs_path() {
	case "$1" in
		/*) return 0 ;;
		*) return 1 ;;
	esac
}

is_port() {
	case "$1" in
		""|*[!0-9]*) return 1 ;;
	esac

	[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ]
}

run_with_timeout() {
	local timeout pid i

	timeout="$1"
	shift

	"$@" >/dev/null 2>&1 &
	pid="$!"
	i=0

	while kill -0 "$pid" >/dev/null 2>&1; do
		if [ "$i" -ge "$timeout" ]; then
			kill "$pid" >/dev/null 2>&1
			sleep 1
			kill -9 "$pid" >/dev/null 2>&1
			wait "$pid" 2>/dev/null
			return 1
		fi
		sleep 1
		i=$((i + 1))
	done

	wait "$pid"
}

is_ipv4() {
	local old_ifs part

	old_ifs="$IFS"
	IFS=.
	set -- $1
	IFS="$old_ifs"

	[ "$#" -eq 4 ] || return 1
	for part in "$@"; do
		case "$part" in
			""|*[!0-9]*) return 1 ;;
		esac
		[ "$part" -le 255 ] 2>/dev/null || return 1
	done

	return 0
}

cert_matches_san() {
	local cert_file="$1"
	local cert_host="$2"
	local cert_ip="$3"
	local san

	[ -s "$cert_file" ] || return 1
	san="$(openssl x509 -in "$cert_file" -noout -ext subjectAltName 2>/dev/null)" || return 1
	echo "$san" | grep -F -q "DNS:${cert_host}" || return 1
	[ -z "$cert_ip" ] || echo "$san" | grep -F -q "IP Address:${cert_ip}" || return 1

	return 0
}

generate_default_cert() {
	local cert_host cert_ip cert_cn cert_san tmp_crt tmp_key

	if ! command -v openssl >/dev/null 2>&1; then
		logger -t gecoosac "openssl is required to generate the default HTTPS certificate"
		return 1
	fi

	cert_host="$(uci -q get system.@system[0].hostname)"
	case "$cert_host" in
		""|*[!A-Za-z0-9.-]*) cert_host="GecoosAC" ;;
	esac

	cert_ip="$(uci -q get network.lan.ipaddr)"
	is_ipv4 "$cert_ip" || cert_ip=""

	if [ -s "$DEFAULT_KEY_FILE" ] && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip"; then
		return 0
	fi

	mkdir -p /etc/gecoosac/tls

	cert_cn="${cert_ip:-$cert_host}"
	cert_san="DNS:$cert_host"
	[ -n "$cert_ip" ] && cert_san="${cert_san},IP:${cert_ip}"
	tmp_crt="${DEFAULT_CRT_FILE}.tmp"
	tmp_key="${DEFAULT_KEY_FILE}.tmp"

	rm -f "$tmp_crt" "$tmp_key"
	run_with_timeout "$CERT_TIMEOUT" openssl req \
		-x509 \
		-nodes \
		-newkey ec \
		-pkeyopt ec_paramgen_curve:prime256v1 \
		-pkeyopt ec_param_enc:named_curve \
		-sha256 \
		-days 3650 \
		-keyout "$tmp_key" \
		-out "$tmp_crt" \
		-subj "/CN=$cert_cn" \
		-addext "basicConstraints=critical,CA:FALSE" \
		-addext "keyUsage=digitalSignature" \
		-addext "extendedKeyUsage=serverAuth" \
		-addext "subjectAltName=$cert_san"

	if [ -s "$tmp_crt" ] && [ -s "$tmp_key" ] && cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip"; then
		mv "$tmp_key" "$DEFAULT_KEY_FILE"
		mv "$tmp_crt" "$DEFAULT_CRT_FILE"
		chmod 600 "$DEFAULT_KEY_FILE"
		chmod 644 "$DEFAULT_CRT_FILE"
		return 0
	fi

	rm -f "$tmp_crt" "$tmp_key"
	logger -t gecoosac "failed to generate default HTTPS certificate"
	return 1
}

normalize_conf() {
	is_abs_path "$upload_dir" || upload_dir="$DEFAULT_UPLOAD_DIR"
	is_abs_path "$db_dir" || db_dir="$DEFAULT_DB_DIR"
	is_abs_path "$crt_file" || crt_file="$DEFAULT_CRT_FILE"
	is_abs_path "$key_file" || key_file="$DEFAULT_KEY_FILE"
	is_abs_path "$piddir" && [ "$piddir" != "/" ] || piddir="$DEFAULT_PID_DIR"
	is_port "$port" || port="60650"
	is_port "$m_port" || m_port="8080"

	[ "$enabled" = "1" ] || enabled="0"
	[ "$isonlyoneprot" = "0" ] || isonlyoneprot="1"
	[ "$https" = "1" ] || https="0"
	case "$lang" in
		zh|en) ;;
		*) lang="$DEFAULT_LANG" ;;
	esac
	[ "$debug" = "1" ] || debug="0"
	[ "$showtip" = "1" ] || showtip="0"
	[ "$log" = "1" ] || log="0"
}

ensure_dirs() {
	mkdir -p "$upload_dir" "$db_dir" "$piddir" /etc/gecoosac/tls
}

start_service() {
	init_conf
	normalize_conf

	[ "$enabled" = "1" ] || return 0
	if [ ! -x "$PROG" ]; then
		logger -t gecoosac "program not found: $PROG"
		return 1
	fi

	ensure_dirs

	if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
		if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
			generate_default_cert || return 1
		fi

		if [ ! -r "$crt_file" ] || [ ! -r "$key_file" ]; then
			logger -t gecoosac "HTTPS is enabled but certificate or key file is missing"
			return 1
		fi
	fi

	procd_open_instance gecoosac
	procd_set_param command "$PROG"
	procd_append_param command -f "$upload_dir"
	procd_append_param command -p "$port"
	procd_append_param command -debug "$debug"
	procd_append_param command -dbpath "$db_dir"
	procd_append_param command -token "1"
	procd_append_param command -lang "$lang"
	procd_append_param command -piddir "$piddir"
	if [ "$isonlyoneprot" = "0" ]; then
		procd_append_param command -isonlyoneprot "0"
		procd_append_param command -mp "$m_port"
		if [ "$https" = "1" ]; then
			procd_append_param command -https "1"
			procd_append_param command -crt "$crt_file"
			procd_append_param command -key "$key_file"
		else
			procd_append_param command -https "0"
		fi
	else
		procd_append_param command -isonlyoneprot "1"
	fi
	procd_append_param command -showtip "$showtip"
	procd_set_param env GIN_MODE=release
	procd_set_param file /etc/config/gecoosac
	procd_set_param pidfile "${piddir%/}/gecoosac.pid"
	procd_set_param stdout "$log"
	procd_set_param stderr "$log"
	procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
	procd_close_instance
}

reload_service() {
	stop
	start
}

service_triggers() {
	procd_add_reload_trigger "gecoosac"
}
