mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
This commit is contained in:
@@ -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 <ardeleanalex@gmail.com>
|
||||
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))
|
||||
@@ -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'
|
||||
-1003
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# frpc/frps both accept "verify -c <file>" 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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <script>, so define
|
||||
* them here, guarded, so a full load's copies (any theme) are not clobbered.
|
||||
*
|
||||
* This is the OTHER thing the include location used to give for free, and the one that survived the
|
||||
* move intact: the definitions had to exist before any include renders, which module eval inside
|
||||
* index.load() guaranteed. A chrome module evaluates once, at chrome init — i.e. before any SPA
|
||||
* navigation can possibly happen — so the guarantee is now stronger rather than weaker. On a full
|
||||
* load of the overview the template's own copies win the race and these are no-ops, as before.
|
||||
*
|
||||
* Bodies are verbatim from upstream except L.itemlist → window.L.itemlist (the two-L trap,
|
||||
* docs/spa-router.md). */
|
||||
function ensureOverviewHelpers() {
|
||||
/* eslint-disable no-var -- these three bodies are copied VERBATIM from LuCI's
|
||||
admin_status/index.ut so they can be diffed against upstream when it changes.
|
||||
Modernising the `var`s would silently break that property, which is the whole
|
||||
reason the copies are safe to carry. */
|
||||
if (typeof window.progressbar !== 'function')
|
||||
window.progressbar = function(query, value, max, byte) {
|
||||
var pg = document.querySelector(query),
|
||||
vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
if (pg) {
|
||||
pg.firstElementChild.style.width = pc + '%';
|
||||
pg.setAttribute('title', '%s / %s (%d%%)'.format(fv, fm, pc));
|
||||
}
|
||||
};
|
||||
if (typeof window.renderBox !== 'function')
|
||||
window.renderBox = function(title, active, childs) {
|
||||
childs = childs || [];
|
||||
childs.unshift(window.L.itemlist(E('span'), [].slice.call(arguments, 3)));
|
||||
return E('div', { class: 'ifacebox' }, [
|
||||
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
||||
E('strong', title)),
|
||||
E('div', { class: 'ifacebox-body left' }, childs)
|
||||
]);
|
||||
};
|
||||
if (typeof window.renderBadge !== 'function')
|
||||
window.renderBadge = function(icon, title) {
|
||||
return E('span', { class: 'ifacebadge' }, [
|
||||
E('img', { src: icon, title: title || '' }),
|
||||
window.L.itemlist(E('span'), [].slice.call(arguments, 2))
|
||||
]);
|
||||
};
|
||||
/* eslint-enable no-var */
|
||||
}
|
||||
/* Unconditional: the typeof guards make it a no-op wherever the real definitions already exist. */
|
||||
ensureOverviewHelpers();
|
||||
/* THE THREE TEMPLATE GLOBALS ARE NOT HERE, and where they went is the point. `progressbar`,
|
||||
* `renderBox` and `renderBadge` come from `admin_status/index.ut`'s inline script, which an SPA
|
||||
* arrival never runs, so the theme defines them — but a stock include calls them bare from its own
|
||||
* `render()`, so they must exist before the view class does. While this file was in the chrome's
|
||||
* directive prologue that was free: it evaluated at chrome init. As a page module it is required
|
||||
* during the navigation that needs it, racing the router's require of the view class, so the
|
||||
* definitions moved to `menu-footstrap-common.js`, which every page evaluates before the router
|
||||
* exists. This module keeps only what the Overview itself needs. */
|
||||
|
||||
return baseclass.extend({
|
||||
/* Called by menu-footstrap-common's init, once. Everything route-dependent hangs off the
|
||||
|
||||
@@ -1130,9 +1130,10 @@ function navigate(pathname, push, kbd) {
|
||||
* singleton whose __init__ already ran. `_seen` is that distinction, and it must be read BEFORE
|
||||
* the require resolves, since the require is what fills LuCI's cache. */
|
||||
/* Status→Overview needs the 3 template globals (progressbar/renderBox/renderBadge) an SPA
|
||||
* arrival never defines. fs-overview.js defines them at its own module eval — which, as a
|
||||
* chrome module, happens at chrome init, i.e. before any SPA navigation can occur. Not here:
|
||||
* the router has no business owning luci-mod-status's globals. */
|
||||
* arrival never defines. `menu-footstrap-common.js` defines them at its own module eval, which
|
||||
* every page performs before this router exists — the one page module that could have carried
|
||||
* them (fs-overview) now loads DURING the navigation and would race this require. Not here
|
||||
* either way: the router has no business owning luci-mod-status's globals. */
|
||||
const RT = window.L;
|
||||
const cached = _seen.has(className);
|
||||
/* WAIT for an in-flight prefetch of this class rather than racing it. Two requests for the same
|
||||
|
||||
@@ -51,6 +51,67 @@ function wirePageModules() {
|
||||
load();
|
||||
}
|
||||
|
||||
/* THE THREE TEMPLATE GLOBALS STATUS→OVERVIEW NEEDS, DEFINED WHERE ORDER IS GUARANTEED.
|
||||
*
|
||||
* `admin_status/index.ut` defines `progressbar`, `renderBox` and `renderBadge` in an inline script
|
||||
* and then instantiates `view.status.index`; the stock includes (18_cpu, 30_network, 60_wifi…) call
|
||||
* them bare from their own `render()`. An SPA arrival never runs that inline script, so this theme
|
||||
* is their only definition — and a definition that arrives late is a `ReferenceError` thrown from a
|
||||
* stock include on a page already committed to the document.
|
||||
*
|
||||
* THEY LIVE HERE RATHER THAN IN `fs-overview.js` FOR EXACTLY ONE REASON: ordering. While that module
|
||||
* was in the directive prologue it evaluated at chrome init, i.e. before any navigation could
|
||||
* happen, and the guarantee was free. It is a page module now (PAGE_MODULES above) — required
|
||||
* DURING the navigation that needs it, in a chain that races the router's own require of the view
|
||||
* class. fs-overview's chain is the shorter of the two and should win, but nothing orders them, and
|
||||
* losing costs the page. This file is required by the footer on every page and evaluates before the
|
||||
* router exists, so moving the ~40 dependency-free lines here restores the guarantee at a cost of
|
||||
* their own bytes; the 3.8 KB of Overview layout code stays on the Overview.
|
||||
*
|
||||
* Bodies are verbatim from upstream except L.itemlist → window.L.itemlist (the two-L trap,
|
||||
* docs/spa-router.md), and the typeof guards make every one a no-op on a full page load, where the
|
||||
* template's own copies win the race — any theme, as before. */
|
||||
function ensureOverviewHelpers() {
|
||||
/* eslint-disable no-var -- these three bodies are copied VERBATIM from LuCI's
|
||||
admin_status/index.ut so they can be diffed against upstream when it changes.
|
||||
Modernising the `var`s would silently break that property, which is the whole
|
||||
reason the copies are safe to carry. */
|
||||
if (typeof window.progressbar !== 'function')
|
||||
window.progressbar = function(query, value, max, byte) {
|
||||
var pg = document.querySelector(query),
|
||||
vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
if (pg) {
|
||||
pg.firstElementChild.style.width = pc + '%';
|
||||
pg.setAttribute('title', '%s / %s (%d%%)'.format(fv, fm, pc));
|
||||
}
|
||||
};
|
||||
if (typeof window.renderBox !== 'function')
|
||||
window.renderBox = function(title, active, childs) {
|
||||
childs = childs || [];
|
||||
childs.unshift(window.L.itemlist(E('span'), [].slice.call(arguments, 3)));
|
||||
return E('div', { class: 'ifacebox' }, [
|
||||
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
||||
E('strong', title)),
|
||||
E('div', { class: 'ifacebox-body left' }, childs)
|
||||
]);
|
||||
};
|
||||
if (typeof window.renderBadge !== 'function')
|
||||
window.renderBadge = function(icon, title) {
|
||||
return E('span', { class: 'ifacebadge' }, [
|
||||
E('img', { src: icon, title: title || '' }),
|
||||
window.L.itemlist(E('span'), [].slice.call(arguments, 2))
|
||||
]);
|
||||
};
|
||||
/* eslint-enable no-var */
|
||||
}
|
||||
|
||||
/* Unconditional: the typeof guards make it a no-op wherever the real definitions already exist. */
|
||||
ensureOverviewHelpers();
|
||||
|
||||
/* The chrome BOOTSTRAP: load the menu tree once, hand it to the parts that need it, and wire them
|
||||
* in the right order. It renders nothing itself — every piece lives in its own module:
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user