#!/bin/sh /etc/rc.common # UCI config_get assigns variables; rc.common consumes START and USE_PROCD. # The && chains deliberately handle failure from every command. # shellcheck disable=SC2034,SC2154,SC2015 . /usr/share/libubox/jshn.sh . /usr/share/openlist2/functions.sh . /usr/share/openlist2/tls.sh START=99 USE_PROCD=1 PROG=/usr/bin/openlist2 append_dest_port() { local value="$1" [ -n "$value" ] && [ "$value" != "-1" ] || return 0 dest_ports="${dest_ports:+$dest_ports }$value" } json_add_filter_object() { json_add_object "" json_add_string "cidr" "$1" json_add_string "path" "$2" json_add_string "method" "$3" json_close_object } json_add_csv_array() { local name="$1" local values="$2" local oldifs="$IFS" local noglob_set=0 local item local count=0 json_add_array "$name" case "$-" in *f*) noglob_set=1 ;; *) set -f ;; esac IFS=',' for item in $values; do IFS="$oldifs" item="$(printf '%s' "$item" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" IFS=',' if [ -n "$item" ]; then json_add_string "" "$item" count=$((count + 1)) fi done IFS="$oldifs" [ "$noglob_set" = "1" ] || set +f [ "$count" -eq 0 ] && json_add_string "" "*" json_close_array } get_config() { # LuCI manages the first section and procd runs one instance. [ -z "$section_id" ] || return 0 section_id="$1" uci_changed=0 config_get_bool enabled "$1" enabled 1 config_get_bool debug "$1" debug 0 config_get_bool allow_wan "$1" allow_wan 0 config_get delayed_start "$1" delayed_start 0 config_get_bool force "$1" force 1 config_get site_url "$1" site_url "" config_get cdn "$1" cdn "" config_get jwt_secret "$1" jwt_secret "" config_get data_dir "$1" data_dir "/etc/openlist2" config_get temp_dir "$1" temp_dir "/tmp/openlist2" config_get bleve_dir "$1" bleve_dir "" config_get dist_dir "$1" dist_dir "" config_get token_expires_in "$1" token_expires_in 48 config_get proxy_address "$1" proxy_address "" config_get auto_memory_limit "$1" auto_memory_limit 4 config_get min_free_memory "$1" min_free_memory 0 config_get max_block_limit "$1" max_block_limit 0 config_get max_connections "$1" max_connections 0 config_get max_concurrency "$1" max_concurrency 64 config_get_bool tls_insecure_skip_verify "$1" tls_insecure_skip_verify 0 # log config_get_bool log "$1" log 1 config_get log_path "$1" log_path '/var/log/openlist2.log' if ! openlist2_safe_log_path "$log_path"; then log_path='/var/log/openlist2.log' uci -q set "openlist2.$section_id.log_path=$log_path" || { config_error=1; return 1; } uci_changed=1 fi config_get log_max_size "$1" log_max_size 1 config_get log_max_backups "$1" log_max_backups 3 config_get log_max_age "$1" log_max_age 28 config_get_bool log_compress "$1" log_compress 0 config_get_bool log_filter "$1" log_filter 0 config_get log_filter_cidr "$1" log_filter_cidr "" config_get log_filter_path "$1" log_filter_path "" config_get log_filter_method "$1" log_filter_method "" # scheme config_get_bool force_https "$1" force_https 0 config_get ssl_cert "$1" ssl_cert "" config_get ssl_key "$1" ssl_key "" config_get listen_addr "$1" listen_addr "0.0.0.0" config_get listen_http_port "$1" listen_http_port "5244" config_get listen_https_port "$1" listen_https_port "-1" config_get listen_unix_file "$1" listen_unix_file "" config_get listen_unix_file_perm "$1" listen_unix_file_perm "" config_get_bool listen_enable_h2c "$1" listen_enable_h2c 0 config_get_bool listen_enable_h3 "$1" listen_enable_h3 0 # database config_get database_type "$1" database_type "sqlite3" config_get mysql_host "$1" mysql_host "" config_get mysql_port "$1" mysql_port "0" case "$database_type:$mysql_port" in mysql:|mysql:0) mysql_port="3306" ;; postgres:|postgres:0) mysql_port="5432" ;; esac config_get mysql_username "$1" mysql_username "" config_get mysql_password "$1" mysql_password "" config_get mysql_database "$1" mysql_database "" config_get mysql_table_prefix "$1" mysql_table_prefix "x_" config_get mysql_ssl_mode "$1" mysql_ssl_mode "" config_get mysql_dsn "$1" mysql_dsn "" config_get meilisearch_host "$1" meilisearch_host "http://localhost:7700" config_get meilisearch_api_key "$1" meilisearch_api_key "" config_get meilisearch_index "$1" meilisearch_index "openlist" # tasks config_get download_workers "$1" download_workers 5 config_get download_max_retry "$1" download_max_retry 1 config_get_bool download_task_persistant "$1" download_task_persistant 0 config_get transfer_workers "$1" transfer_workers 5 config_get transfer_max_retry "$1" transfer_max_retry 2 config_get_bool transfer_task_persistant "$1" transfer_task_persistant 0 config_get upload_workers "$1" upload_workers 5 config_get upload_max_retry "$1" upload_max_retry 0 config_get copy_workers "$1" copy_workers 5 config_get copy_max_retry "$1" copy_max_retry 2 config_get_bool copy_task_persistant "$1" copy_task_persistant 0 config_get move_workers "$1" move_workers 5 config_get move_max_retry "$1" move_max_retry 2 config_get_bool move_task_persistant "$1" move_task_persistant 0 config_get decompress_workers "$1" decompress_workers 5 config_get decompress_max_retry "$1" decompress_max_retry 2 config_get_bool decompress_task_persistant "$1" decompress_task_persistant 0 config_get decompress_upload_workers "$1" decompress_upload_workers 5 config_get decompress_upload_max_retry "$1" decompress_upload_max_retry 2 config_get_bool allow_retry_canceled "$1" allow_retry_canceled 0 # cors config_get cors_allow_origins "$1" cors_allow_origins '*' config_get cors_allow_methods "$1" cors_allow_methods '*' config_get cors_allow_headers "$1" cors_allow_headers '*' # s3 config_get_bool s3 "$1" s3 0 config_get s3_port "$1" s3_port 5246 config_get_bool s3_ssl "$1" s3_ssl 0 # ftp config_get_bool ftp "$1" ftp 0 config_get ftp_port "$1" ftp_port 5221 config_get ftp_pasv_ports "$1" ftp_pasv_ports "" config_get find_pasv_port_attempts "$1" find_pasv_port_attempts 50 config_get_bool active_transfer_port_non_20 "$1" active_transfer_port_non_20 0 config_get idle_timeout "$1" idle_timeout 900 config_get connection_timeout "$1" connection_timeout 30 config_get_bool disable_active_mode "$1" disable_active_mode 0 config_get_bool default_transfer_binary "$1" default_transfer_binary 0 config_get_bool enable_active_conn_ip_check "$1" enable_active_conn_ip_check 1 config_get_bool enable_pasv_conn_ip_check "$1" enable_pasv_conn_ip_check 1 # sftp config_get_bool sftp "$1" sftp 0 config_get sftp_port "$1" sftp_port 5222 # mcp config_get_bool mcp "$1" mcp 0 # init jwt_secret if [ -z "$jwt_secret" ]; then jwt_secret=$(tr -cd "a-zA-Z0-9" < "/dev/urandom" | head -c16) [ "${#jwt_secret}" -eq 16 ] || { config_error=1; return 1; } uci -q set "openlist2.$section_id.jwt_secret=$jwt_secret" || { config_error=1; return 1; } uci_changed=1 fi if [ "$uci_changed" = "1" ]; then uci commit openlist2 || { config_error=1; return 1; } fi return 0 } validate_config() { http_port="$(openlist2_port "${listen_http_port:-5244}" 1)" && https_port="$(openlist2_port "${listen_https_port:--1}" 1)" && s3_port="$(openlist2_port "$s3_port")" && ftp_port="$(openlist2_port "$ftp_port")" && sftp_port="$(openlist2_port "$sftp_port")" || { openlist2_error "Invalid listen port. Use 1-65535, or -1 to disable HTTP/HTTPS."; return 1; } if [ "$ftp" = "1" ] && [ "$allow_wan" = "1" ]; then ftp_pasv_ports="$(openlist2_pasv_ports "$ftp_pasv_ports")" || { openlist2_error "FTP passive firewall ports must be ports or ranges within 1024-65535."; return 1; } else ftp_pasv_ports="" fi if [ "$https_port" = "-1" ] && { [ "$listen_enable_h3" = "1" ] || [ "$force_https" = "1" ]; }; then openlist2_error "Force HTTPS and HTTP/3 require an enabled HTTPS port." return 1 fi if [ -n "$listen_unix_file" ]; then case "$listen_unix_file" in /*) ;; *) openlist2_error "Unix socket path must be absolute."; return 1 ;; esac [ ! -e "$listen_unix_file" ] || [ -S "$listen_unix_file" ] || { openlist2_error "Unix socket path is occupied by a non-socket file."; return 1; } fi if [ -n "$listen_unix_file_perm" ]; then printf '%s\n' "$listen_unix_file_perm" | grep -Eq '^[0-7]{3,4}$' || { openlist2_error "Unix socket permissions must contain 3 or 4 octal digits."; return 1; } fi local ports="" port item first last dest_ports="" append_dest_port "$http_port" append_dest_port "$https_port" [ "$s3" = "1" ] && append_dest_port "$s3_port" [ "$ftp" = "1" ] && append_dest_port "$ftp_port" [ "$sftp" = "1" ] && append_dest_port "$sftp_port" for port in $dest_ports; do case " $ports " in *" $port "*) openlist2_error "Enabled services cannot share TCP port $port."; return 1 ;; esac ports="$ports $port" if [ "$ftp" = "1" ]; then for item in $ftp_pasv_ports; do first="${item%%-*}"; last="${item#*-}" if [ "$port" -ge "$first" ] && [ "$port" -le "$last" ]; then openlist2_error "FTP passive ports must not overlap service port $port."; return 1 fi done fi done if [ "$log" = "1" ] && ! [ "$log_max_size" -ge 1 ] 2>/dev/null; then openlist2_error "Log size must be at least 1 MB." return 1 fi openlist2_validate_dirs && openlist2_configure_tls } write_config() ( local config_tmp umask 077 config_tmp="$(mktemp "$data_dir/.config.json.XXXXXX")" || exit 1 trap 'rm -f "$config_tmp"' 0 trap 'exit 1' HUP INT TERM json_dump > "$config_tmp" && jsonfilter -qi "$config_tmp" -e '@' >/dev/null && chmod 600 "$config_tmp" && mv -f "$config_tmp" "$data_dir/config.json" ) set_firewall_rule() { local section="$1" name="$2" proto="$3" ports="$4" if [ -z "$ports" ]; then if uci -q get "firewall.$section" >/dev/null; then uci -q delete "firewall.$section" || return 1 firewall_changed=1 fi return 0 fi if [ "$(uci -q get "firewall.$section")" = "rule" ] && [ "$(uci -q get "firewall.$section.name")" = "$name" ] && [ "$(uci -q get "firewall.$section.src")" = "wan" ] && [ "$(uci -q get "firewall.$section.target")" = "ACCEPT" ] && [ "$(uci -q get "firewall.$section.proto")" = "$proto" ] && [ "$(uci -q get "firewall.$section.dest_port")" = "$ports" ] && [ "$(uci -q get "firewall.$section.enabled")" = "1" ]; then return 0 fi uci -q delete "firewall.$section" uci set "firewall.$section=rule" && uci set "firewall.$section.name=$name" && uci set "firewall.$section.target=ACCEPT" && uci set "firewall.$section.src=wan" && uci set "firewall.$section.proto=$proto" && uci set "firewall.$section.dest_port=$ports" && uci set "firewall.$section.enabled=1" || return 1 firewall_changed=1 } set_firewall() { local quic_ports="" legacy legacy_ports firewall_changed=0 legacy="$(uci -q get 'openlist2.@openlist2[0].legacy_firewall_port')" legacy_ports="$(uci -q get firewall.openlist.dest_port)" # Only retire an old rule when the migrated config and rule fingerprint agree. if [ -n "$legacy" ] && [ "$(uci -q get firewall.openlist.name)" = "openlist" ] && [ "$(uci -q get firewall.openlist.src)" = "wan" ] && [ "$(uci -q get firewall.openlist.target)" = "ACCEPT" ] && [ "$(uci -q get firewall.openlist.proto)" = "tcp" ]; then case " $legacy_ports " in *" $legacy "*) uci -q delete firewall.openlist || return 1 firewall_changed=1 ;; esac fi dest_ports="" if [ "$external_access" = "allow" ]; then append_dest_port "$http_port" append_dest_port "$https_port" [ "$s3" = "1" ] && append_dest_port "$s3_port" if [ "$ftp" = "1" ]; then append_dest_port "$ftp_port" append_dest_port "$ftp_pasv_ports" fi [ "$sftp" = "1" ] && append_dest_port "$sftp_port" [ "$listen_enable_h3" = "1" ] && quic_ports="$https_port" fi set_firewall_rule openlist2 openlist2 tcp "$dest_ports" && set_firewall_rule openlist2_quic "openlist2 QUIC" udp "$quic_ports" || return 1 if [ "$firewall_changed" = "1" ] || uci -q changes firewall | grep -Eq '^[-+]?firewall\.openlist'; then uci commit firewall && /etc/init.d/firewall reload || { openlist2_error "Unable to apply OpenList firewall rules." return 1 } fi if [ -n "$legacy" ]; then uci -q delete 'openlist2.@openlist2[0].legacy_firewall_port' && uci commit openlist2 || return 1 fi return 0 } start_openlist2() { openlist2_migrate_config || return 1 config_load openlist2 || return 1 section_id="" config_error=0 config_foreach get_config openlist2 [ -n "$section_id" ] && [ "$config_error" = "0" ] || { openlist2_error "Unable to load or save OpenList configuration."; return 1; } if [ "$enabled" != "1" ]; then external_access="deny" set_firewall return $? fi validate_config && openlist2_prepare_dirs && openlist2_prepare_tls || return 1 if [ -e /proc/uptime ]; then boot_uptime="$(awk -F. '{print $1}' /proc/uptime)" [ "$boot_uptime" -lt "120" ] || delayed_start=0 fi [ "$allow_wan" -eq "1" ] && external_access="allow" || external_access="deny" if [ "$log" = "1" ]; then openlist2_safe_log_path "$log_path" || { openlist2_error "Unsafe log path or non-regular log file."; return 1; } log_dir="${log_path%/*}" mkdir -p "$log_dir" && : >> "$log_path" && chmod 600 "$log_path" || { openlist2_error "Unable to open the log file."; return 1; } fi last_launched_version="" if command -v jsonfilter >/dev/null 2>&1 && [ -s "$data_dir/config.json" ]; then last_launched_version="$(jsonfilter -qi "$data_dir/config.json" -e '@.last_launched_version' 2>/dev/null)" fi # init config json_init json_add_boolean "force" "$force" json_add_string "site_url" "$site_url" json_add_string "cdn" "$cdn" json_add_string "jwt_secret" "$jwt_secret" json_add_int "token_expires_in" "$token_expires_in" # database json_add_object 'database' json_add_string "type" "$database_type" json_add_string "host" "$mysql_host" json_add_int "port" "$mysql_port" json_add_string "user" "$mysql_username" json_add_string "password" "$mysql_password" json_add_string "name" "$mysql_database" json_add_string "db_file" "$data_dir/data.db" json_add_string "table_prefix" "$mysql_table_prefix" json_add_string "ssl_mode" "$mysql_ssl_mode" json_add_string "dsn" "$mysql_dsn" json_close_object # meilisearch json_add_object "meilisearch" json_add_string "host" "$meilisearch_host" json_add_string "api_key" "$meilisearch_api_key" json_add_string "index" "$meilisearch_index" json_close_object # scheme json_add_object "scheme" json_add_string "address" "$listen_addr" json_add_int "http_port" "$http_port" json_add_int "https_port" "$https_port" json_add_boolean "force_https" "$force_https" json_add_string "cert_file" "$ssl_cert" json_add_string "key_file" "$ssl_key" json_add_string "unix_file" "$listen_unix_file" json_add_string "unix_file_perm" "$listen_unix_file_perm" json_add_boolean "enable_h2c" "$listen_enable_h2c" json_add_boolean "enable_h3" "$listen_enable_h3" json_close_object json_add_string "temp_dir" "$temp_dir" json_add_string "bleve_dir" "$bleve_dir" json_add_string "dist_dir" "$dist_dir" # log json_add_object "log" json_add_boolean "enable" "$log" json_add_string "name" "$log_path" json_add_int "max_size" "$log_max_size" json_add_int "max_backups" "$log_max_backups" json_add_int "max_age" "$log_max_age" json_add_boolean "compress" "$log_compress" json_add_object "filter" json_add_boolean "enable" "$log_filter" json_add_array "filters" json_add_filter_object "" "/ping" "" json_add_filter_object "" "" "HEAD" json_add_filter_object "" "/dav/" "PROPFIND" if [ -n "$log_filter_cidr" ] || [ -n "$log_filter_path" ] || [ -n "$log_filter_method" ]; then json_add_filter_object "$log_filter_cidr" "$log_filter_path" "$log_filter_method" fi json_close_array json_close_object json_close_object json_add_int "delayed_start" "$delayed_start" json_add_int "auto_memory_limit" "$auto_memory_limit" json_add_int "min_free_memory" "$min_free_memory" json_add_int "max_block_limit" "$max_block_limit" json_add_int "max_connections" "$max_connections" json_add_int "max_concurrency" "$max_concurrency" json_add_boolean "tls_insecure_skip_verify" "$tls_insecure_skip_verify" # tasks json_add_object "tasks" json_add_object "download" json_add_int "workers" "$download_workers" json_add_int "max_retry" "$download_max_retry" json_add_boolean "task_persistant" "$download_task_persistant" json_close_object json_add_object "transfer" json_add_int "workers" "$transfer_workers" json_add_int "max_retry" "$transfer_max_retry" json_add_boolean "task_persistant" "$transfer_task_persistant" json_close_object json_add_object "upload" json_add_int "workers" "$upload_workers" json_add_int "max_retry" "$upload_max_retry" json_add_boolean "task_persistant" 0 json_close_object json_add_object "copy" json_add_int "workers" "$copy_workers" json_add_int "max_retry" "$copy_max_retry" json_add_boolean "task_persistant" "$copy_task_persistant" json_close_object json_add_object "move" json_add_int "workers" "$move_workers" json_add_int "max_retry" "$move_max_retry" json_add_boolean "task_persistant" "$move_task_persistant" json_close_object json_add_object "decompress" json_add_int "workers" "$decompress_workers" json_add_int "max_retry" "$decompress_max_retry" json_add_boolean "task_persistant" "$decompress_task_persistant" json_close_object json_add_object "decompress_upload" json_add_int "workers" "$decompress_upload_workers" json_add_int "max_retry" "$decompress_upload_max_retry" json_add_boolean "task_persistant" 0 json_close_object json_add_boolean "allow_retry_canceled" "$allow_retry_canceled" json_close_object # cors json_add_object "cors" json_add_csv_array "allow_origins" "$cors_allow_origins" json_add_csv_array "allow_methods" "$cors_allow_methods" json_add_csv_array "allow_headers" "$cors_allow_headers" json_close_object # s3 json_add_object "s3" json_add_boolean "enable" "$s3" json_add_int "port" "$s3_port" json_add_boolean "ssl" "$s3_ssl" json_close_object # ftp json_add_object "ftp" json_add_boolean "enable" "$ftp" json_add_string "listen" ":$ftp_port" json_add_int "find_pasv_port_attempts" "$find_pasv_port_attempts" json_add_boolean "active_transfer_port_non_20" "$active_transfer_port_non_20" json_add_int "idle_timeout" "$idle_timeout" json_add_int "connection_timeout" "$connection_timeout" json_add_boolean "disable_active_mode" "$disable_active_mode" json_add_boolean "default_transfer_binary" "$default_transfer_binary" json_add_boolean "enable_active_conn_ip_check" "$enable_active_conn_ip_check" json_add_boolean "enable_pasv_conn_ip_check" "$enable_pasv_conn_ip_check" json_close_object # sftp json_add_object "sftp" json_add_boolean "enable" "$sftp" json_add_string "listen" ":$sftp_port" json_close_object # mcp json_add_object "mcp" json_add_boolean "enable" "$mcp" json_close_object json_add_string "last_launched_version" "$last_launched_version" json_add_string "proxy_address" "$proxy_address" write_config || { openlist2_error "Unable to atomically save config.json; service was not started."; return 1; } set_firewall || return 1 procd_open_instance openlist2 procd_set_param env OPENLIST_ADMIN_PASSWORD=admin procd_set_param command $PROG procd_append_param command server procd_append_param command --data "$data_dir" procd_append_param command --no-prefix [ "$debug" = "1" ] && procd_append_param command --debug procd_set_param stderr 1 [ "$debug" = "1" ] && procd_set_param stdout 1 procd_set_param respawn procd_set_param limits core="unlimited" procd_set_param limits nofile="200000 200000" procd_close_instance openlist2 } start_service() { # rc.common closes the procd request even if start_service fails. # Preserve our result for its service_started hook instead of losing it. openlist2_start_status=0 start_openlist2 "$@" || openlist2_start_status=$? return "$openlist2_start_status" } service_started() { return "${openlist2_start_status:-1}" } service_triggers() { procd_add_reload_trigger "openlist2" } reload_service() { stop start } stop_service() { external_access="deny" set_firewall }