diff --git a/frp/Makefile b/frp/Makefile deleted file mode 100644 index 22604ae3..00000000 --- a/frp/Makefile +++ /dev/null @@ -1,80 +0,0 @@ -include $(TOPDIR)/rules.mk - -PKG_NAME:=frp -PKG_VERSION:=0.70.1 -PKG_RELEASE:=3 - -PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=skip - -PKG_MAINTAINER:=Alexandru Ardelean -PKG_LICENSE:=Apache-2.0 -PKG_LICENSE_FILES:=LICENSE - -PKG_BUILD_DEPENDS:=golang/host node/host -PKG_BUILD_PARALLEL:=1 -PKG_BUILD_FLAGS:=no-mips16 - -GO_PKG:=github.com/fatedier/frp -GO_PKG_BUILD_PKG:=github.com/fatedier/frp/cmd/... -GO_PKG_LDFLAGS:=-s -w -GO_PKG_LDFLAGS_X:=github.com/fatedier/frp/pkg/util/version.version=$(PKG_VERSION) - -include $(INCLUDE_DIR)/package.mk -include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk - -define Build/Compile - ( \ - $(MAKE) -C $(PKG_BUILD_DIR)/web/frpc install ; \ - $(MAKE) -C $(PKG_BUILD_DIR)/web/frps install ; \ - $(MAKE) -C $(PKG_BUILD_DIR) web ; \ - $(call GoPackage/Build/Compile) ; \ - ) -endef - -define Package/frp/install - $(INSTALL_DIR) $(1)/usr/bin/ - $(INSTALL_DIR) $(1)/etc/config/ - $(INSTALL_DIR) $(1)/etc/frp/$(2).d/ - $(INSTALL_DIR) $(1)/etc/init.d/ - - $(INSTALL_BIN) $(GO_PKG_BUILD_BIN_DIR)/$(2) $(1)/usr/bin/ - $(INSTALL_CONF) ./files/$(2).config $(1)/etc/config/$(2) - $(INSTALL_BIN) ./files/$(2).init $(1)/etc/init.d/$(2) - - if [ -r ./files/$(2).uci-defaults ]; then \ - $(INSTALL_DIR) $(1)/etc/uci-defaults; \ - $(INSTALL_DATA) ./files/$(2).uci-defaults $(1)/etc/uci-defaults/$(2); \ - fi -endef - -define Package/frp/template - define Package/$(1) - SECTION:=net - CATEGORY:=Network - SUBMENU:=Web Servers/Proxies - TITLE:=$(1) - fast reverse proxy $(2) - URL:=https://github.com/fatedier/frp - DEPENDS:=$(GO_ARCH_DEPENDS) - endef - - define Package/$(1)/description - $(1) is a fast reverse proxy $(2) that helps you expose a local server - behind a NAT or firewall to the internet. - endef - - define Package/$(1)/conffiles -/etc/config/$(1) -/etc/frp/$(1).d/ - endef - - define Package/$(1)/install - $(call Package/frp/install,$$(1),$(1)) - endef -endef - -$(eval $(call Package/frp/template,frpc,client)) -$(eval $(call Package/frp/template,frps,server)) -$(eval $(call BuildPackage,frpc)) -$(eval $(call BuildPackage,frps)) diff --git a/frp/files/frpc.config b/frp/files/frpc.config deleted file mode 100644 index 333caa10..00000000 --- a/frp/files/frpc.config +++ /dev/null @@ -1,64 +0,0 @@ -config init - option stdout '1' - option stderr '1' -# Uncomment to run frpc as an existing user/group. Keep disabled by -# default to avoid permission issues with certificate, log and included -# config files. -# option user 'nobody' -# option group 'nogroup' - option respawn '1' -# For full configuration options, see: -# https://github.com/fatedier/frp/blob/master/conf/frpc_full_example.toml -# -# Additional config files should be readable root-level TOML fragments. -# The service will refuse to start if a listed fragment is missing or outside /etc/frp/frpc.d/. -# Use frp's own includes option or per-proxy raw settings for proxy/visitor tables. -# list conf_inc '/etc/frp/frpc.d/frpc_extra.toml' - -config conf 'common' - option server_addr '127.0.0.1' - option server_port '7000' - option authentication_method 'token' -# option token 'your_token' -# Alternatively, load a token from a file or command. Exec token sources -# require frpc to run with --allow-unsafe=TokenSourceExec; the init script -# adds that flag automatically when token_source_type is exec. -# option token_source_type 'file' -# option token_source_file_path '/etc/frp/client_token' -# option token_source_type 'exec' -# option token_source_exec_command '/usr/bin/get-frpc-token' -# list token_source_exec_args '--format' -# list token_source_exec_args 'raw' -# list token_source_exec_env 'TOKEN_SERVICE=production' - option login_fail_exit 'true' - option protocol 'tcp' - option wire_protocol 'v1' - option tcp_mux 'true' - option tls_enable 'true' - option disable_custom_tls_first_byte 'true' - # Web server is disabled when admin_port is empty or 0. -# option admin_addr '127.0.0.1' -# option admin_port '7400' -# option admin_user 'admin' -# option admin_pwd 'admin' - option admin_tls_enable 'false' - option pprof_enable 'false' -# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt' -# option admin_tls_key_file '/etc/ssl/acme/example.com.key' - option log_file 'console' - option log_level 'info' - option log_max_days '3' -# Uncomment to enable runtime proxy/visitor persistence via frpc web UI or API. -# option store_path '/etc/frp/frpc_store.json' -# List options with name "_" will be directly appended as raw TOML lines. -# Use this only for options not covered by UCI options above. -# Do not duplicate keys generated by UCI options above. -# list _ 'uncovered.option = "value"' - -config conf 'ssh' - option name 'ssh' - option type 'tcp' - option local_ip '127.0.0.1' - option local_port '22' - option remote_port '6000' - option enabled 'true' diff --git a/frp/files/frpc.init b/frp/files/frpc.init deleted file mode 100644 index 62c2062f..00000000 --- a/frp/files/frpc.init +++ /dev/null @@ -1,1003 +0,0 @@ -#!/bin/sh /etc/rc.common - -START=99 -USE_PROCD=1 - -NAME=frpc -PROG=/usr/bin/$NAME -CONF_FILE=/var/etc/$NAME.toml - -_err() { - echo "$*" >&2 - logger -p daemon.err -t "$NAME" "$*" -} - -_trim() { - printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' -} - -_toml_escape() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -_toml_quote() { - printf '"%s"' "$(_toml_escape "$1")" -} - -_toml_key_quote() { - _toml_quote "$1" -} - -_toml_bool() { - local v - v="$(printf '%s' "$1" | tr 'A-Z' 'a-z')" - - case "$v" in - 1|true|yes|on|enabled) - printf 'true' - ;; - 0|false|no|off|disabled) - printf 'false' - ;; - *) - printf 'false' - ;; - esac -} - -_TOML_ERR=0 -_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 - -_is_uinteger() { - case "$1" in - ''|*[!0-9]*) - return 1 - ;; - esac - - return 0 -} - -_is_integer() { - local value="$1" - - case "$value" in - +*|-*) - value="${value#?}" - ;; - esac - - _is_uinteger "$value" -} - -_is_port_value() { - _is_uinteger "$1" || return 1 - [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null -} - -_is_port_or_zero_value() { - _is_uinteger "$1" || return 1 - [ "$1" -ge 0 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null -} - -_is_negative_integer() { - local value - - case "$1" in - -*) - value="${1#-}" - _is_uinteger "$value" && [ "$value" -gt 0 ] 2>/dev/null - ;; - *) - return 1 - ;; - esac -} - -_is_visitor_bind_port_value() { - _is_negative_integer "$1" && return 0 - _is_port_value "$1" -} - -_toml_line() { - local key="$1" - local value="$2" - local type="$3" - - [ -z "$value" ] && return 0 - - case "$type" in - bool) - printf '%s = %s\n' "$key" "$(_toml_bool "$value")" - ;; - int|integer|number) - if ! _is_integer "$value"; then - _err "invalid integer for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - port) - if ! _is_port_value "$value"; then - _err "invalid port for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - port0) - if ! _is_port_or_zero_value "$value"; then - _err "invalid port for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - visitor_bind_port) - if ! _is_visitor_bind_port_value "$value"; then - _err "invalid visitor bind port for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - *) - printf '%s = %s\n' "$key" "$(_toml_quote "$value")" - ;; - esac -} - -_emit_opt() { - local section="$1" - local option="$2" - local toml_key="$3" - local type="$4" - local value - - config_get value "$section" "$option" - _toml_line "$toml_key" "$value" "$type" -} - -_TOML_ARRAY= -_TOML_HAS_LIST=0 - -_toml_array_add() { - local item="$1" - - item="$(_trim "$item")" - [ -z "$item" ] && return 0 - - if [ -n "$_TOML_ARRAY" ]; then - _TOML_ARRAY="${_TOML_ARRAY}, " - fi - - _TOML_ARRAY="${_TOML_ARRAY}$(_toml_quote "$item")" -} - -_collect_array_item() { - _TOML_HAS_LIST=1 - _toml_array_add "$1" -} - -_collect_array_option() { - local section="$1" - local option="$2" - local scalar item - - _TOML_ARRAY= - _TOML_HAS_LIST=0 - - config_list_foreach "$section" "$option" _collect_array_item - - if [ "$_TOML_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - - while [ -n "$scalar" ]; do - case "$scalar" in - *,*) - item="${scalar%%,*}" - scalar="${scalar#*,}" - ;; - *) - item="$scalar" - scalar= - ;; - esac - - _toml_array_add "$item" - done - fi - - return 0 -} - -_emit_array_opt() { - local section="$1" - local option="$2" - local toml_key="$3" - - _collect_array_option "$section" "$option" - - [ -n "$_TOML_ARRAY" ] || return 0 - printf '%s = [%s]\n' "$toml_key" "$_TOML_ARRAY" -} - -_emit_kv_pair() { - local prefix="$1" - local line="$2" - local value_type="$3" - local key value toml_key - - case "$line" in - *=*) - key="${line%%=*}" - value="${line#*=}" - ;; - *) - return 0 - ;; - esac - - key="$(_trim "$key")" - value="$(_trim "$value")" - - [ -z "$key" ] && return 0 - - toml_key="${prefix}.$(_toml_key_quote "$key")" - - case "$value_type" in - bool) - printf '%s = %s\n' "$toml_key" "$(_toml_bool "$value")" - ;; - int|integer|number) - printf '%s = %s\n' "$toml_key" "$value" - ;; - *) - printf '%s = %s\n' "$toml_key" "$(_toml_quote "$value")" - ;; - esac -} - -_KV_PREFIX= -_KV_TYPE= -_KV_HAS_LIST=0 - -_emit_kv_list_item() { - _KV_HAS_LIST=1 - _emit_kv_pair "$_KV_PREFIX" "$1" "$_KV_TYPE" -} - -_emit_kv_opt() { - local section="$1" - local option="$2" - local prefix="$3" - local value_type="$4" - local scalar - - _KV_PREFIX="$prefix" - _KV_TYPE="${value_type:-string}" - _KV_HAS_LIST=0 - - config_list_foreach "$section" "$option" _emit_kv_list_item - - if [ "$_KV_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - [ -n "$scalar" ] && _emit_kv_pair "$prefix" "$scalar" "$value_type" - fi - - return 0 -} - -_HEADERS_ARRAY= -_HEADERS_HAS_LIST=0 - -_headers_array_add() { - local line="$1" - local key value item - - case "$line" in - *=*) - key="${line%%=*}" - value="${line#*=}" - ;; - *) - return 0 - ;; - esac - - key="$(_trim "$key")" - value="$(_trim "$value")" - - [ -z "$key" ] && return 0 - - item="{ name = $(_toml_quote "$key"), value = $(_toml_quote "$value") }" - - if [ -n "$_HEADERS_ARRAY" ]; then - _HEADERS_ARRAY="${_HEADERS_ARRAY}, " - fi - - _HEADERS_ARRAY="${_HEADERS_ARRAY}${item}" -} - -_collect_header_item() { - _HEADERS_HAS_LIST=1 - _headers_array_add "$1" -} - -_emit_headers_opt() { - local section="$1" - local option="$2" - local toml_key="$3" - local scalar - - _HEADERS_ARRAY= - _HEADERS_HAS_LIST=0 - - config_list_foreach "$section" "$option" _collect_header_item - - if [ "$_HEADERS_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - [ -n "$scalar" ] && _headers_array_add "$scalar" - fi - - [ -n "$_HEADERS_ARRAY" ] || return 0 - printf '%s = [%s]\n' "$toml_key" "$_HEADERS_ARRAY" -} - -_emit_name_value_array_opt() { - _emit_headers_opt "$@" -} - -_RAW_HAS_LIST=0 - -_emit_raw_item() { - _RAW_HAS_LIST=1 - [ -n "$1" ] || return 0 - printf '%s\n' "$1" -} - -_emit_raw_opt() { - local section="$1" - local option="$2" - local scalar - - _RAW_HAS_LIST=0 - config_list_foreach "$section" "$option" _emit_raw_item - - if [ "$_RAW_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - [ -n "$scalar" ] || return 0 - printf '%s\n' "$scalar" - return $? - fi - - return 0 -} - -_emit_auth_scopes() { - local section="$1" - local hb nwc v - - _collect_array_option "$section" auth_additional_scopes - - if [ -z "$_TOML_ARRAY" ]; then - config_get hb "$section" authenticate_heartbeats - config_get nwc "$section" authenticate_new_work_conns - - v="$(_toml_bool "$hb")" - [ "$v" = "true" ] && _toml_array_add "HeartBeats" - - v="$(_toml_bool "$nwc")" - [ "$v" = "true" ] && _toml_array_add "NewWorkConns" - fi - - [ -n "$_TOML_ARRAY" ] || return 0 - printf 'auth.additionalScopes = [%s]\n' "$_TOML_ARRAY" -} - -_emit_admin_web_tls() { - local section="$1" - local enabled cert key - - config_get enabled "$section" admin_tls_enable - - [ "$(_toml_bool "$enabled")" = "true" ] || return 0 - - config_get cert "$section" admin_tls_cert_file - config_get key "$section" admin_tls_key_file - - if [ -z "$cert" ] || [ -z "$key" ]; then - _err "admin_tls_cert_file and admin_tls_key_file are required when admin_tls_enable is enabled" - return 1 - fi - - if [ ! -r "$cert" ]; then - _err "admin TLS certificate file is not readable: $cert" - return 1 - fi - - if [ ! -r "$key" ]; then - _err "admin TLS private key file is not readable: $key" - return 1 - fi - - _toml_line webServer.tls.certFile "$cert" string - _toml_line webServer.tls.keyFile "$key" string - - return 0 -} - -_emit_admin_web() { - local section="$1" - local port addr - - config_get port "$section" admin_port - - # Empty or 0 means web server is disabled. - [ -n "$port" ] && [ "$port" != "0" ] || return 0 - - config_get addr "$section" admin_addr - _toml_line webServer.addr "${addr:-127.0.0.1}" string - _toml_line webServer.port "$port" port - _emit_opt "$section" admin_user webServer.user string - _emit_opt "$section" admin_pwd webServer.password string - _emit_admin_web_tls "$section" || return 1 - _emit_opt "$section" assets_dir webServer.assetsDir string - _emit_opt "$section" pprof_enable webServer.pprofEnable bool - - return 0 -} - -_emit_common() { - local section="common" - local method token token_source_type token_source_file_path - - # Root options - _emit_opt "$section" client_id clientID string - _emit_opt "$section" user user string - _emit_opt "$section" server_addr serverAddr string - _emit_opt "$section" server_port serverPort port - _emit_opt "$section" nat_hole_stun_server natHoleStunServer string - _emit_opt "$section" login_fail_exit loginFailExit bool - _emit_opt "$section" dns_server dnsServer string - _emit_array_opt "$section" start start - _emit_opt "$section" udp_packet_size udpPacketSize int - _emit_array_opt "$section" includes includes - - # Auth - config_get method "$section" authentication_method - config_get token "$section" token - config_get token_source_type "$section" token_source_type - config_get token_source_file_path "$section" token_source_file_path - - [ -z "$method" ] && { [ -n "$token" ] || [ -n "$token_source_type" ]; } && method="token" - - _toml_line auth.method "$method" string - - if [ "$method" = "token" ] || [ -z "$method" ]; then - if [ -n "$token_source_type" ]; then - if [ -n "$token" ]; then - _err "token and token_source_type are mutually exclusive" - return 1 - fi - - case "$token_source_type" in - file) - if [ -z "$token_source_file_path" ]; then - _err "token_source_file_path is required when token_source_type=file" - return 1 - fi - - _toml_line auth.tokenSource.type "$token_source_type" string - _toml_line auth.tokenSource.file.path "$token_source_file_path" string - ;; - - exec) - local token_source_exec_command - - config_get token_source_exec_command "$section" token_source_exec_command - if [ -z "$token_source_exec_command" ]; then - _err "token_source_exec_command is required when token_source_type=exec" - return 1 - fi - - _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=1 - _toml_line auth.tokenSource.type "$token_source_type" string - _toml_line auth.tokenSource.exec.command "$token_source_exec_command" string - _emit_array_opt "$section" token_source_exec_args auth.tokenSource.exec.args - _emit_name_value_array_opt "$section" token_source_exec_env auth.tokenSource.exec.env - ;; - - *) - _err "unsupported token_source_type: $token_source_type" - return 1 - ;; - esac - else - _toml_line auth.token "$token" string - fi - fi - - _emit_auth_scopes "$section" - - if [ "$method" = "oidc" ]; then - _emit_opt "$section" oidc_client_id auth.oidc.clientID string - _emit_opt "$section" oidc_client_secret auth.oidc.clientSecret string - _emit_opt "$section" oidc_audience auth.oidc.audience string - _emit_opt "$section" oidc_scope auth.oidc.scope string - _emit_opt "$section" oidc_token_endpoint_url auth.oidc.tokenEndpointURL string - _emit_kv_opt "$section" oidc_additional_endpoint_params auth.oidc.additionalEndpointParams string - _emit_opt "$section" oidc_trusted_ca_file auth.oidc.trustedCaFile string - _emit_opt "$section" oidc_insecure_skip_verify auth.oidc.insecureSkipVerify bool - _emit_opt "$section" oidc_proxy_url auth.oidc.proxyURL string - fi - - # Transport - _emit_opt "$section" dial_server_timeout transport.dialServerTimeout int - _emit_opt "$section" dial_server_keepalive transport.dialServerKeepalive int - _emit_opt "$section" http_proxy transport.proxyURL string - _emit_opt "$section" pool_count transport.poolCount int - _emit_opt "$section" tcp_mux transport.tcpMux bool - _emit_opt "$section" tcp_mux_keepalive_interval transport.tcpMuxKeepaliveInterval int - _emit_opt "$section" protocol transport.protocol string - _emit_opt "$section" wire_protocol transport.wireProtocol string - _emit_opt "$section" connect_server_local_ip transport.connectServerLocalIP string - _emit_opt "$section" heartbeat_interval transport.heartbeatInterval int - _emit_opt "$section" heartbeat_timeout transport.heartbeatTimeout int - - # QUIC - _emit_opt "$section" quic_keepalive_period transport.quic.keepalivePeriod int - _emit_opt "$section" quic_max_idle_timeout transport.quic.maxIdleTimeout int - _emit_opt "$section" quic_max_incoming_streams transport.quic.maxIncomingStreams int - - # TLS - _emit_opt "$section" tls_enable transport.tls.enable bool - _emit_opt "$section" tls_cert_file transport.tls.certFile string - _emit_opt "$section" tls_key_file transport.tls.keyFile string - _emit_opt "$section" tls_trusted_ca_file transport.tls.trustedCaFile string - _emit_opt "$section" tls_server_name transport.tls.serverName string - _emit_opt "$section" disable_custom_tls_first_byte transport.tls.disableCustomTLSFirstByte bool - - # Web admin server - _emit_admin_web "$section" || return 1 - - # Feature gates / virtual net - _emit_kv_opt "$section" feature_gates featureGates bool - _emit_opt "$section" virtual_net_address virtualNet.address string - _emit_opt "$section" store_path store.path string - - # Client metadata - _emit_kv_opt "$section" metadatas metadatas string - - # Log - _emit_opt "$section" log_file log.to string - _emit_opt "$section" log_level log.level string - _emit_opt "$section" log_max_days log.maxDays int - _emit_opt "$section" disable_log_color log.disablePrintColor bool - - # Raw extra TOML lines kept for manual UCI usage; LuCI intentionally hides this. - _emit_raw_opt "$section" _ -} - -_emit_plugin() { - local section="$1" - local plugin role - - config_get plugin "$section" plugin - config_get role "$section" role - - [ -z "$plugin" ] && return 0 - - _toml_line plugin.type "$plugin" string - - case "$plugin" in - http_proxy) - _emit_opt "$section" plugin_http_user plugin.httpUser string - _emit_opt "$section" plugin_http_passwd plugin.httpPassword string - ;; - - socks5) - _emit_opt "$section" plugin_user plugin.username string - _emit_opt "$section" plugin_passwd plugin.password string - ;; - - unix_domain_socket) - _emit_opt "$section" plugin_unix_path plugin.unixPath string - ;; - - static_file) - _emit_opt "$section" plugin_local_path plugin.localPath string - _emit_opt "$section" plugin_strip_prefix plugin.stripPrefix string - _emit_opt "$section" plugin_http_user plugin.httpUser string - _emit_opt "$section" plugin_http_passwd plugin.httpPassword string - ;; - - https2http|https2https) - _emit_opt "$section" plugin_local_addr plugin.localAddr string - _emit_opt "$section" plugin_crt_path plugin.crtPath string - _emit_opt "$section" plugin_key_path plugin.keyPath string - _emit_opt "$section" plugin_host_header_rewrite plugin.hostHeaderRewrite string - _emit_opt "$section" plugin_enable_http2 plugin.enableHTTP2 bool - _emit_kv_opt "$section" plugin_request_headers plugin.requestHeaders.set string - ;; - - http2https|http2http) - _emit_opt "$section" plugin_local_addr plugin.localAddr string - _emit_opt "$section" plugin_host_header_rewrite plugin.hostHeaderRewrite string - _emit_kv_opt "$section" plugin_request_headers plugin.requestHeaders.set string - ;; - - tls2raw) - _emit_opt "$section" plugin_local_addr plugin.localAddr string - _emit_opt "$section" plugin_crt_path plugin.crtPath string - _emit_opt "$section" plugin_key_path plugin.keyPath string - ;; - - virtual_net) - [ "$role" = "visitor" ] && \ - _emit_opt "$section" plugin_destination_ip plugin.destinationIP string - ;; - esac - - return 0 -} - -_emit_proxy() { - local section="$1" - local pname ptype plugin hct - - [ "$section" = "common" ] && return 0 - - config_get pname "$section" name "$section" - config_get ptype "$section" type - config_get plugin "$section" plugin - - [ -z "$ptype" ] && ptype="tcp" - - printf '\n[[proxies]]\n' - _toml_line name "$pname" string - _toml_line type "$ptype" string - - _emit_opt "$section" enabled enabled bool - - # localIP/localPort are useful for normal local-service proxies. - # If plugin is enabled, plugin usually handles the local service itself. - if [ -z "$plugin" ]; then - case "$ptype" in - tcp|udp|http|https|stcp|xtcp|sudp|tcpmux) - _emit_opt "$section" local_ip localIP string - _emit_opt "$section" local_port localPort port - ;; - esac - fi - - # remotePort is mainly used by tcp/udp-style listeners. - # Plugin proxies with type=tcp also need remotePort. - case "$ptype" in - tcp|udp) - _emit_opt "$section" remote_port remotePort port0 - ;; - esac - - # Common proxy transport options. - _emit_opt "$section" bandwidth_limit transport.bandwidthLimit string - _emit_opt "$section" bandwidth_limit_mode transport.bandwidthLimitMode string - _emit_opt "$section" use_encryption transport.useEncryption bool - _emit_opt "$section" use_compression transport.useCompression bool - _emit_opt "$section" proxy_protocol_version transport.proxyProtocolVersion string - - # HTTP / HTTPS domain options. - case "$ptype" in - http|https) - _emit_array_opt "$section" custom_domains customDomains - _emit_opt "$section" subdomain subdomain string - ;; - esac - - # HTTP-only options. Skip these when plugin is enabled to avoid stale normal HTTP proxy fields. - case "$ptype" in - http) - _emit_opt "$section" route_by_http_user routeByHTTPUser string - if [ -z "$plugin" ]; then - _emit_array_opt "$section" locations locations - _emit_opt "$section" http_user httpUser string - _emit_opt "$section" http_pwd httpPassword string - _emit_opt "$section" host_header_rewrite hostHeaderRewrite string - _emit_kv_opt "$section" request_headers requestHeaders.set string - _emit_kv_opt "$section" response_headers responseHeaders.set string - fi - ;; - esac - - # TCPMUX options. - case "$ptype" in - tcpmux) - _emit_array_opt "$section" custom_domains customDomains - _emit_opt "$section" http_user httpUser string - _emit_opt "$section" http_pwd httpPassword string - _emit_opt "$section" multiplexer multiplexer string - _emit_opt "$section" route_by_http_user routeByHTTPUser string - ;; - esac - - # Load balancer options. - _emit_opt "$section" group loadBalancer.group string - _emit_opt "$section" group_key loadBalancer.groupKey string - - # Health check options. - config_get hct "$section" health_check_type - if [ -n "$hct" ] && [ -z "$plugin" ]; then - case "$ptype" in - tcp|http|https|tcpmux) - _emit_opt "$section" health_check_type healthCheck.type string - _emit_opt "$section" health_check_timeout_s healthCheck.timeoutSeconds int - _emit_opt "$section" health_check_max_failed healthCheck.maxFailed int - _emit_opt "$section" health_check_interval_s healthCheck.intervalSeconds int - - if [ "$hct" = "http" ]; then - _emit_opt "$section" health_check_url healthCheck.path string - _emit_headers_opt "$section" health_check_headers healthCheck.httpHeaders - fi - ;; - esac - fi - - # STCP / XTCP / SUDP server options. - case "$ptype" in - stcp|xtcp|sudp) - _emit_opt "$section" sk secretKey string - _emit_array_opt "$section" allow_users allowUsers - ;; - esac - - # NAT traversal options are mainly useful for XTCP. - case "$ptype" in - xtcp) - _emit_opt "$section" nat_disable_assisted_addrs natTraversal.disableAssistedAddrs bool - ;; - esac - - # Metadata / annotations. - _emit_kv_opt "$section" metadatas metadatas string - _emit_kv_opt "$section" annotations annotations string - - # Plugin options are filtered by plugin type in _emit_plugin(). - _emit_plugin "$section" - - # Raw extra TOML lines. - _emit_raw_opt "$section" _ -} - -_emit_visitor() { - local section="$1" - local vname vtype - - config_get vname "$section" name "$section" - config_get vtype "$section" type - - [ -z "$vtype" ] && vtype="stcp" - - printf '\n[[visitors]]\n' - _toml_line name "$vname" string - _toml_line type "$vtype" string - - _emit_opt "$section" server_user serverUser string - _emit_opt "$section" server_name serverName string - _emit_opt "$section" sk secretKey string - _emit_opt "$section" bind_addr bindAddr string - _emit_opt "$section" bind_port bindPort visitor_bind_port - _emit_opt "$section" enabled enabled bool - _emit_opt "$section" use_encryption transport.useEncryption bool - _emit_opt "$section" use_compression transport.useCompression bool - - # XTCP visitor-specific options. - case "$vtype" in - xtcp) - _emit_opt "$section" visitor_protocol protocol string - _emit_opt "$section" keep_tunnel_open keepTunnelOpen bool - _emit_opt "$section" max_retries_an_hour maxRetriesAnHour int - _emit_opt "$section" min_retry_interval minRetryInterval int - _emit_opt "$section" fallback_to fallbackTo string - _emit_opt "$section" fallback_timeout_ms fallbackTimeoutMs int - _emit_opt "$section" nat_disable_assisted_addrs natTraversal.disableAssistedAddrs bool - ;; - esac - - # Visitor plugin, for example virtual_net. - _emit_plugin "$section" - - # Raw extra TOML lines. - _emit_raw_opt "$section" _ -} - -_emit_conf_section() { - local section="$1" - local role ptype - - [ "$section" = "common" ] && return 0 - - config_get role "$section" role - config_get ptype "$section" type - - if [ "$role" = "visitor" ]; then - case "$ptype" in - ''|stcp|xtcp|sudp) - _emit_visitor "$section" - return $? - ;; - *) - _err "visitor section $section has unsupported type: $ptype" - _TOML_ERR=1 - return 1 - ;; - esac - fi - - _emit_proxy "$section" -} - -_find_init_section() { - [ -z "$init_cfg" ] && init_cfg="$1" - return 0 -} - -_append_conf_file() { - local file="$1" - local dir="/etc/frp/$NAME.d/" - local real_file - - case "$file" in - "$dir"*.toml) - case "$file" in - *../*) - _err "additional config file outside $dir is not allowed: $file" - _TOML_ERR=1 - return 1 - ;; - esac - ;; - *) - _err "additional config file must be under $dir and end with .toml: $file" - _TOML_ERR=1 - return 1 - ;; - esac - - [ -r "$file" ] || { - _err "additional config file not readable: $file" - _TOML_ERR=1 - return 1 - } - - real_file="$(readlink -f "$file")" || { - _err "additional config file path cannot be resolved: $file" - _TOML_ERR=1 - return 1 - } - - case "$real_file" in - "$dir"*.toml) - ;; - *) - _err "additional config file resolves outside $dir or is not .toml: $file" - _TOML_ERR=1 - return 1 - ;; - esac - - printf '\n' >> "$CONF_FILE" - cat "$real_file" >> "$CONF_FILE" - printf '\n' >> "$CONF_FILE" -} - -_watch_conf_file() { - local file="$1" - - [ -r "$file" ] && procd_set_param file "$file" - return 0 -} - -_append_env() { - procd_append_param env "$1" -} - -service_triggers() { - procd_add_reload_trigger "$NAME" -} - -start_service() { - local init_cfg= - local stdout=1 - local stderr=1 - local respawn=1 - local run_user= - local run_group= - local old_umask - - mkdir -p /var/etc - _TOML_ERR=0 - _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 - - config_load "$NAME" - - config_foreach _find_init_section init - - old_umask="$(umask)" - umask 077 - : > "$CONF_FILE" || { - umask "$old_umask" - _err "failed to create $CONF_FILE" - return 1 - } - umask "$old_umask" - chmod 600 "$CONF_FILE" || { - _err "failed to chmod $CONF_FILE" - return 1 - } - - { - printf '# This file is automatically generated from /etc/config/%s.\n' "$NAME" - printf '# Do not edit this file directly.\n\n' - - _emit_common - } >> "$CONF_FILE" || return 1 - [ "$_TOML_ERR" = "0" ] || return 1 - - if [ -n "$init_cfg" ]; then - config_list_foreach "$init_cfg" conf_inc _append_conf_file - - config_get_bool stdout "$init_cfg" stdout 1 - config_get_bool stderr "$init_cfg" stderr 1 - config_get_bool respawn "$init_cfg" respawn 1 - config_get run_user "$init_cfg" user - config_get run_group "$init_cfg" group - fi - - { - config_foreach _emit_conf_section conf - } >> "$CONF_FILE" || return 1 - [ "$_TOML_ERR" = "0" ] || return 1 - - if [ -n "$run_user" ]; then - chown "$run_user${run_group:+:$run_group}" "$CONF_FILE" 2>/dev/null || { - _err "failed to chown $CONF_FILE to $run_user${run_group:+:$run_group}" - return 1 - } - fi - - chmod 600 "$CONF_FILE" || { - _err "failed to chmod $CONF_FILE" - return 1 - } - - procd_open_instance - if [ "$_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC" = "1" ]; then - procd_set_param command "$PROG" -c "$CONF_FILE" --allow-unsafe=TokenSourceExec - else - procd_set_param command "$PROG" -c "$CONF_FILE" - fi - procd_set_param file "$CONF_FILE" - procd_set_param file "/etc/config/$NAME" - - if [ -n "$init_cfg" ]; then - config_list_foreach "$init_cfg" conf_inc _watch_conf_file - fi - - procd_set_param stdout "$stdout" - procd_set_param stderr "$stderr" - - [ -n "$run_user" ] && procd_set_param user "$run_user" - [ -n "$run_group" ] && procd_set_param group "$run_group" - [ "$respawn" -eq 1 ] && procd_set_param respawn - - if [ -n "$init_cfg" ]; then - config_list_foreach "$init_cfg" env _append_env - fi - - procd_close_instance -} diff --git a/frp/files/frpc.uci-defaults b/frp/files/frpc.uci-defaults deleted file mode 100644 index 4887acc9..00000000 --- a/frp/files/frpc.uci-defaults +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/sh - -. /lib/functions.sh - -changed=0 -init_section= -package=frpc - -find_init_section() { - [ -z "$init_section" ] && init_section="$1" - return 0 -} - -set_if_empty() { - local section="$1" - local option="$2" - local value="$3" - local cur - - config_get cur "$section" "$option" - - if [ -z "$cur" ]; then - uci_set "$package" "$section" "$option" "$value" - changed=1 - fi -} - -copy_if_empty() { - local section="$1" - local old_option="$2" - local new_option="$3" - local value - - config_get value "$section" "$old_option" - [ -n "$value" ] || return 0 - - set_if_empty "$section" "$new_option" "$value" -} - -upgrade_common() { - local section="$1" - local dashboard_tls_mode - - [ "$section" = "common" ] || return 0 - - set_if_empty "$section" server_addr 127.0.0.1 - set_if_empty "$section" server_port 7000 - set_if_empty "$section" authentication_method token - set_if_empty "$section" login_fail_exit true - set_if_empty "$section" protocol tcp - set_if_empty "$section" wire_protocol v1 - set_if_empty "$section" tcp_mux true - set_if_empty "$section" tls_enable true - set_if_empty "$section" disable_custom_tls_first_byte true - - config_get dashboard_tls_mode "$section" dashboard_tls_mode - if [ -n "$dashboard_tls_mode" ]; then - set_if_empty "$section" admin_tls_enable "$dashboard_tls_mode" - else - set_if_empty "$section" admin_tls_enable false - fi - - copy_if_empty "$section" dashboard_addr admin_addr - copy_if_empty "$section" dashboard_port admin_port - copy_if_empty "$section" dashboard_user admin_user - copy_if_empty "$section" dashboard_pwd admin_pwd - copy_if_empty "$section" dashboard_tls_cert_file admin_tls_cert_file - copy_if_empty "$section" dashboard_tls_key_file admin_tls_key_file - - set_if_empty "$section" pprof_enable false - set_if_empty "$section" log_file console - set_if_empty "$section" log_level info - set_if_empty "$section" log_max_days 3 -} - -upgrade_init() { - if [ -z "$init_section" ]; then - init_section="$(uci add "$package" init)" || return 0 - changed=1 - fi - - set_if_empty "$init_section" stdout 1 - set_if_empty "$init_section" stderr 1 - set_if_empty "$init_section" respawn 1 -} - -upgrade_proxy_name() { - local section="$1" - local name - - [ "$section" != "common" ] || return 0 - - config_get name "$section" name - - if [ -z "$name" ]; then - uci_set "$package" "$section" name "$section" - changed=1 - fi -} - -config_load "$package" -config_foreach find_init_section init -upgrade_init -config_foreach upgrade_common conf -config_foreach upgrade_proxy_name conf - -[ "$changed" -eq 1 ] && uci_commit "$package" - -exit 0 diff --git a/frp/files/frps.config b/frp/files/frps.config deleted file mode 100644 index 1cd8bfa3..00000000 --- a/frp/files/frps.config +++ /dev/null @@ -1,77 +0,0 @@ -config init - option stdout '1' - option stderr '1' -# Uncomment to run frps as an existing user/group. Keep disabled by -# default to avoid permission issues with certificate, log and included -# config files. -# option user 'nobody' -# option group 'nogroup' - option respawn '1' -# For full configuration options, see: -# https://github.com/fatedier/frp/blob/master/conf/frps_full_example.toml -# -# Additional config files should be readable root-level TOML fragments. -# The service will refuse to start if a listed fragment is missing or outside /etc/frp/frps.d/. -# Use raw settings below for table-specific extra options such as HTTP plugins. -# list conf_inc '/etc/frp/frps.d/frps_extra.toml' - -config conf 'common' - option bind_addr '0.0.0.0' - option bind_port '7000' - - option authentication_method 'token' -# option token 'your_token' -# Alternatively, load a token from a file or command. Exec token sources -# require frps to run with --allow-unsafe=TokenSourceExec; the init script -# adds that flag automatically when token_source_type is exec. -# option token_source_type 'file' -# option token_source_file_path '/etc/frp/server_token' -# option token_source_type 'exec' -# option token_source_exec_command '/usr/bin/get-frps-token' -# list token_source_exec_args '--format' -# list token_source_exec_args 'raw' -# list token_source_exec_env 'TOKEN_SERVICE=production' - - option max_pool_count '5' - option tcp_mux 'true' - option tls_force 'false' - option detailed_errors_to_client 'true' - - # Web server is disabled when admin_port is empty or 0. -# option admin_addr '127.0.0.1' -# option admin_port '7500' -# option admin_user 'admin' -# option admin_pwd 'admin' - option admin_tls_enable 'false' - option pprof_enable 'false' - option enable_prometheus 'false' -# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt' -# option admin_tls_key_file '/etc/ssl/acme/example.com.key' - -# Allow ports can be single ports or ranges. -# list allow_ports '2000-3000' -# list allow_ports '3001' - - option log_file 'console' - option log_level 'info' - option log_max_days '3' - -# List options with name "_" will be directly appended as raw TOML lines. -# Use this only for options not covered by UCI options above. -# Do not duplicate keys generated by UCI options above. -# list _ 'uncovered.option = "value"' - -# HTTP plugin hooks for frps. This emits [[httpPlugins]] TOML tables. -#config http_plugin 'user_manager' -# option name 'user-manager' -# option addr '127.0.0.1:9000' -# option path '/handler' -# list ops 'Login' -# option tls_verify 'false' - -#config http_plugin 'port_manager' -# option name 'port-manager' -# option addr '127.0.0.1:9001' -# option path '/handler' -# list ops 'NewProxy' -# option tls_verify 'false' diff --git a/frp/files/frps.init b/frp/files/frps.init deleted file mode 100644 index 86c4fd0d..00000000 --- a/frp/files/frps.init +++ /dev/null @@ -1,780 +0,0 @@ -#!/bin/sh /etc/rc.common - -START=99 -USE_PROCD=1 - -NAME=frps -PROG=/usr/bin/$NAME -CONF_FILE=/var/etc/$NAME.toml - -_err() { - echo "$*" >&2 - logger -p daemon.err -t "$NAME" "$*" -} - -_trim() { - printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' -} - -_toml_escape() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -_toml_quote() { - printf '"%s"' "$(_toml_escape "$1")" -} - -_toml_key_quote() { - _toml_quote "$1" -} - -_toml_bool() { - local v - v="$(printf '%s' "$1" | tr 'A-Z' 'a-z')" - - case "$v" in - 1|true|yes|on|enabled) - printf 'true' - ;; - 0|false|no|off|disabled) - printf 'false' - ;; - *) - printf 'false' - ;; - esac -} - -_TOML_ERR=0 -_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 - -_is_uinteger() { - case "$1" in - ''|*[!0-9]*) - return 1 - ;; - esac - - return 0 -} - -_is_integer() { - local value="$1" - - case "$value" in - +*|-*) - value="${value#?}" - ;; - esac - - _is_uinteger "$value" -} - -_is_port_value() { - _is_uinteger "$1" || return 1 - [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null -} - -_is_port_or_zero_value() { - _is_uinteger "$1" || return 1 - [ "$1" -ge 0 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null -} - -_toml_line() { - local key="$1" - local value="$2" - local type="$3" - - [ -z "$value" ] && return 0 - - case "$type" in - bool) - printf '%s = %s\n' "$key" "$(_toml_bool "$value")" - ;; - int|integer|number) - if ! _is_integer "$value"; then - _err "invalid integer for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - port) - if ! _is_port_value "$value"; then - _err "invalid port for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - port0) - if ! _is_port_or_zero_value "$value"; then - _err "invalid port for $key: $value" - _TOML_ERR=1 - return 1 - fi - - printf '%s = %s\n' "$key" "$value" - ;; - *) - printf '%s = %s\n' "$key" "$(_toml_quote "$value")" - ;; - esac -} - -_emit_opt() { - local section="$1" - local option="$2" - local toml_key="$3" - local type="$4" - local value - - config_get value "$section" "$option" - _toml_line "$toml_key" "$value" "$type" -} - -_TOML_ARRAY= -_TOML_HAS_LIST=0 - -_toml_array_add() { - local item="$1" - - item="$(_trim "$item")" - [ -z "$item" ] && return 0 - - if [ -n "$_TOML_ARRAY" ]; then - _TOML_ARRAY="${_TOML_ARRAY}, " - fi - - _TOML_ARRAY="${_TOML_ARRAY}$(_toml_quote "$item")" -} - -_collect_array_item() { - _TOML_HAS_LIST=1 - _toml_array_add "$1" -} - -_collect_array_option() { - local section="$1" - local option="$2" - local scalar item - - _TOML_ARRAY= - _TOML_HAS_LIST=0 - - config_list_foreach "$section" "$option" _collect_array_item - - if [ "$_TOML_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - - while [ -n "$scalar" ]; do - case "$scalar" in - *,*) - item="${scalar%%,*}" - scalar="${scalar#*,}" - ;; - *) - item="$scalar" - scalar= - ;; - esac - - _toml_array_add "$item" - done - fi - - return 0 -} - -_emit_array_opt() { - local section="$1" - local option="$2" - local toml_key="$3" - - _collect_array_option "$section" "$option" - - [ -n "$_TOML_ARRAY" ] || return 0 - printf '%s = [%s]\n' "$toml_key" "$_TOML_ARRAY" -} - -_NAME_VALUE_ARRAY= -_NAME_VALUE_HAS_LIST=0 - -_name_value_array_add() { - local line="$1" - local key value item - - case "$line" in - *=*) - key="${line%%=*}" - value="${line#*=}" - ;; - *) - return 0 - ;; - esac - - key="$(_trim "$key")" - value="$(_trim "$value")" - - [ -n "$key" ] || return 0 - - item="{ name = $(_toml_quote "$key"), value = $(_toml_quote "$value") }" - - if [ -n "$_NAME_VALUE_ARRAY" ]; then - _NAME_VALUE_ARRAY="${_NAME_VALUE_ARRAY}, " - fi - - _NAME_VALUE_ARRAY="${_NAME_VALUE_ARRAY}${item}" -} - -_collect_name_value_item() { - _NAME_VALUE_HAS_LIST=1 - _name_value_array_add "$1" -} - -_emit_name_value_array_opt() { - local section="$1" - local option="$2" - local toml_key="$3" - local scalar - - _NAME_VALUE_ARRAY= - _NAME_VALUE_HAS_LIST=0 - - config_list_foreach "$section" "$option" _collect_name_value_item - - if [ "$_NAME_VALUE_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - [ -n "$scalar" ] && _name_value_array_add "$scalar" - fi - - [ -n "$_NAME_VALUE_ARRAY" ] || return 0 - printf '%s = [%s]\n' "$toml_key" "$_NAME_VALUE_ARRAY" -} - -_RAW_HAS_LIST=0 - -_emit_raw_item() { - _RAW_HAS_LIST=1 - [ -n "$1" ] || return 0 - printf '%s\n' "$1" -} - -_emit_raw_opt() { - local section="$1" - local option="$2" - local scalar - - _RAW_HAS_LIST=0 - config_list_foreach "$section" "$option" _emit_raw_item - - if [ "$_RAW_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - [ -n "$scalar" ] || return 0 - printf '%s\n' "$scalar" - return $? - fi - - return 0 -} - -_is_port() { - _is_port_value "$1" -} - -_ALLOW_PORTS_ARRAY= -_ALLOW_PORTS_HAS_LIST=0 -_ALLOW_PORTS_ERR=0 - -_allow_ports_array_add() { - local item="$1" - local start end table - - item="$(_trim "$item")" - [ -z "$item" ] && return 0 - - case "$item" in - \{*\}) - table="$item" - ;; - - *-*) - start="${item%%-*}" - end="${item#*-}" - - start="$(_trim "$start")" - end="$(_trim "$end")" - - if ! _is_port "$start" || ! _is_port "$end"; then - _err "invalid allow_ports range: $item" - _ALLOW_PORTS_ERR=1 - return 0 - fi - - if [ "$start" -gt "$end" ]; then - _err "invalid allow_ports range, start is greater than end: $item" - _ALLOW_PORTS_ERR=1 - return 0 - fi - - if [ "$start" = "$end" ]; then - table="{ single = $start }" - else - table="{ start = $start, end = $end }" - fi - ;; - - *) - if ! _is_port "$item"; then - _err "invalid allow_ports value: $item" - _ALLOW_PORTS_ERR=1 - return 0 - fi - - table="{ single = $item }" - ;; - esac - - if [ -n "$_ALLOW_PORTS_ARRAY" ]; then - _ALLOW_PORTS_ARRAY="${_ALLOW_PORTS_ARRAY}, " - fi - - _ALLOW_PORTS_ARRAY="${_ALLOW_PORTS_ARRAY}${table}" -} - -_collect_allow_port_item() { - _ALLOW_PORTS_HAS_LIST=1 - _allow_ports_array_add "$1" -} - -_emit_allow_ports() { - local section="$1" - local option="$2" - local scalar item - - _ALLOW_PORTS_ARRAY= - _ALLOW_PORTS_HAS_LIST=0 - _ALLOW_PORTS_ERR=0 - - config_list_foreach "$section" "$option" _collect_allow_port_item - - if [ "$_ALLOW_PORTS_HAS_LIST" = "0" ]; then - config_get scalar "$section" "$option" - - while [ -n "$scalar" ]; do - case "$scalar" in - *,*) - item="${scalar%%,*}" - scalar="${scalar#*,}" - ;; - *) - item="$scalar" - scalar= - ;; - esac - - _allow_ports_array_add "$item" - done - fi - - [ "$_ALLOW_PORTS_ERR" = "0" ] || return 1 - [ -n "$_ALLOW_PORTS_ARRAY" ] || return 0 - - printf 'allowPorts = [%s]\n' "$_ALLOW_PORTS_ARRAY" -} - -_emit_auth_scopes() { - local section="$1" - local hb nwc v - - _collect_array_option "$section" auth_additional_scopes - - if [ -z "$_TOML_ARRAY" ]; then - config_get hb "$section" authenticate_heartbeats - config_get nwc "$section" authenticate_new_work_conns - - v="$(_toml_bool "$hb")" - [ "$v" = "true" ] && _toml_array_add "HeartBeats" - - v="$(_toml_bool "$nwc")" - [ "$v" = "true" ] && _toml_array_add "NewWorkConns" - fi - - [ -n "$_TOML_ARRAY" ] || return 0 - printf 'auth.additionalScopes = [%s]\n' "$_TOML_ARRAY" -} - -_emit_admin_web_tls() { - local section="$1" - local enabled cert key - - config_get enabled "$section" admin_tls_enable - - [ "$(_toml_bool "$enabled")" = "true" ] || return 0 - - config_get cert "$section" admin_tls_cert_file - config_get key "$section" admin_tls_key_file - - if [ -z "$cert" ] || [ -z "$key" ]; then - _err "admin_tls_cert_file and admin_tls_key_file are required when admin_tls_enable is enabled" - return 1 - fi - - if [ ! -r "$cert" ]; then - _err "admin TLS certificate file is not readable: $cert" - return 1 - fi - - if [ ! -r "$key" ]; then - _err "admin TLS private key file is not readable: $key" - return 1 - fi - - _toml_line webServer.tls.certFile "$cert" string - _toml_line webServer.tls.keyFile "$key" string - - return 0 -} - -_emit_admin_web() { - local section="$1" - local port addr - - config_get port "$section" admin_port - - # Empty or 0 means web server is disabled. - [ -n "$port" ] && [ "$port" != "0" ] || return 0 - - config_get addr "$section" admin_addr - _toml_line webServer.addr "${addr:-127.0.0.1}" string - _toml_line webServer.port "$port" port - _emit_opt "$section" admin_user webServer.user string - _emit_opt "$section" admin_pwd webServer.password string - _emit_admin_web_tls "$section" || return 1 - _emit_opt "$section" assets_dir webServer.assetsDir string - _emit_opt "$section" pprof_enable webServer.pprofEnable bool - _emit_opt "$section" enable_prometheus enablePrometheus bool - - return 0 -} - -_emit_common() { - local section="common" - local method token token_source_type token_source_file_path - - # Root options - _emit_opt "$section" bind_addr bindAddr string - _emit_opt "$section" bind_port bindPort port - _emit_opt "$section" kcp_bind_port kcpBindPort port0 - _emit_opt "$section" quic_bind_port quicBindPort port0 - _emit_opt "$section" proxy_bind_addr proxyBindAddr string - _emit_opt "$section" vhost_http_port vhostHTTPPort port0 - _emit_opt "$section" vhost_https_port vhostHTTPSPort port0 - _emit_opt "$section" vhost_http_timeout vhostHTTPTimeout int - _emit_opt "$section" tcpmux_httpconnect_port tcpmuxHTTPConnectPort port0 - _emit_opt "$section" tcpmux_passthrough tcpmuxPassthrough bool - _emit_opt "$section" subdomain_host subDomainHost string - _emit_opt "$section" custom_404_page custom404Page string - _emit_opt "$section" udp_packet_size udpPacketSize int - _emit_opt "$section" detailed_errors_to_client detailedErrorsToClient bool - _emit_opt "$section" user_conn_timeout userConnTimeout int - _emit_opt "$section" nathole_analysis_data_reserve_hours natholeAnalysisDataReserveHours int - - # Auth - config_get method "$section" authentication_method - config_get token "$section" token - config_get token_source_type "$section" token_source_type - config_get token_source_file_path "$section" token_source_file_path - - [ -z "$method" ] && { [ -n "$token" ] || [ -n "$token_source_type" ]; } && method="token" - - _toml_line auth.method "$method" string - - if [ "$method" = "token" ] || [ -z "$method" ]; then - if [ -n "$token_source_type" ]; then - if [ -n "$token" ]; then - _err "token and token_source_type are mutually exclusive" - return 1 - fi - - case "$token_source_type" in - file) - if [ -z "$token_source_file_path" ]; then - _err "token_source_file_path is required when token_source_type=file" - return 1 - fi - - _toml_line auth.tokenSource.type "$token_source_type" string - _toml_line auth.tokenSource.file.path "$token_source_file_path" string - ;; - - exec) - local token_source_exec_command - - config_get token_source_exec_command "$section" token_source_exec_command - if [ -z "$token_source_exec_command" ]; then - _err "token_source_exec_command is required when token_source_type=exec" - return 1 - fi - - _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=1 - _toml_line auth.tokenSource.type "$token_source_type" string - _toml_line auth.tokenSource.exec.command "$token_source_exec_command" string - _emit_array_opt "$section" token_source_exec_args auth.tokenSource.exec.args - _emit_name_value_array_opt "$section" token_source_exec_env auth.tokenSource.exec.env - ;; - - *) - _err "unsupported token_source_type: $token_source_type" - return 1 - ;; - esac - else - _toml_line auth.token "$token" string - fi - fi - - _emit_auth_scopes "$section" - - if [ "$method" = "oidc" ]; then - _emit_opt "$section" oidc_issuer auth.oidc.issuer string - _emit_opt "$section" oidc_audience auth.oidc.audience string - _emit_opt "$section" oidc_skip_expiry_check auth.oidc.skipExpiryCheck bool - _emit_opt "$section" oidc_skip_issuer_check auth.oidc.skipIssuerCheck bool - fi - - # Transport - _emit_opt "$section" max_pool_count transport.maxPoolCount int - _emit_opt "$section" tcp_mux transport.tcpMux bool - _emit_opt "$section" tcp_mux_keepalive_interval transport.tcpMuxKeepaliveInterval int - _emit_opt "$section" tcp_keepalive transport.tcpKeepalive int - _emit_opt "$section" heartbeat_timeout transport.heartbeatTimeout int - - # QUIC - _emit_opt "$section" quic_keepalive_period transport.quic.keepalivePeriod int - _emit_opt "$section" quic_max_idle_timeout transport.quic.maxIdleTimeout int - _emit_opt "$section" quic_max_incoming_streams transport.quic.maxIncomingStreams int - - # TLS - _emit_opt "$section" tls_force transport.tls.force bool - _emit_opt "$section" tls_cert_file transport.tls.certFile string - _emit_opt "$section" tls_key_file transport.tls.keyFile string - _emit_opt "$section" tls_trusted_ca_file transport.tls.trustedCaFile string - - # Web dashboard server - _emit_admin_web "$section" || return 1 - - # Access control - _emit_allow_ports "$section" allow_ports || return 1 - _emit_opt "$section" max_ports_per_client maxPortsPerClient int - - # SSH tunnel gateway - _emit_opt "$section" ssh_tunnel_bind_port sshTunnelGateway.bindPort port0 - _emit_opt "$section" ssh_tunnel_private_key_file sshTunnelGateway.privateKeyFile string - _emit_opt "$section" ssh_tunnel_auto_gen_private_key_path sshTunnelGateway.autoGenPrivateKeyPath string - _emit_opt "$section" ssh_tunnel_authorized_keys_file sshTunnelGateway.authorizedKeysFile string - - # Log - _emit_opt "$section" log_file log.to string - _emit_opt "$section" log_level log.level string - _emit_opt "$section" log_max_days log.maxDays int - _emit_opt "$section" disable_log_color log.disablePrintColor bool - - # Raw extra TOML lines kept for manual UCI usage; LuCI intentionally hides this. - _emit_raw_opt "$section" _ -} - -_emit_http_plugin() { - local section="$1" - local name addr path - - config_get name "$section" name "$section" - config_get addr "$section" addr - config_get path "$section" path - - if [ -z "$addr" ] || [ -z "$path" ]; then - _err "http plugin $name requires addr and path" - return 1 - fi - - _collect_array_option "$section" ops - - if [ -z "$_TOML_ARRAY" ]; then - _err "http plugin $name requires at least one operation" - return 1 - fi - - printf '\n[[httpPlugins]]\n' - _toml_line name "$name" string - _toml_line addr "$addr" string - _toml_line path "$path" string - printf 'ops = [%s]\n' "$_TOML_ARRAY" - _emit_opt "$section" tls_verify tlsVerify bool - - # Raw extra TOML lines for plugin options not covered by UCI options above. - _emit_raw_opt "$section" _ -} - -_find_init_section() { - [ -z "$init_cfg" ] && init_cfg="$1" - return 0 -} - -_append_conf_file() { - local file="$1" - local dir="/etc/frp/$NAME.d/" - local real_file - - case "$file" in - "$dir"*.toml) - case "$file" in - *../*) - _err "additional config file outside $dir is not allowed: $file" - _TOML_ERR=1 - return 1 - ;; - esac - ;; - *) - _err "additional config file must be under $dir and end with .toml: $file" - _TOML_ERR=1 - return 1 - ;; - esac - - [ -r "$file" ] || { - _err "additional config file not readable: $file" - _TOML_ERR=1 - return 1 - } - - real_file="$(readlink -f "$file")" || { - _err "additional config file path cannot be resolved: $file" - _TOML_ERR=1 - return 1 - } - - case "$real_file" in - "$dir"*.toml) - ;; - *) - _err "additional config file resolves outside $dir or is not .toml: $file" - _TOML_ERR=1 - return 1 - ;; - esac - - printf '\n' >> "$CONF_FILE" - cat "$real_file" >> "$CONF_FILE" - printf '\n' >> "$CONF_FILE" -} - -_watch_conf_file() { - local file="$1" - - [ -r "$file" ] && procd_set_param file "$file" - return 0 -} - -_append_env() { - procd_append_param env "$1" -} - -service_triggers() { - procd_add_reload_trigger "$NAME" -} - -start_service() { - local init_cfg= - local stdout=1 - local stderr=1 - local respawn=1 - local run_user= - local run_group= - local old_umask - - mkdir -p /var/etc - _TOML_ERR=0 - _ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0 - - config_load "$NAME" - - config_foreach _find_init_section init - - old_umask="$(umask)" - umask 077 - : > "$CONF_FILE" || { - umask "$old_umask" - _err "failed to create $CONF_FILE" - return 1 - } - umask "$old_umask" - chmod 600 "$CONF_FILE" || { - _err "failed to chmod $CONF_FILE" - return 1 - } - - { - printf '# This file is automatically generated from /etc/config/%s.\n' "$NAME" - printf '# Do not edit this file directly.\n\n' - - _emit_common - } >> "$CONF_FILE" || return 1 - [ "$_TOML_ERR" = "0" ] || return 1 - - if [ -n "$init_cfg" ]; then - config_list_foreach "$init_cfg" conf_inc _append_conf_file - - config_get_bool stdout "$init_cfg" stdout 1 - config_get_bool stderr "$init_cfg" stderr 1 - config_get_bool respawn "$init_cfg" respawn 1 - config_get run_user "$init_cfg" user - config_get run_group "$init_cfg" group - fi - - { - config_foreach _emit_http_plugin http_plugin - } >> "$CONF_FILE" || return 1 - [ "$_TOML_ERR" = "0" ] || return 1 - - if [ -n "$run_user" ]; then - chown "$run_user${run_group:+:$run_group}" "$CONF_FILE" 2>/dev/null || { - _err "failed to chown $CONF_FILE to $run_user${run_group:+:$run_group}" - return 1 - } - fi - - chmod 600 "$CONF_FILE" || { - _err "failed to chmod $CONF_FILE" - return 1 - } - - procd_open_instance - if [ "$_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC" = "1" ]; then - procd_set_param command "$PROG" -c "$CONF_FILE" --allow-unsafe=TokenSourceExec - else - procd_set_param command "$PROG" -c "$CONF_FILE" - fi - procd_set_param file "$CONF_FILE" - procd_set_param file "/etc/config/$NAME" - - if [ -n "$init_cfg" ]; then - config_list_foreach "$init_cfg" conf_inc _watch_conf_file - fi - - procd_set_param stdout "$stdout" - procd_set_param stderr "$stderr" - - [ -n "$run_user" ] && procd_set_param user "$run_user" - [ -n "$run_group" ] && procd_set_param group "$run_group" - [ "$respawn" -eq 1 ] && procd_set_param respawn - - if [ -n "$init_cfg" ]; then - config_list_foreach "$init_cfg" env _append_env - fi - - procd_close_instance -} diff --git a/frp/files/frps.uci-defaults b/frp/files/frps.uci-defaults deleted file mode 100644 index 2e3133a3..00000000 --- a/frp/files/frps.uci-defaults +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/sh - -. /lib/functions.sh - -changed=0 -init_section= -package=frps - -find_init_section() { - [ -z "$init_section" ] && init_section="$1" - return 0 -} - -set_if_empty() { - local section="$1" - local option="$2" - local value="$3" - local cur - - config_get cur "$section" "$option" - - if [ -z "$cur" ]; then - uci_set "$package" "$section" "$option" "$value" - changed=1 - fi -} - -copy_if_empty() { - local section="$1" - local old_option="$2" - local new_option="$3" - local value - - config_get value "$section" "$old_option" - [ -n "$value" ] || return 0 - - set_if_empty "$section" "$new_option" "$value" -} - -upgrade_common() { - local section="$1" - local dashboard_tls_mode - local tls_only - local nat_hole_hours - - [ "$section" = "common" ] || return 0 - - set_if_empty "$section" bind_addr 0.0.0.0 - set_if_empty "$section" bind_port 7000 - set_if_empty "$section" authentication_method token - set_if_empty "$section" max_pool_count 5 - set_if_empty "$section" tcp_mux true - set_if_empty "$section" detailed_errors_to_client true - set_if_empty "$section" pprof_enable false - set_if_empty "$section" enable_prometheus false - - config_get tls_only "$section" tls_only - if [ -n "$tls_only" ]; then - set_if_empty "$section" tls_force "$tls_only" - else - set_if_empty "$section" tls_force false - fi - - config_get dashboard_tls_mode "$section" dashboard_tls_mode - if [ -n "$dashboard_tls_mode" ]; then - set_if_empty "$section" admin_tls_enable "$dashboard_tls_mode" - else - set_if_empty "$section" admin_tls_enable false - fi - - copy_if_empty "$section" dashboard_addr admin_addr - copy_if_empty "$section" dashboard_port admin_port - copy_if_empty "$section" dashboard_user admin_user - copy_if_empty "$section" dashboard_pwd admin_pwd - copy_if_empty "$section" dashboard_tls_cert_file admin_tls_cert_file - copy_if_empty "$section" dashboard_tls_key_file admin_tls_key_file - - config_get nat_hole_hours "$section" nat_hole_analysis_data_reserve_hours - [ -n "$nat_hole_hours" ] && \ - set_if_empty "$section" nathole_analysis_data_reserve_hours "$nat_hole_hours" - - set_if_empty "$section" log_file console - set_if_empty "$section" log_level info - set_if_empty "$section" log_max_days 3 -} - -upgrade_init() { - if [ -z "$init_section" ]; then - init_section="$(uci add "$package" init)" || return 0 - changed=1 - fi - - set_if_empty "$init_section" stdout 1 - set_if_empty "$init_section" stderr 1 - set_if_empty "$init_section" respawn 1 -} - -config_load "$package" -config_foreach find_init_section init -upgrade_init -config_foreach upgrade_common conf - -[ "$changed" -eq 1 ] && uci_commit "$package" - -exit 0 diff --git a/frp/test.sh b/frp/test.sh deleted file mode 100755 index fd53475e..00000000 --- a/frp/test.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -# frpc/frps both accept "verify -c " to validate a config without -# connecting, which exercises the TOML config loader end-to-end. -case "$1" in -frpc) - conf="/tmp/frpc.$$.toml" - printf 'serverAddr = "127.0.0.1"\nserverPort = 7000\n' > "$conf" - frpc verify -c "$conf" || { echo "FAIL: frpc rejected a valid config"; rm -f "$conf"; exit 1; } - rm -f "$conf" - ;; -frps) - conf="/tmp/frps.$$.toml" - printf 'bindPort = 7000\n' > "$conf" - frps verify -c "$conf" || { echo "FAIL: frps rejected a valid config"; rm -f "$conf"; exit 1; } - rm -f "$conf" - ;; -*) - exit 0 - ;; -esac diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 6ac0db86..821b6a2a 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=27 +PKG_RELEASE:=28 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js index acc1cd00..991d4492 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js @@ -228,11 +228,17 @@ function measureShell() { * value is what everything downstream is measured against */ _shellOuter = document.documentElement.clientWidth; /* The GUTTER, though, is resolved style, and this runs on every mutation batch — once a second - * on any polled page. The two things that move it are the width (a media query re-paddings the - * column below 767px) and the density (the token is `calc(28px * var(--fs-density-space))`), so - * both are the key and an unchanged page resolves nothing. Same trade as the token memo above, - * for the same reason. */ - const key = (document.documentElement.getAttribute('data-density') || '') + '|' + _shellOuter; + * on any polled page. Three things move it: the width (a media query re-paddings the column + * below 767px), the density (the token is `calc(28px * var(--fs-density-space))`) and the PAGE. + * The third is the one the paragraph above already names and this key used to miss: `.fs-content` + * carries no chrome mark, so a foreign sheet may re-pad it — and `sheets.scopeToCurrentPage()` + * enables and disables those sheets on every client navigation, with no width and no density + * change to notice it by. Navigating off a page whose sheet re-padded the column would otherwise + * leave this pinned at that app's gutter for as long as the width held. `body[data-page]` is the + * one attribute a navigation always restamps, so it is the third term; an unchanged page still + * resolves nothing. Same trade as the token memo above, for the same reason. */ + const key = (document.documentElement.getAttribute('data-density') || '') + '|' + _shellOuter + + '|' + (document.body ? document.body.getAttribute('data-page') || '' : ''); if (_padAt === key && _shellPad != null) return; const host = document.querySelector('.fs-content'); const cs = host ? getComputedStyle(host) : null; diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index 02050664..fa697a99 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -329,12 +329,23 @@ const ENGINE_ANCHORS = (() => { * matter — the engine covers the other half. On one that does not, this is the half nobody covers, * so the reference is kept from the last still moment instead. */ let _rest = null; +/* THE OFFSET IS REMEMBERED EVEN WHEN THE ELEMENT IS NOT, and that is the difference between putting + * the reader back on 25.12 and putting them back everywhere. See anchorFor(). `_restPage` goes with + * it because a page the reader NAVIGATED away from is not a page whose offset means anything: the + * router resets both scrollers on a client navigation and replays them on a Back, and neither is a + * clamp to undo. */ +let _restAt = null, _restPage = null; +function pageStamp() { + return (document.body && document.body.getAttribute('data-page')) || ''; +} function rememberRest() { if (ENGINE_ANCHORS || scrolling()) return; const ref = anchorRef(); /* the offset it was taken at travels with it: a reference is only about the page, and the page * moving under the reader is a different fact from the reader moving through it */ - _rest = ref ? { el: ref.el, top: ref.top, at: scrollTop() } : null; + _restAt = scrollTop(); + _restPage = pageStamp(); + _rest = ref ? { el: ref.el, top: ref.top, at: _restAt } : null; } /* -> the reference to correct against: the pre-mutation one where the engine leaves that to us, the @@ -343,13 +354,65 @@ function rememberRest() { * left the document, or that the reader has since scrolled a screen away from, is not one. */ function anchorFor() { if (ENGINE_ANCHORS) return anchorRef(); - if (!_rest || !_rest.el.isConnected) return anchorRef(); + const at = scrollTop(); + /* AN OFFSET THAT DROPPED WITH NOBODY SCROLLING, ON THE PAGE IT WAS TAKEN ON, IS A CLAMP. + * All three of those conditions are load-bearing. A clamp only ever moves the offset DOWN — it is the + * page running out of length, never gaining it — and a reader who moved is a reader `scrolling()` + * still answers for: their scroll starts the sampler, while the clamp's own scroll event arrives + * in the rendering step AFTER this microtask. The page stamp is the third: fs-router resets both + * scrollers on a client navigation and replays them on a Back, and neither of those is a clamp to + * undo — restoring there would drag the reader down a page they had just left. */ + const clamped = (_restAt != null && at < _restAt && !scrolling() && _restPage === pageStamp()); + /* THE REFERENCE DID NOT SURVIVE THE TICK, WHICH IS THE COMMON CASE RATHER THAN AN EDGE ONE. + * `dom.content()` replaces a section's children with NEW nodes, so the element that happened to + * sit at the top of the content area is gone by the time this runs. Measured on a 24.10 stand, + * where the theme cannot reach the poll at all — `view.status.index` there keeps its step + * function in a closure (no `poll_status` on the prototype), so fs-overview's height pin has + * nothing to hook and every tick empties its sections the hard way: the reference was + * disconnected on the tick that mattered, the fallback took a fresh one, measured a drift the + * ceiling then refused, and the reader stayed 1206px from where they had been. + * + * With no element there is no drift to measure — but there is still a number known exactly, and + * it is the one the engine took: the offset dropped by this much and nothing else happened. + * Giving it back IS the correction, and it cannot run away with the page — if the document really + * is shorter now, the browser clamps the write straight back and the reader keeps the offset they + * already had. The element path below stays preferred where it survives, because it compensates + * the height change the tick brought with it as well. */ + if (!_rest || !_rest.el.isConnected) + return clamped ? { by: _restAt - at } : anchorRef(); /* THE READER MOVED, NOT THE PAGE. A reference taken at one offset says nothing about a document * seen from another: correcting against it would drag the page back to where the reader had - * scrolled FROM. Anything but the offset it was captured at means take a fresh one — which, on - * the mutation path, is the same as not correcting this tick. */ - if (_rest.at !== scrollTop()) return anchorRef(); - return _rest; + * scrolled FROM. So an offset that is not the one the reference was captured at normally means + * take a fresh one — which, on the mutation path, is the same as not correcting this tick. + * + * EXCEPT WHEN THE ENGINE MOVED IT, and that exception is the whole reason a poll tick could + * still throw the Overview across the screen with the compensation above already in place. + * `dom.content()` — what every LuCI poll calls to refresh a section — empties the container + * before it refills it, and for that moment the document is SHORTER than the offset the reader + * is at. The engine clamps the offset to what is left, the container fills again and nothing + * puts the offset back; the reader is simply somewhere else. Measured in WebKit with the + * engine's own anchoring off, a 30-row section swapped for a 35-row one two screens above the + * reader: the offset clamped by 130px and the page moved 255px under them. + * + * The test above cannot tell that from a reader who scrolled, because both changed the offset — + * so it threw away the one reference that describes where the page WAS and took a fresh one + * after the clamp, which measures a drift of zero and corrects nothing. Two facts separate them: + * a clamp only ever moves the offset DOWN (it is the page running out of length, never gaining + * it), and a reader who moved is a reader `scrolling()` still answers for — a scroll of theirs + * fires the event that starts the sampler, while the clamp's own scroll event arrives in the + * rendering step AFTER this microtask. An offset that dropped with nobody scrolling is the + * engine's doing, and the remembered reference is exactly what puts the reader back. + * + * Measured on the same harness with the reader flicking while the swap lands: the theme writes + * no offset at all, before this change and after it. */ + if (at !== _rest.at && !clamped) return anchorRef(); + /* HOW MUCH OF THE DRIFT IS ALREADY ACCOUNTED FOR. applyAnchor() refuses a correction bigger than + * a viewport because a drift that size normally means the view replaced its whole subtree and + * the reference is describing a page that no longer exists. A clamp is the one drift that big + * with a receipt: the offset dropped by exactly this much with nobody scrolling, so the ceiling + * is raised by that measured amount and by nothing else. Without it the worst clamps — the ones + * that hurt, 690px in a 300px viewport on the harness — were the ones refused. */ + return { el: _rest.el, top: _rest.top, slack: Math.max(0, _rest.at - at) }; } function anchorRef() { @@ -421,19 +484,36 @@ function scheduleAnchor(ref) { }); } function applyAnchor(ref) { - if (!ref || !ref.el.isConnected) return; + if (!ref) return; /* through scroller(), not a second probe of its own: the two asked the same question in the * same two lines and could already answer differently within one frame */ const sc = scroller(); const at = sc ? sc.scrollTop : window.scrollY; + /* THE ELEMENT-FREE FORM: give back exactly what the engine clamped away, no geometry read at all + * (anchorFor() explains when this is the only form available). No ceiling here and none wanted: + * the number is not an estimate of where the reader was, it is what the offset lost, and the + * document's own length is what bounds the write. + * + * It runs BEFORE the "a page at the top is left alone" rule below, and has to: a collapse deep + * enough clamps the offset to zero, and that is the worst version of this fault rather than the + * one case to sit out. The rule below is about a drift measured from a reference, where an offset + * of zero means there is nothing to give back. */ + if (ref.by != null) { + if (ref.by < 1) return; + if (sc) sc.scrollTop = at + ref.by; + else window.scrollTo(0, at + ref.by); + return; + } if (at <= 0) return; + if (!ref.el.isConnected) return; const drift = ref.el.getBoundingClientRect().top - ref.top; if (Math.abs(drift) < 1) return; /* A CORRECTION IS A SCROLL THE READER DID NOT ASK FOR, so an absurd one is a bug rather than a * fix: a view that replaced its whole subtree can move a reference by thousands of pixels, and * jumping there is worse than leaving the page where it is. One viewport is the most a single - * tick can honestly account for. */ - if (Math.abs(drift) > (window.innerHeight || 0) + 200) return; + * tick can honestly account for — plus whatever the engine is on record for having clamped away + * (`slack`, see anchorFor()), which is measured rather than assumed. */ + if (Math.abs(drift) > (window.innerHeight || 0) + 200 + (ref.slack || 0)) return; if (sc) sc.scrollTop = at + drift; else window.scrollTo(0, at + drift); } diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js index 200e9d96..b20edcde 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js @@ -165,7 +165,8 @@ function readonlyForSegs(segs) { /* The view class a menu node instantiates, or null if the node isn't SPA-able. The Status→Overview * `template` node maps to view.status.index (its server template just instantiates that — the - * globals that template also defines are fs-overview.js's, see ensureOverviewHelpers there). Shared by navigate() and the hover prefetch. */ + * globals that template also defines are the chrome bootstrap's, see ensureOverviewHelpers in + * menu-footstrap-common.js). Shared by navigate() and the hover prefetch. */ function viewClassFor(node) { if (!node || !node.action || node.satisfied === false) return null; diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js index ba9ab342..94c7ecd2 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js @@ -191,10 +191,44 @@ function fillSection(inc, container, res) { container.parentNode.style.display = ''; container.parentNode.classList.add('fade-in'); if (!inc.hide) - dom.content(container, content); + swapContent(container, content); } } +/* WHAT `dom.content()` DOES TO A READER TWO SCREENS DOWN, and the reason a section is filled through + * here rather than through it directly. + * + * `dom.content()` removes every child and then appends the new ones. Between those two halves the + * section has NO height, and a document that just lost several hundred pixels is a document the + * engine clamps the scroll offset into: the offset drops to whatever length is left, the section + * fills again, and nothing puts the offset back. The reader is now somewhere else, once a second, + * for as long as the Overview is open. Reported from Safari on macOS and iOS with the offset moving + * 200-887px per tick, and reproduced in WebKit with the engine's own scroll anchoring off: a 30-row + * section swapped for a 35-row one two screens above the reader moved the page 255px under them — + * 0px with the height held across the swap. + * + * fs-fit's anchoring covers the same fault from the other side (it now recognises a clamped offset + * and puts the reader back), and it has to: every OTHER page's poll calls `dom.content()` too and no + * theme code is in that path. This is the half that can be prevented rather than corrected, and + * prevention is worth the line — a correction is a scroll the reader did not ask for, and on iOS it + * lands in the middle of whatever momentum the page still has. + * + * The cost is one `offsetHeight` per filled section per tick — a forced layout, which this theme + * spends carefully (fs-fit.js). It is paid because the alternative is not "no layout" but "a layout + * whose result is the page jumping": the swap dirties this container anyway. + * + * The pin is released in the same call, so nothing is left to a later frame that could take the + * minimum with it if the next tick throws — and a section that legitimately got shorter is shorter + * again by the time this returns. */ +function swapContent(container, content) { + const hold = container.offsetHeight; + const prev = container.style.minHeight; + if (hold > 0) + container.style.minHeight = hold + 'px'; + try { dom.content(container, content); } + finally { if (hold > 0) container.style.minHeight = prev; } +} + let _inflight = null; /* WHICH containers the in-flight run is filling. The guard below is module-level because the * duplicate load it kills is module-level, but the frames are per RENDER — so joining a run blindly @@ -296,58 +330,14 @@ function patchOverview() { }).catch((e) => console.error('footstrap: overview progressive paint not applied', e)); } -/* Status→Overview is a `template` node whose server template (admin_status/index.ut) defines 3 - * globals the stock status includes use (18_cpu/20_memory/25_storage/…) and then instantiates - * view.status.index. Arriving by the theme's SPA router never runs that inline