diff --git a/labels/debug.asm b/labels/debug.asm index 370f5a3..177ceb4 100644 --- a/labels/debug.asm +++ b/labels/debug.asm @@ -233,6 +233,9 @@ dbg_sighandler_success: cmp r10, 2 je .sigint + cmp r10, 1 + je .sighup + je dbg_skip ; if its something unexpected, just skip .sigchld: @@ -247,6 +250,10 @@ dbg_sighandler_success: AAPPEND r9, str_sighanlder_sigint jmp .end +.sighup: + AAPPEND r9, str_sighanlder_sighup + jmp .end + .end: AAPPEND r9, str_success_sighandler_p2 diff --git a/labels/initialsetup.asm b/labels/initialsetup.asm index e2871b3..5908d98 100644 --- a/labels/initialsetup.asm +++ b/labels/initialsetup.asm @@ -5,73 +5,6 @@ extern inet_pton ; to parse the interface section .data env_path db ".env", 0 - ; keys & defaults if no .env is provided or found - key_bindaddr db "BIND_ADDRESS", 0 - default_bindaddr db "0.0.0.0", 0 - - key_port db "PORT", 0 - default_port db "8080", 0 - - key_docroot db "DOCUMENT_ROOT", 0 ; document root, no trailing slash ! - default_docroot db ".", 0 - - key_index db "INDEX_FILE", 0 ; default file if a directory is fetched (eg '/' becomes internally '/index.txt') - default_index db "index.html", 0 - - key_maxconns db "MAX_REQUESTS", 0 ; max concurrent requests (and threads) - default_maxconns db "20", 0 - - key_name db "SERVER_NAME", 0 ; server name provided in the response headers - default_name db "NASMServer/", 0 ; version will be appended later - - key_authuser db "AUTH_USER", 0 - default_authuser db "", 0 - - key_authpass db "AUTH_PASSWORD", 0 - default_authpass db "", 0 - - key_authrealm db "AUTH_REALM", 0 - default_authrealm db "None", 0 - - key_servedots db "SERVE_DOTS", 0 - default_servedots db "false", 0 - - key_maxage db "MAX_AGE", 0 - default_maxage db "600", 0 - - key_logfile db "LOG_FILE", 0 - default_logfile db "", 0 - - key_use_xri db "USE_X_REAL_IP", 0 ; if we should use 'X-Real-IP' to display the IP address in the logs - default_use_xri db "false", 0 - - key_use_chroot db "USE_CHROOT", 0 - default_use_chroot db "true", 0 - - key_noperms db "DROP_PRIVILEGES", 0 - default_noperms db "true", 0 - - key_loglevel db "LOG_LEVEL", 0 - default_loglevel db "info", 0 - - key_linger_to db "LINGER_TIMEOUT", 0 - default_linger_to db "5", 0 - - ; errordocs files, relatively to the document_root (empty = none) - ; start them with a slash ! - - key_errordoc_405 db "ERRORDOC_405", 0 - key_errordoc_404 db "ERRORDOC_404", 0 - key_errordoc_403 db "ERRORDOC_403", 0 - key_errordoc_401 db "ERRORDOC_401", 0 - key_errordoc_400 db "ERRORDOC_400", 0 - - default_errordoc_405 db "", 0 - default_errordoc_404 db "", 0 - default_errordoc_403 db "", 0 - default_errordoc_401 db "", 0 - default_errordoc_400 db "", 0 - section .bss ; system current_uid resd 1 ; Storing the current uid to check for root @@ -195,74 +128,25 @@ initial_setup: mov r10, rax ITOA r10, current_pid_str, rcx -.load_env: - ; load all config from .env (or fall back to defaults) + call load_config ; from labels/envreload.asm + mov byte [rel first_load], 0 - ENV_DEFAULT env_path_buf, key_docroot, document_root, 129, default_docroot - ENV_DEFAULT env_path_buf, key_index, index_file, 129, default_index - ENV_DEFAULT env_path_buf, key_name, server_name, 129, server_w_ver - ENV_DEFAULT env_path_buf, key_authuser, auth_username, 129, default_authuser - ENV_DEFAULT env_path_buf, key_authpass, auth_password, 129, default_authpass - ENV_DEFAULT env_path_buf, key_authrealm, auth_realm, 129, default_authrealm - - ENV_DEFAULT env_path_buf, key_errordoc_405, errordoc_405, 129, default_errordoc_405 - ENV_DEFAULT env_path_buf, key_errordoc_404, errordoc_404, 129, default_errordoc_404 - ENV_DEFAULT env_path_buf, key_errordoc_403, errordoc_403, 129, default_errordoc_403 - ENV_DEFAULT env_path_buf, key_errordoc_401, errordoc_401, 129, default_errordoc_401 - ENV_DEFAULT env_path_buf, key_errordoc_400, errordoc_400, 129, default_errordoc_400 - - ; port: read as ascii, then convert to integer - ENV_DEFAULT env_path_buf, key_port, word_str_buf, 8, default_port - ATOI word_str_buf, rax - mov word [rel port], ax - - ENV_DEFAULT env_path_buf, key_maxconns, word_str_buf, 8, default_maxconns ; reuse word_str_buf, we're done with it - ATOI word_str_buf, rax - mov word [rel max_requests], ax - - ENV_DEFAULT env_path_buf, key_maxage, max_age_str, 12, default_maxage - ATOI max_age_str, rax - mov dword [rel max_age], eax - - ENV_DEFAULT env_path_buf, key_linger_to, max_age_str, 12, default_linger_to - ATOI max_age_str, rax - mov dword [rel linger_to], eax - - ENV_DEFAULT env_path_buf, key_servedots, serve_dots_str, 5, default_servedots - BOOL_FLAG serve_dots_str, serve_dots - - ENV_DEFAULT env_path_buf, key_use_xri, use_xri_str, 5, default_use_xri - BOOL_FLAG use_xri_str, use_xri - - ENV_DEFAULT env_path_buf, key_use_chroot, use_chroot_str, 5, default_use_chroot - BOOL_FLAG use_chroot_str, use_chroot - - ENV_DEFAULT env_path_buf, key_noperms, be_nobody_str, 5, default_noperms - BOOL_FLAG be_nobody_str, be_nobody - - ; process the log level - ENV_DEFAULT env_path_buf, key_loglevel, str_level_str, 6, default_loglevel - call .parse_str_level - ; open the log file - ENV_DEFAULT env_path_buf, key_logfile, str_file_path, 129, default_logfile call .open_logfile - ENV_DEFAULT env_path_buf, key_bindaddr, bind_addr_str, 16, default_bindaddr - ; build sockaddr from the now-loaded port/interface movzx eax, word [rel port] - xchg al, ah ; htons(), swap bytes for big-endian + xchg al, ah ; htons(), swap bytes for big-endian mov word [rel sockaddr + 2], ax ; inet_pton(af, src, dst) - mov rdi, 2 ; AF_INET (ipv4) + mov rdi, 2 ; AF_INET (ipv4) lea rsi, [rel bind_addr_str] lea rdx, [rel interface] call inet_pton cmp rax, 0 - jle .bad_bind_addr ; 0 = invalid format, -1 = unsupported af + jle .bad_bind_addr ; 0 = invalid format, -1 = unsupported af mov eax, [rel interface] mov dword [rel sockaddr + 4], eax @@ -303,37 +187,6 @@ initial_setup: .str_file_end: ret ; open_logfile len -.parse_str_level: - lea rax, [rel str_level_str] - - ; "debug" - cmp dword [rax], 'debu' - jne .check_none - - cmp byte [rax+4], 'g' - jne .check_none - - mov byte [rel str_level], 2 - - jmp .str_level_end - -.check_none: - ; "none" - cmp dword [rax], 'none' - jne .check_info - - mov byte [rel str_level], 0 - - jmp .str_level_end - -.check_info: - ; "info" or anything unrecognized = 0 - mov byte [rel str_level], 1 - ret - -.str_level_end: - ret ; .parse_str_level return point - .failed_read_file: LOG_ERR str_fail_read_env, str_fail_read_env_len EXIT 1 diff --git a/labels/loadenv.asm b/labels/loadenv.asm new file mode 100644 index 0000000..b6eb20d --- /dev/null +++ b/labels/loadenv.asm @@ -0,0 +1,327 @@ +; loadenv.asm - Loads config from .env, on boot and on reloads + +section .data + first_load db 1 ; 1 = boot, 0 = subsequent reloads + + ; keys & defaults if no .env is provided or found + key_bindaddr db "BIND_ADDRESS", 0 + default_bindaddr db "0.0.0.0", 0 + + key_port db "PORT", 0 + default_port db "8080", 0 + + key_docroot db "DOCUMENT_ROOT", 0 ; document root, no trailing slash ! + default_docroot db ".", 0 + + key_index db "INDEX_FILE", 0 ; default file if a directory is fetched (eg '/' becomes internally '/index.txt') + default_index db "index.html", 0 + + key_maxconns db "MAX_REQUESTS", 0 ; max concurrent requests (and threads) + default_maxconns db "20", 0 + + key_name db "SERVER_NAME", 0 ; server name provided in the response headers + default_name db "NASMServer/", 0 ; version will be appended later + + key_authuser db "AUTH_USER", 0 + default_authuser db "", 0 + + key_authpass db "AUTH_PASSWORD", 0 + default_authpass db "", 0 + + key_authrealm db "AUTH_REALM", 0 + default_authrealm db "None", 0 + + key_servedots db "SERVE_DOTS", 0 + default_servedots db "false", 0 + + key_maxage db "MAX_AGE", 0 + default_maxage db "600", 0 + + key_logfile db "LOG_FILE", 0 + default_logfile db "", 0 + + key_use_xri db "USE_X_REAL_IP", 0 ; if we should use 'X-Real-IP' to display the IP address in the logs + default_use_xri db "false", 0 + + key_use_chroot db "USE_CHROOT", 0 + default_use_chroot db "true", 0 + + key_noperms db "DROP_PRIVILEGES", 0 + default_noperms db "true", 0 + + key_loglevel db "LOG_LEVEL", 0 + default_loglevel db "info", 0 + + key_linger_to db "LINGER_TIMEOUT", 0 + default_linger_to db "5", 0 + + ; errordocs files, relatively to the document_root (empty = none) + ; start them with a slash ! + + key_errordoc_405 db "ERRORDOC_405", 0 + key_errordoc_404 db "ERRORDOC_404", 0 + key_errordoc_403 db "ERRORDOC_403", 0 + key_errordoc_401 db "ERRORDOC_401", 0 + key_errordoc_400 db "ERRORDOC_400", 0 + + default_errordoc_405 db "", 0 + default_errordoc_404 db "", 0 + default_errordoc_403 db "", 0 + default_errordoc_401 db "", 0 + default_errordoc_400 db "", 0 + +section .bss + ; snapshots of boot-only settings, used to detect drift on reload + old_document_root resb 129 + old_bind_addr_str resb 16 + old_port resw 1 + old_str_file_path resb 129 + old_use_chroot resb 1 + old_be_nobody resb 1 + +section .text + global load_config + +; load_config +; Loads config from .env (or defaults) into BSS buffers. +; On first_load: loads everything, same behavior as the old initial_setup.load_env. +; On reloads: loads reloadable settings, and warns + restores the old +; value for anything that would require a restart to take effect (docroot, +; bind address, port, log file path, chroot/nobody toggles). +; Expects: env_path_buf to already be set (initial_setup does this once at boot). +; Clobbers: rax, rbx, rcx, rdx, rsi, rdi, r8, r9, r10 +load_config: + cmp byte [rel first_load], 1 + jne .snapshot_boot_only + + jmp .load_body ; first boot: load everything, nothing to compare against yet + +.snapshot_boot_only: + ; save current values of boot-only settings so we can detect drift after reload + + lea rsi, [rel document_root] + lea rdi, [rel old_document_root] + call .copy_str + + lea rsi, [rel bind_addr_str] + lea rdi, [rel old_bind_addr_str] + call .copy_str + + mov ax, [rel port] + mov [rel old_port], ax + + lea rsi, [rel str_file_path] + lea rdi, [rel old_str_file_path] + call .copy_str + + mov al, [rel use_chroot] + mov [rel old_use_chroot], al + + mov al, [rel be_nobody] + mov [rel old_be_nobody], al + +.load_body: + ; load all config from .env (or fall back to defaults) + + ENV_DEFAULT env_path_buf, key_docroot, document_root, 129, default_docroot + ENV_DEFAULT env_path_buf, key_index, index_file, 129, default_index + ENV_DEFAULT env_path_buf, key_name, server_name, 129, server_w_ver + ENV_DEFAULT env_path_buf, key_authuser, auth_username, 129, default_authuser + ENV_DEFAULT env_path_buf, key_authpass, auth_password, 129, default_authpass + ENV_DEFAULT env_path_buf, key_authrealm, auth_realm, 129, default_authrealm + + ENV_DEFAULT env_path_buf, key_errordoc_405, errordoc_405, 129, default_errordoc_405 + ENV_DEFAULT env_path_buf, key_errordoc_404, errordoc_404, 129, default_errordoc_404 + ENV_DEFAULT env_path_buf, key_errordoc_403, errordoc_403, 129, default_errordoc_403 + ENV_DEFAULT env_path_buf, key_errordoc_401, errordoc_401, 129, default_errordoc_401 + ENV_DEFAULT env_path_buf, key_errordoc_400, errordoc_400, 129, default_errordoc_400 + + ; port: read as ascii, then convert to integer + ENV_DEFAULT env_path_buf, key_port, word_str_buf, 8, default_port + ATOI word_str_buf, rax + mov word [rel port], ax + + ENV_DEFAULT env_path_buf, key_maxconns, word_str_buf, 8, default_maxconns ; reuse word_str_buf, we're done with it + ATOI word_str_buf, rax + mov word [rel max_requests], ax + + ENV_DEFAULT env_path_buf, key_maxage, max_age_str, 12, default_maxage + ATOI max_age_str, rax + mov dword [rel max_age], eax + + ENV_DEFAULT env_path_buf, key_linger_to, max_age_str, 12, default_linger_to + ATOI max_age_str, rax + mov dword [rel linger_to], eax + + ENV_DEFAULT env_path_buf, key_servedots, serve_dots_str, 5, default_servedots + BOOL_FLAG serve_dots_str, serve_dots + + ENV_DEFAULT env_path_buf, key_use_xri, use_xri_str, 5, default_use_xri + BOOL_FLAG use_xri_str, use_xri + + ENV_DEFAULT env_path_buf, key_use_chroot, use_chroot_str, 5, default_use_chroot + BOOL_FLAG use_chroot_str, use_chroot + + ENV_DEFAULT env_path_buf, key_noperms, be_nobody_str, 5, default_noperms + BOOL_FLAG be_nobody_str, be_nobody + + ; process the log level + ENV_DEFAULT env_path_buf, key_loglevel, str_level_str, 6, default_loglevel + call .parse_str_level + + ENV_DEFAULT env_path_buf, key_logfile, str_file_path, 129, default_logfile + + ENV_DEFAULT env_path_buf, key_bindaddr, bind_addr_str, 16, default_bindaddr + + cmp byte [rel first_load], 1 + jne .check_drift + + ret ; load_config return point for first load + +.check_drift: + ; warn (and restore the old value) for anything that changed but can't be hot-reloaded + + STREQ document_root, old_document_root, rcx + cmp rcx, 1 + je .drift_docroot_ok + + lea rsi, [rel key_docroot] + call .warn_boot_only + + lea rsi, [rel old_document_root] + lea rdi, [rel document_root] + call .copy_str + +.drift_docroot_ok: + STREQ bind_addr_str, old_bind_addr_str, rcx + cmp rcx, 1 + je .drift_bindaddr_ok + + lea rsi, [rel key_bindaddr] + call .warn_boot_only + + lea rsi, [rel old_bind_addr_str] + lea rdi, [rel bind_addr_str] + call .copy_str + +.drift_bindaddr_ok: + mov ax, [rel port] + cmp ax, [rel old_port] + je .drift_port_ok + + lea rsi, [rel key_port] + call .warn_boot_only + + mov ax, [rel old_port] + mov [rel port], ax + +.drift_port_ok: + STREQ str_file_path, old_str_file_path, rcx + cmp rcx, 1 + je .drift_logfile_ok + + lea rsi, [rel key_logfile] + call .warn_boot_only + + lea rsi, [rel old_str_file_path] + lea rdi, [rel str_file_path] + call .copy_str + +.drift_logfile_ok: + mov al, [rel use_chroot] + cmp al, [rel old_use_chroot] + je .drift_chroot_ok + + lea rsi, [rel key_use_chroot] + call .warn_boot_only + + mov al, [rel old_use_chroot] + mov [rel use_chroot], al + +.drift_chroot_ok: + mov al, [rel be_nobody] + cmp al, [rel old_be_nobody] + je .drift_nobody_ok + + lea rsi, [rel key_noperms] + call .warn_boot_only + + mov al, [rel old_be_nobody] + mov [rel be_nobody], al + +.drift_nobody_ok: + ; rebuild errordoc full paths (document_root + errordoc_*), in case either changed + BUILDPATH errordoc_405_path, document_root, errordoc_405 + BUILDPATH errordoc_404_path, document_root, errordoc_404 + BUILDPATH errordoc_403_path, document_root, errordoc_403 + BUILDPATH errordoc_401_path, document_root, errordoc_401 + BUILDPATH errordoc_400_path, document_root, errordoc_400 + + LOG_INFO str_reload_done, str_reload_done_len + + ret ; load_config return point for reloads + +.copy_str: + ; copies a null-terminated string from rsi to rdi + ; Args: + ; rsi: source + ; rdi: dest + ; Clobbers: al, rsi, rdi + mov al, [rsi] + mov [rdi], al + + inc rsi + inc rdi + + test al, al + jnz .copy_str + + ret ; .copy_str return point + +.warn_boot_only: + ; Logs: "Reload: changed but requires a restart, ignoring" + mov r9, rsi ; AAPPEND clobbers rsi + + CLB + lea r8, [rel log_buffer] + + AAPPEND r8, str_reload_p1 + AAPPEND r8, rsi + AAPPEND r8, str_reload_p2 + + lea rcx, [rel log_buffer] + sub r8, rcx + mov rbx, r8 + + LOG_WARNING log_buffer, rbx + + ret ; .warn_boot_only return point + +; log level parsing +.parse_str_level: + lea rax, [rel str_level_str] + + ; "debug" + cmp dword [rax], 'debu' + jne .check_none + + cmp byte [rax+4], 'g' + jne .check_none + + mov byte [rel str_level], 2 + + ret + +.check_none: + ; "none" + cmp dword [rax], 'none' + jne .check_info + + mov byte [rel str_level], 0 + + ret + +.check_info: + ; "info" or anything unrecognized = 0 + mov byte [rel str_level], 1 + ret ; .parse_str_level return point \ No newline at end of file diff --git a/labels/preserve.asm b/labels/preserve.asm index da17de1..bb8860a 100644 --- a/labels/preserve.asm +++ b/labels/preserve.asm @@ -23,8 +23,9 @@ pre_serve: call .sigchld_setup call .sigterm_setup call .sigint_setup + call .sighup_setup - ret ; pre_serve return point + ret ; pre_serve return point .create_socket: ; socket(domain, type, protocol) @@ -279,10 +280,10 @@ pre_serve: ; rt_sigaction(signum, newact, oldact) mov rax, 13 - mov rdi, 2 ; SIGINT + mov rdi, 2 ; SIGINT lea rsi, [rel sigaction] xor rdx, rdx - mov r10, 8 ; "sigsetsize" + mov r10, 8 ; "sigsetsize" syscall mov rdi, rax @@ -305,6 +306,51 @@ pre_serve: mov byte [rel shutdown], 1 ret ; return point for .sigint_handler +.sighup_setup: + ; setups the SIGHUP handler (config reload) + + ; 0 the struct + lea rdi, [rel sigaction] + mov rcx, 152 + xor al, al + rep stosb + + lea rax, [rel .sighup_handler] + mov [rel sigaction], rax ; sa_handler + + mov qword [rel sigaction + 8], 0x04000000 ; sa_flags SA_RESTORER + + lea rax, [rel .sig_restorer] + mov [rel sigaction + 16], rax ; sa_restorer + + ; rt_sigaction(signum, newact, oldact) + mov rax, 13 + mov rdi, 1 ; SIGHUP + lea rsi, [rel sigaction] + xor rdx, rdx + mov r10, 8 ; "sigsetsize" + syscall + + mov rdi, rax + mov rax, 1 ; for warn / debug logs + + cmp rdi, 0 + jl .sighup_fail + + call dbg_sighandler_success + + jmp .sighup_end + +.sighup_fail: + call warn_sighandler_fail + +.sighup_end: + ret ; return point for .sighup_setup + +.sighup_handler: + mov byte [rel reload], 1 + ret ; return point for .sighup_handler + .fail_socket: LOG_ERR str_fail_socket, str_fail_socket_len mov rdi, rax diff --git a/labels/strings.asm b/labels/strings.asm index 92eebc5..79519be 100644 --- a/labels/strings.asm +++ b/labels/strings.asm @@ -153,9 +153,16 @@ section .text str_fail_sighandler_p1 db "Failed to register the ", 0 str_fail_sighandler_p2 db ", continuing anyways...", 0 + str_reload_p1 db "Reload: ", 0 + str_reload_p2 db " changed but requires a restart, ignoring", 0 + + str_reload_done db "Config reloaded", 0 + str_reload_done_len equ $ - str_reload_done - 1 + str_sighanlder_sigterm db "SIGTERM", 0 str_sighanlder_sigint db "SIGINT", 0 str_sighanlder_sigchld db "SIGCHLD", 0 + str_sighanlder_sighup db "SIGHUP", 0 str_process_started_p1 db "Main process started by UID ", 0 str_process_started_p2 db " with PID ", 0 diff --git a/program.asm b/program.asm index b361326..a3fe5ee 100644 --- a/program.asm +++ b/program.asm @@ -13,6 +13,7 @@ %include "./labels/debug.asm" %include "./labels/flagparser.asm" %include "./labels/initialsetup.asm" +%include "./labels/loadenv.asm" %include "./labels/preserve.asm" %include "./labels/startupchecks.asm" @@ -63,6 +64,7 @@ section .bss ; system process_count resw 1 ; current processes count shutdown resb 1 ; if a shutdown was requested by a signal + reload resb 1 ; if a config reload was requested by a signal ; network client_addr resb 16 @@ -145,6 +147,9 @@ _start: cmp byte [rel shutdown], 1 je .shutdown + cmp byte [rel reload], 1 + je .reload + cmp rax, -4 je .wait ; -4 = EINTR, stopped by signal, just restart @@ -842,6 +847,11 @@ _start: EXIT 0 +.reload: + mov byte [rel reload], 0 + call load_config ; from labels/envreload.asm + jmp .wait + .fail_accept: LOG_ERR str_fail_accept, str_fail_accept_len jmp .wait ; child exits