mirror of
https://github.com/kenzok8/small-package
synced 2025-01-05 11:36:47 +08:00
update 2024-07-17 12:20:19
This commit is contained in:
parent
e2e50208c9
commit
e512448331
131
dae/Makefile
Normal file
131
dae/Makefile
Normal file
@ -0,0 +1,131 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=0.6.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).zip
|
||||
PKG_SOURCE_URL:=https://github.com/daeuniverse/dae/releases/download/v$(PKG_VERSION)/dae-full-src.zip?
|
||||
PKG_HASH:=e4ab51493f7a65402b468c38647e79cfa669203b5295a616b7f8c1416d8f1bbe
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
PKG_LICENSE_FILE:=LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host bpf-headers
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
GO_PKG:=github.com/daeuniverse/dae
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/cmd.Version=$(PKG_VERSION) \
|
||||
$(GO_PKG)/common/consts.MaxMatchSetLen_=64
|
||||
GO_PKG_TAGS:=trace
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/bpf.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
UNZIP_CMD:=unzip -q -d $(PKG_BUILD_DIR) $(DL_DIR)/$(PKG_SOURCE)
|
||||
|
||||
define Package/dae/Default
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
URL:=https://github.com/daeuniverse/dae
|
||||
endef
|
||||
|
||||
define Package/dae
|
||||
$(call Package/dae/Default)
|
||||
TITLE:=A lightweight and high-performance transparent proxy solution
|
||||
# You need enable KERNEL_DEBUG_INFO_BTF and KERNEL_BPF_EVENTS
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) $(BPF_DEPENDS) \
|
||||
+ca-bundle +kmod-sched-core +kmod-sched-bpf +kmod-xdp-sockets-diag \
|
||||
+kmod-veth
|
||||
endef
|
||||
|
||||
define Package/dae-geoip
|
||||
$(call Package/dae/Default)
|
||||
TITLE:=geoip for dae
|
||||
DEPENDS:=+dae +v2ray-geoip
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/dae-geosite
|
||||
$(call Package/dae/Default)
|
||||
TITLE:=geosite for dae
|
||||
DEPENDS:=+dae +v2ray-geosite
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/dae/description
|
||||
dae, means goose, is a lightweight and high-performance transparent
|
||||
proxy solution.
|
||||
|
||||
In order to improve the traffic diversion performance as much as possible,
|
||||
dae runs the transparent proxy and traffic diversion suite in the linux
|
||||
kernel by eBPF. Therefore, we have the opportunity to make the direct
|
||||
traffic bypass the forwarding by proxy application and achieve true direct
|
||||
traffic through. Under such a magic trick, there is almost no performance
|
||||
loss and additional resource consumption for direct traffic.
|
||||
endef
|
||||
|
||||
define Package/dae/conffiles
|
||||
/etc/dae/config.dae
|
||||
/etc/config/dae
|
||||
endef
|
||||
|
||||
DAE_CFLAGS:= \
|
||||
-O2 -Wall -Werror \
|
||||
-DMAX_MATCH_SET_LEN=64 \
|
||||
-I$(BPF_HEADERS_DIR)/tools/lib \
|
||||
-I$(BPF_HEADERS_DIR)/arch/$(BPF_KARCH)/include/asm/mach-generic
|
||||
|
||||
define Build/Compile
|
||||
( \
|
||||
export \
|
||||
$(GO_GENERAL_BUILD_CONFIG_VARS) \
|
||||
$(GO_PKG_BUILD_CONFIG_VARS) \
|
||||
$(GO_PKG_BUILD_VARS) \
|
||||
BPF_CLANG="$(CLANG)" \
|
||||
BPF_STRIP_FLAG="-strip=$(LLVM_STRIP)" \
|
||||
BPF_CFLAGS="$(DAE_CFLAGS)" \
|
||||
BPF_TARGET="bpfel,bpfeb" \
|
||||
BPF_TRACE_TARGET="$(GO_ARCH)" ; \
|
||||
go generate $(PKG_BUILD_DIR)/control/control.go ; \
|
||||
go generate $(PKG_BUILD_DIR)/trace/trace.go ; \
|
||||
$(call GoPackage/Build/Compile) ; \
|
||||
)
|
||||
endef
|
||||
|
||||
define Package/dae/install
|
||||
$(call GoPackage/Package/Install/Bin,$(1))
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/dae/
|
||||
$(INSTALL_CONF) $(PKG_BUILD_DIR)/example.dae $(1)/etc/dae/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/dae.config $(1)/etc/config/dae
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/dae.init $(1)/etc/init.d/dae
|
||||
endef
|
||||
|
||||
define Package/dae-geoip/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/dae
|
||||
$(LN) ../v2ray/geoip.dat $(1)/usr/share/dae/geoip.dat
|
||||
endef
|
||||
|
||||
define Package/dae-geosite/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/dae
|
||||
$(LN) ../v2ray/geosite.dat $(1)/usr/share/dae/geosite.dat
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,dae))
|
||||
$(eval $(call BuildPackage,dae))
|
||||
$(eval $(call BuildPackage,dae-geoip))
|
||||
$(eval $(call BuildPackage,dae-geosite))
|
7
dae/files/dae.config
Normal file
7
dae/files/dae.config
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
config dae 'config'
|
||||
option enabled '0'
|
||||
option config_file '/etc/dae/config.dae'
|
||||
option log_maxbackups '1'
|
||||
option log_maxsize '1'
|
||||
|
56
dae/files/dae.init
Normal file
56
dae/files/dae.init
Normal file
@ -0,0 +1,56 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
extra_command "hot_reload" "Hot-reload service"
|
||||
|
||||
CONF="dae"
|
||||
PROG="/usr/bin/dae"
|
||||
LOG_DIR="/var/log/dae"
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
local config_file
|
||||
config_get config_file "config" "config_file" "/etc/dae/config.dae"
|
||||
|
||||
"$PROG" validate -c "$config_file" || return 1
|
||||
|
||||
local log_maxbackups log_maxsize
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "1"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param command "$PROG" run
|
||||
procd_append_param command --config "$config_file"
|
||||
procd_append_param command --disable-timestamp
|
||||
procd_append_param command --logfile "$LOG_DIR/dae.log"
|
||||
procd_append_param command --logfile-maxbackups "$log_maxbackups"
|
||||
procd_append_param command --logfile-maxsize "$log_maxsize"
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param respawn
|
||||
# procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
rm -rf "$LOG_DIR"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
||||
|
||||
hot_reload() {
|
||||
"$PROG" reload "$(cat /var/run/dae.pid)"
|
||||
}
|
7
dae/test.sh
Normal file
7
dae/test.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
case "$1" in
|
||||
"dae")
|
||||
dae --version | grep "$PKG_VERSION"
|
||||
;;
|
||||
esac
|
162
daed/Makefile
Normal file
162
daed/Makefile
Normal file
@ -0,0 +1,162 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION=0.6.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/daeuniverse/daed.git
|
||||
PKG_SOURCE_VERSION:=v$(PKG_VERSION)
|
||||
PKG_MIRROR_HASH:=a14a7c05816034d5beee050c535e636c39430253a00a9621202d8683373a7c71
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only MIT
|
||||
PKG_LICENSE_FILES:=LICENSE wing/LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
PKG_BUILD_DIR=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing
|
||||
PKG_BUILD_DEPENDS:=golang/host bpf-headers
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
GO_PKG:=github.com/daeuniverse/dae-wing
|
||||
GO_PKG_LDFLAGS:= \
|
||||
-X '$(GO_PKG)/db.AppDescription=$(PKG_NAME) is a integration solution of dae, API and UI.'
|
||||
GO_PKG_LDFLAGS_X= \
|
||||
$(GO_PKG)/db.AppName=$(PKG_NAME) \
|
||||
$(GO_PKG)/db.AppVersion=$(PKG_VERSION)
|
||||
GO_PKG_TAGS:=embedallowed,trace
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/bpf.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
TAR_CMD=$(HOST_TAR) -C $(BUILD_DIR)/ $(TAR_OPTIONS)
|
||||
|
||||
define Package/daed/Default
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
URL:=https://github.com/daeuniverse/daed
|
||||
endef
|
||||
|
||||
define Package/daed
|
||||
$(call Package/daed/Default)
|
||||
TITLE:=A Modern Dashboard For dae
|
||||
# You need enable KERNEL_DEBUG_INFO_BTF and KERNEL_BPF_EVENTS
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) $(BPF_DEPENDS) \
|
||||
+ca-bundle +kmod-sched-core +kmod-sched-bpf +kmod-xdp-sockets-diag \
|
||||
+kmod-veth
|
||||
endef
|
||||
|
||||
define Package/daed-geoip
|
||||
$(call Package/daed/Default)
|
||||
TITLE:=geoip for daed
|
||||
DEPENDS:=+daed +v2ray-geoip
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/daed-geosite
|
||||
$(call Package/daed/Default)
|
||||
TITLE:=geosite for daed
|
||||
DEPENDS:=+daed +v2ray-geosite
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/daed/description
|
||||
daed is a backend of dae, provides a method to bundle arbitrary
|
||||
frontend, dae and geodata into one binary.
|
||||
endef
|
||||
|
||||
define Package/daed/conffiles
|
||||
/etc/daed/wing.db
|
||||
/etc/config/daed
|
||||
endef
|
||||
|
||||
WEB_FILE:=$(PKG_NAME)-web-$(PKG_VERSION).zip
|
||||
define Download/daed-web
|
||||
URL:=https://github.com/daeuniverse/daed/releases/download/v$(PKG_VERSION)
|
||||
URL_FILE:=web.zip
|
||||
FILE:=$(WEB_FILE)
|
||||
HASH:=f8a5f28643c990408f7b6d324b4cc8b5e7445e6255689a5f10f5545be033c1ad
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
|
||||
( \
|
||||
mkdir -p $(PKG_BUILD_DIR)/webrender ; \
|
||||
unzip -q -d $(PKG_BUILD_DIR)/webrender/ $(DL_DIR)/$(WEB_FILE) ; \
|
||||
find $(PKG_BUILD_DIR)/webrender/web -type f -size +4k ! -name "*.gz" ! -name "*.woff" ! -name "*.woff2" -exec sh -c '\
|
||||
gzip -9 -k "{}"; \
|
||||
if [ "$$$$(stat -c %s "{}")" -lt "$$$$(stat -c %s "{}.gz")" ]; then \
|
||||
rm "{}.gz"; \
|
||||
else \
|
||||
rm "{}"; \
|
||||
fi' \
|
||||
";" ; \
|
||||
)
|
||||
endef
|
||||
|
||||
DAE_CFLAGS:= \
|
||||
-O2 -Wall -Werror \
|
||||
-DMAX_MATCH_SET_LEN=64 \
|
||||
-I$(BPF_HEADERS_DIR)/tools/lib \
|
||||
-I$(BPF_HEADERS_DIR)/arch/$(BPF_KARCH)/include/asm/mach-generic
|
||||
|
||||
ifneq ($(CONFIG_USE_MUSL),)
|
||||
TARGET_CFLAGS += -D_LARGEFILE64_SOURCE
|
||||
endif
|
||||
|
||||
define Build/Compile
|
||||
( \
|
||||
pushd $(PKG_BUILD_DIR) ; \
|
||||
export \
|
||||
$(GO_GENERAL_BUILD_CONFIG_VARS) \
|
||||
$(GO_PKG_BUILD_CONFIG_VARS) \
|
||||
$(GO_PKG_BUILD_VARS); \
|
||||
go generate ./... ; \
|
||||
cd dae-core ; \
|
||||
export \
|
||||
BPF_CLANG="$(CLANG)" \
|
||||
BPF_STRIP_FLAG="-strip=$(LLVM_STRIP)" \
|
||||
BPF_CFLAGS="$(DAE_CFLAGS)" \
|
||||
BPF_TARGET="bpfel,bpfeb" \
|
||||
BPF_TRACE_TARGET="$(GO_ARCH)" ; \
|
||||
go generate control/control.go ; \
|
||||
go generate trace/trace.go ; \
|
||||
popd ; \
|
||||
$(call GoPackage/Build/Compile) ; \
|
||||
)
|
||||
endef
|
||||
|
||||
define Package/daed/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae-wing $(1)/usr/bin/daed
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/daed.config $(1)/etc/config/daed
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/daed.init $(1)/etc/init.d/daed
|
||||
endef
|
||||
|
||||
define Package/daed-geoip/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/daed
|
||||
$(LN) ../v2ray/geoip.dat $(1)/usr/share/daed/geoip.dat
|
||||
endef
|
||||
|
||||
define Package/daed-geosite/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/daed
|
||||
$(LN) ../v2ray/geosite.dat $(1)/usr/share/daed/geosite.dat
|
||||
endef
|
||||
|
||||
$(eval $(call Download,daed-web))
|
||||
$(eval $(call GoBinPackage,daed))
|
||||
$(eval $(call BuildPackage,daed))
|
||||
$(eval $(call BuildPackage,daed-geoip))
|
||||
$(eval $(call BuildPackage,daed-geosite))
|
7
daed/files/daed.config
Normal file
7
daed/files/daed.config
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
config daed 'config'
|
||||
option enabled '0'
|
||||
option listen_addr '0.0.0.0:2023'
|
||||
option log_maxbackups '1'
|
||||
option log_maxsize '5'
|
||||
|
46
daed/files/daed.init
Normal file
46
daed/files/daed.init
Normal file
@ -0,0 +1,46 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
CONF="daed"
|
||||
PROG="/usr/bin/daed"
|
||||
LOG="/var/log/daed/daed.log"
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
local listen_addr log_maxbackups log_maxsize
|
||||
config_get listen_addr "config" "listen_addr" "0.0.0.0:2023"
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "5"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param command "$PROG" run
|
||||
procd_append_param command --config "/etc/daed/"
|
||||
procd_append_param command --listen "$listen_addr"
|
||||
procd_append_param command --logfile "$LOG"
|
||||
procd_append_param command --logfile-maxbackups "$log_maxbackups"
|
||||
procd_append_param command --logfile-maxsize "$log_maxsize"
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param respawn
|
||||
# procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
rm -f "$LOG"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
7
daed/test.sh
Normal file
7
daed/test.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
case "$1" in
|
||||
"daed")
|
||||
daed --version | grep "$PKG_VERSION"
|
||||
;;
|
||||
esac
|
13
luci-app-daed/Makefile
Normal file
13
luci-app-daed/Makefile
Normal file
@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI app for dae dashboard
|
||||
LUCI_DEPENDS:=+daed +daed-geoip +daed-geosite
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require view';
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('daed'), {}).then(function (res) {
|
||||
var isRunning = false;
|
||||
try {
|
||||
isRunning = res['daed']['instances']['daed']['running'];
|
||||
} catch (e) { }
|
||||
return isRunning;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning, port) {
|
||||
var spanTemp = '<span style="color:%s"><strong>%s %s</strong></span>';
|
||||
var renderHTML;
|
||||
if (isRunning) {
|
||||
var button = String.format(' <a class="btn cbi-button" href="http://%s:%s" target="_blank" rel="noreferrer noopener">%s</a>',
|
||||
window.location.hostname, port, _('Open Web Interface'));
|
||||
renderHTML = spanTemp.format('green', _('daed'), _('RUNNING')) + button;
|
||||
} else {
|
||||
renderHTML = spanTemp.format('red', _('daed'), _('NOT RUNNING'));
|
||||
}
|
||||
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('daed')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o;
|
||||
var webport = (uci.get(data[0], 'config', 'address') || '0.0.0.0:2023').split(':').slice(-1)[0];
|
||||
|
||||
m = new form.Map('daed', _('daed'),
|
||||
_('A modern dashboard for dae.'));
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.anonymous = true;
|
||||
s.render = function () {
|
||||
poll.add(function () {
|
||||
return L.resolveDefault(getServiceStatus()).then(function (res) {
|
||||
var view = document.getElementById('service_status');
|
||||
view.innerHTML = renderStatus(res, webport);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'daed');
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'listen_addr', _('Listening address'));
|
||||
o.datatype = 'ipaddrport(1)';
|
||||
o.default = '0.0.0.0:2023';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'log_maxbackups', _('Max log backups'),
|
||||
_('The maximum number of old log files to retain.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Value, 'log_maxsize', _('Max log size'),
|
||||
_('The maximum size in megabytes of the log file before it gets rotated.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
75
luci-app-daed/htdocs/luci-static/resources/view/daed/log.js
Normal file
75
luci-app-daed/htdocs/luci-static/resources/view/daed/log.js
Normal file
@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require view';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
/* Thanks to luci-app-aria2 */
|
||||
var css = ' \
|
||||
#log_textarea { \
|
||||
padding: 10px; \
|
||||
text-align: left; \
|
||||
} \
|
||||
#log_textarea pre { \
|
||||
padding: .5rem; \
|
||||
word-break: break-all; \
|
||||
margin: 0; \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
}';
|
||||
|
||||
var log_textarea = E('div', { 'id': 'log_textarea' },
|
||||
E('img', {
|
||||
'src': L.resource(['icons/loading.gif']),
|
||||
'alt': _('Loading...'),
|
||||
'style': 'vertical-align:middle'
|
||||
}, _('Collecting data...'))
|
||||
);
|
||||
|
||||
poll.add(L.bind(function() {
|
||||
return fs.read_direct('/var/log/daed/daed.log', 'text')
|
||||
.then(function(res) {
|
||||
var log = E('pre', { 'wrap': 'pre' }, [
|
||||
res.trim() || _('Log is empty.')
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
}).catch(function(err) {
|
||||
var log;
|
||||
|
||||
if (err.toString().includes('NotFoundError'))
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Log file does not exist.')
|
||||
]);
|
||||
else
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Unknown error: %s').format(err)
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
});
|
||||
}));
|
||||
|
||||
return E([
|
||||
E('style', [ css ]),
|
||||
E('div', {'class': 'cbi-map'}, [
|
||||
E('div', {'class': 'cbi-section'}, [
|
||||
log_textarea,
|
||||
E('div', {'style': 'text-align:right'},
|
||||
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
86
luci-app-daed/po/templates/daed.pot
Normal file
86
luci-app-daed/po/templates/daed.pot
Normal file
@ -0,0 +1,86 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:53
|
||||
msgid "A modern dashboard for dae."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:66
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:32
|
||||
msgid "Collecting data..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:72
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/root/usr/share/rpcd/acl.d/luci-app-daed.json:3
|
||||
msgid "Grant access to daed configuration"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:76
|
||||
msgid "Listening address"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:30
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json:22
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:48
|
||||
msgid "Log file does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:39
|
||||
msgid "Log is empty."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:81
|
||||
msgid "Max log backups"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:86
|
||||
msgid "Max log size"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:35
|
||||
msgid "NOT RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:32
|
||||
msgid "Open Web Interface"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:33
|
||||
msgid "RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:65
|
||||
msgid "Refresh every %s seconds."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json:14
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:82
|
||||
msgid "The maximum number of old log files to retain."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:87
|
||||
msgid "The maximum size in megabytes of the log file before it gets rotated."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:52
|
||||
msgid "Unknown error: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:33
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:35
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:52
|
||||
#: applications/luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json:3
|
||||
msgid "daed"
|
||||
msgstr ""
|
1
luci-app-daed/po/zh-cn
Symbolic link
1
luci-app-daed/po/zh-cn
Symbolic link
@ -0,0 +1 @@
|
||||
zh_Hans
|
93
luci-app-daed/po/zh_Hans/daed.po
Normal file
93
luci-app-daed/po/zh_Hans/daed.po
Normal file
@ -0,0 +1,93 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh-Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:53
|
||||
msgid "A modern dashboard for dae."
|
||||
msgstr "dae 现代化控制面板。"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:66
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:32
|
||||
msgid "Collecting data..."
|
||||
msgstr "收集数据中..."
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:72
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
#: applications/luci-app-daed/root/usr/share/rpcd/acl.d/luci-app-daed.json:3
|
||||
msgid "Grant access to daed configuration"
|
||||
msgstr "授予访问 daed 配置的权限"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:76
|
||||
msgid "Listening address"
|
||||
msgstr "监听地址"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:30
|
||||
msgid "Loading..."
|
||||
msgstr "加载中..."
|
||||
|
||||
#: applications/luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json:22
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:48
|
||||
msgid "Log file does not exist."
|
||||
msgstr "日志文件不存在。"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:39
|
||||
msgid "Log is empty."
|
||||
msgstr "日志为空"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:81
|
||||
msgid "Max log backups"
|
||||
msgstr "最大日志备份"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:86
|
||||
msgid "Max log size"
|
||||
msgstr "最大日志大小"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:35
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:32
|
||||
msgid "Open Web Interface"
|
||||
msgstr "打开 Web 界面"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:33
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:65
|
||||
msgid "Refresh every %s seconds."
|
||||
msgstr "每 %s 秒刷新。"
|
||||
|
||||
#: applications/luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json:14
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:82
|
||||
msgid "The maximum number of old log files to retain."
|
||||
msgstr "要保留的最大旧日志文件数量。"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:87
|
||||
msgid "The maximum size in megabytes of the log file before it gets rotated."
|
||||
msgstr "要保留的最大日志大小(单位:MB)。"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/log.js:52
|
||||
msgid "Unknown error: %s"
|
||||
msgstr "未知错误:%s"
|
||||
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:33
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:35
|
||||
#: applications/luci-app-daed/htdocs/luci-static/resources/view/daed/config.js:52
|
||||
#: applications/luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json:3
|
||||
msgid "daed"
|
||||
msgstr "daed"
|
29
luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json
Normal file
29
luci-app-daed/root/usr/share/luci/menu.d/luci-app-daed.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"admin/services/daed": {
|
||||
"title": "daed",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-daed" ],
|
||||
"uci": { "daed": true }
|
||||
}
|
||||
},
|
||||
"admin/services/daed/config": {
|
||||
"title": "Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daed/config"
|
||||
}
|
||||
},
|
||||
"admin/services/daed/log": {
|
||||
"title": "Log",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daed/log"
|
||||
}
|
||||
}
|
||||
}
|
17
luci-app-daed/root/usr/share/rpcd/acl.d/luci-app-daed.json
Normal file
17
luci-app-daed/root/usr/share/rpcd/acl.d/luci-app-daed.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-daed": {
|
||||
"description": "Grant access to daed configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/var/log/daed/daed.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "daed" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "daed" ]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for FileBrowser
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=1.1
|
||||
LUCI_TITLE:=LuCI File Browser module
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_VERSION:=1.1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Sergey Ponomarev <stokito@gmail.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require form';
|
||||
|
||||
var formData = {
|
||||
files: {
|
||||
root: null,
|
||||
}
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.JSONMap(formData, _('File Browser'), '');
|
||||
|
||||
s = m.section(form.NamedSection, 'files', 'files');
|
||||
|
||||
o = s.option(form.FileUpload, 'root', '');
|
||||
o.root_directory = '/';
|
||||
o.browser = true;
|
||||
o.show_hidden = true;
|
||||
o.enable_upload = true;
|
||||
o.enable_remove = true;
|
||||
o.enable_download = true;
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
})
|
@ -1,61 +0,0 @@
|
||||
-- Copyright 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
module("luci.controller.filebrowser", package.seeall)
|
||||
|
||||
local http = require "luci.http"
|
||||
local api = require "luci.model.cbi.filebrowser.api"
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/filebrowser") then return end
|
||||
|
||||
entry({"admin", "nas"}, firstchild(), "NAS", 44).dependent = false
|
||||
entry({"admin", "nas", "filebrowser"}, cbi("filebrowser/settings"),
|
||||
_("File Browser"), 2).dependent = true
|
||||
|
||||
entry({"admin", "nas", "filebrowser", "check"}, call("action_check")).leaf =
|
||||
true
|
||||
entry({"admin", "nas", "filebrowser", "download"}, call("action_download")).leaf =
|
||||
true
|
||||
entry({"admin", "nas", "filebrowser", "status"}, call("act_status")).leaf =
|
||||
true
|
||||
entry({"admin", "nas", "filebrowser", "get_log"}, call("get_log")).leaf =
|
||||
true
|
||||
entry({"admin", "nas", "filebrowser", "clear_log"}, call("clear_log")).leaf =
|
||||
true
|
||||
end
|
||||
|
||||
local function http_write_json(content)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(content or {code = 1})
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e = {}
|
||||
e.status = luci.sys.call(
|
||||
"busybox ps -w | grep -v grep | grep 'filebrowser -a 0.0.0.0' >/dev/null") ==
|
||||
0
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
function action_check()
|
||||
local json = api.to_check()
|
||||
http_write_json(json)
|
||||
end
|
||||
|
||||
function action_download()
|
||||
local json = nil
|
||||
local task = http.formvalue("task")
|
||||
if task == "extract" then
|
||||
json = api.to_extract(http.formvalue("file"))
|
||||
elseif task == "move" then
|
||||
json = api.to_move(http.formvalue("file"))
|
||||
else
|
||||
json = api.to_download(http.formvalue("url"))
|
||||
end
|
||||
http_write_json(json)
|
||||
end
|
||||
|
||||
function get_log()
|
||||
luci.http.write(luci.sys.exec(
|
||||
"[ -f '/var/log/filebrowser.log' ] && cat /var/log/filebrowser.log"))
|
||||
end
|
||||
function clear_log() luci.sys.call("echo '' > /var/log/filebrowser.log") end
|
@ -1,338 +0,0 @@
|
||||
local fs = require "nixio.fs"
|
||||
local sys = require "luci.sys"
|
||||
local uci = require"luci.model.uci".cursor()
|
||||
local util = require "luci.util"
|
||||
local i18n = require "luci.i18n"
|
||||
|
||||
module("luci.model.cbi.filebrowser.api", package.seeall)
|
||||
|
||||
local appname = "filebrowser"
|
||||
local api_url =
|
||||
"https://api.github.com/repos/filebrowser/filebrowser/releases/latest"
|
||||
|
||||
local wget = "/usr/bin/wget"
|
||||
local wget_args = {
|
||||
"--no-check-certificate", "--quiet", "--timeout=10", "--tries=2"
|
||||
}
|
||||
local command_timeout = 300
|
||||
|
||||
local LEDE_BOARD = nil
|
||||
local DISTRIB_TARGET = nil
|
||||
|
||||
function uci_get_type(type, config, default)
|
||||
value = uci:get_first(appname, type, config, default) or sys.exec(
|
||||
"echo -n `uci -q get " .. appname .. ".@" .. type .. "[0]." ..
|
||||
config .. "`")
|
||||
if (value == nil or value == "") and (default and default ~= "") then
|
||||
value = default
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function _unpack(t, i)
|
||||
i = i or 1
|
||||
if t[i] ~= nil then return t[i], _unpack(t, i + 1) end
|
||||
end
|
||||
|
||||
local function exec(cmd, args, writer, timeout)
|
||||
local os = require "os"
|
||||
local nixio = require "nixio"
|
||||
|
||||
local fdi, fdo = nixio.pipe()
|
||||
local pid = nixio.fork()
|
||||
|
||||
if pid > 0 then
|
||||
fdo:close()
|
||||
|
||||
if writer or timeout then
|
||||
local starttime = os.time()
|
||||
while true do
|
||||
if timeout and os.difftime(os.time(), starttime) >= timeout then
|
||||
nixio.kill(pid, nixio.const.SIGTERM)
|
||||
return 1
|
||||
end
|
||||
|
||||
if writer then
|
||||
local buffer = fdi:read(2048)
|
||||
if buffer and #buffer > 0 then
|
||||
writer(buffer)
|
||||
end
|
||||
end
|
||||
|
||||
local wpid, stat, code = nixio.waitpid(pid, "nohang")
|
||||
|
||||
if wpid and stat == "exited" then return code end
|
||||
|
||||
if not writer and timeout then nixio.nanosleep(1) end
|
||||
end
|
||||
else
|
||||
local wpid, stat, code = nixio.waitpid(pid)
|
||||
return wpid and stat == "exited" and code
|
||||
end
|
||||
elseif pid == 0 then
|
||||
nixio.dup(fdo, nixio.stdout)
|
||||
fdi:close()
|
||||
fdo:close()
|
||||
nixio.exece(cmd, args, nil)
|
||||
nixio.stdout:close()
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function compare_versions(ver1, comp, ver2)
|
||||
local table = table
|
||||
|
||||
local av1 = util.split(ver1, "[%.%-]", nil, true)
|
||||
local av2 = util.split(ver2, "[%.%-]", nil, true)
|
||||
|
||||
local max = table.getn(av1)
|
||||
local n2 = table.getn(av2)
|
||||
if (max < n2) then max = n2 end
|
||||
|
||||
for i = 1, max, 1 do
|
||||
local s1 = av1[i] or ""
|
||||
local s2 = av2[i] or ""
|
||||
|
||||
if comp == "~=" and (s1 ~= s2) then return true end
|
||||
if (comp == "<" or comp == "<=") and (s1 < s2) then return true end
|
||||
if (comp == ">" or comp == ">=") and (s1 > s2) then return true end
|
||||
if (s1 ~= s2) then return false end
|
||||
end
|
||||
|
||||
return not (comp == "<" or comp == ">")
|
||||
end
|
||||
|
||||
local function auto_get_arch()
|
||||
local arch = nixio.uname().machine or ""
|
||||
if fs.access("/usr/lib/os-release") then
|
||||
LEDE_BOARD = sys.exec(
|
||||
"echo -n `grep 'LEDE_BOARD' /usr/lib/os-release | awk -F '[\\042\\047]' '{print $2}'`")
|
||||
end
|
||||
if fs.access("/etc/openwrt_release") then
|
||||
DISTRIB_TARGET = sys.exec(
|
||||
"echo -n `grep 'DISTRIB_TARGET' /etc/openwrt_release | awk -F '[\\042\\047]' '{print $2}'`")
|
||||
end
|
||||
|
||||
if arch == "mips" then
|
||||
if LEDE_BOARD and LEDE_BOARD ~= "" then
|
||||
if string.match(LEDE_BOARD, "ramips") == "ramips" then
|
||||
arch = "ramips"
|
||||
else
|
||||
arch = sys.exec("echo '" .. LEDE_BOARD ..
|
||||
"' | grep -oE 'ramips|ar71xx'")
|
||||
end
|
||||
elseif DISTRIB_TARGET and DISTRIB_TARGET ~= "" then
|
||||
if string.match(DISTRIB_TARGET, "ramips") == "ramips" then
|
||||
arch = "ramips"
|
||||
else
|
||||
arch = sys.exec("echo '" .. DISTRIB_TARGET ..
|
||||
"' | grep -oE 'ramips|ar71xx'")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return util.trim(arch)
|
||||
end
|
||||
|
||||
local function get_file_info(arch)
|
||||
local file_tree = ""
|
||||
local sub_version = ""
|
||||
|
||||
if arch == "x86_64" then
|
||||
file_tree = "amd64"
|
||||
elseif arch == "aarch64" then
|
||||
file_tree = "arm64"
|
||||
elseif arch == "ramips" then
|
||||
file_tree = "mipsle"
|
||||
elseif arch == "ar71xx" then
|
||||
file_tree = "mips"
|
||||
elseif arch:match("^i[%d]86$") then
|
||||
file_tree = "386"
|
||||
elseif arch:match("^armv[5-8]") then
|
||||
file_tree = "armv"
|
||||
sub_version = arch:match("[5-8]")
|
||||
if LEDE_BOARD and string.match(LEDE_BOARD, "bcm53xx") == "bcm53xx" then
|
||||
sub_version = "5"
|
||||
elseif DISTRIB_TARGET and string.match(DISTRIB_TARGET, "bcm53xx") ==
|
||||
"bcm53xx" then
|
||||
sub_version = "5"
|
||||
end
|
||||
sub_version = "5"
|
||||
end
|
||||
|
||||
return file_tree, sub_version
|
||||
end
|
||||
|
||||
local function get_api_json(url)
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
local output = {}
|
||||
-- exec(wget, { "-O-", url, _unpack(wget_args) },
|
||||
-- function(chunk) output[#output + 1] = chunk end)
|
||||
-- local json_content = util.trim(table.concat(output))
|
||||
|
||||
local json_content = luci.sys.exec(wget ..
|
||||
" --no-check-certificate --timeout=10 -t 1 -O- " ..
|
||||
url)
|
||||
|
||||
if json_content == "" then return {} end
|
||||
|
||||
return jsonc.parse(json_content) or {}
|
||||
end
|
||||
|
||||
function get_version() return uci_get_type("global", "version", "0") end
|
||||
|
||||
function to_check(arch)
|
||||
if not arch or arch == "" then arch = auto_get_arch() end
|
||||
|
||||
local file_tree, sub_version = get_file_info(arch)
|
||||
|
||||
if file_tree == "" then
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translate(
|
||||
"Can't determine ARCH, or ARCH not supported.")
|
||||
}
|
||||
end
|
||||
|
||||
local json = get_api_json(api_url)
|
||||
|
||||
if json.tag_name == nil then
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translate("Get remote version info failed.")
|
||||
}
|
||||
end
|
||||
|
||||
local remote_version = json.tag_name:match("[^v]+")
|
||||
|
||||
local needs_update = compare_versions(get_version(), "<", remote_version)
|
||||
local html_url, download_url
|
||||
|
||||
if needs_update then
|
||||
html_url = json.html_url
|
||||
for _, v in ipairs(json.assets) do
|
||||
if v.name and v.name:match("linux%-" .. file_tree .. sub_version) then
|
||||
download_url = v.browser_download_url
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if needs_update and not download_url then
|
||||
return {
|
||||
code = 1,
|
||||
version = remote_version,
|
||||
html_url = html_url,
|
||||
error = i18n.translate(
|
||||
"New version found, but failed to get new version download url.")
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
code = 0,
|
||||
update = needs_update,
|
||||
version = remote_version,
|
||||
url = {html = html_url, download = download_url}
|
||||
}
|
||||
end
|
||||
|
||||
function to_download(url)
|
||||
if not url or url == "" then
|
||||
return {code = 1, error = i18n.translate("Download url is required.")}
|
||||
end
|
||||
|
||||
sys.call("/bin/rm -f /tmp/filebrowser_download.*")
|
||||
|
||||
local tmp_file = util.trim(util.exec(
|
||||
"mktemp -u -t filebrowser_download.XXXXXX"))
|
||||
|
||||
local result = exec(wget, {"-O", tmp_file, url, _unpack(wget_args)}, nil,
|
||||
command_timeout) == 0
|
||||
|
||||
if not result then
|
||||
exec("/bin/rm", {"-f", tmp_file})
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translatef("File download failed or timed out: %s", url)
|
||||
}
|
||||
end
|
||||
|
||||
return {code = 0, file = tmp_file}
|
||||
end
|
||||
|
||||
function to_extract(file, subfix)
|
||||
if not file or file == "" or not fs.access(file) then
|
||||
return {code = 1, error = i18n.translate("File path required.")}
|
||||
end
|
||||
|
||||
sys.call("/bin/rm -rf /tmp/filebrowser_extract.*")
|
||||
local tmp_dir = util.trim(util.exec(
|
||||
"mktemp -d -t filebrowser_extract.XXXXXX"))
|
||||
|
||||
local output = {}
|
||||
exec("/bin/tar", {"-C", tmp_dir, "-zxvf", file},
|
||||
function(chunk) output[#output + 1] = chunk end)
|
||||
|
||||
local files = util.split(table.concat(output))
|
||||
|
||||
exec("/bin/rm", {"-f", file})
|
||||
|
||||
if not new_file then
|
||||
for _, f in pairs(files) do
|
||||
if f:match("filebrowser") then
|
||||
new_file = tmp_dir .. "/" .. util.trim(f)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not new_file then
|
||||
exec("/bin/rm", {"-rf", tmp_dir})
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translatef("Can't find client in file: %s", file)
|
||||
}
|
||||
end
|
||||
|
||||
return {code = 0, file = new_file}
|
||||
end
|
||||
|
||||
function to_move(file)
|
||||
if not file or file == "" or not fs.access(file) then
|
||||
sys.call("/bin/rm -rf /tmp/filebrowser_extract.*")
|
||||
return {code = 1, error = i18n.translate("Client file is required.")}
|
||||
end
|
||||
local project_directory =
|
||||
uci_get_type("global", "project_directory", "/tmp")
|
||||
luci.sys.exec("mkdir -p " .. project_directory)
|
||||
local client_path = project_directory .. "/" .. appname
|
||||
local client_path_bak
|
||||
|
||||
if fs.access(client_path) then
|
||||
client_path_bak = "/tmp/" .. appname .. ".bak"
|
||||
exec("/bin/mv", {"-f", client_path, client_path_bak})
|
||||
end
|
||||
|
||||
local result = exec("/bin/mv", {"-f", file, client_path}, nil,
|
||||
command_timeout) == 0
|
||||
|
||||
if not result or not fs.access(client_path) then
|
||||
if client_path_bak then
|
||||
exec("/bin/mv", {"-f", client_path_bak, client_path})
|
||||
end
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translatef("Can't move new file to path: %s",
|
||||
client_path)
|
||||
}
|
||||
end
|
||||
|
||||
exec("/bin/chmod", {"755", client_path})
|
||||
|
||||
if client_path_bak then exec("/bin/rm", {"-f", client_path_bak}) end
|
||||
|
||||
sys.call("/bin/rm -rf /tmp/filebrowser_extract.*")
|
||||
|
||||
return {code = 0}
|
||||
end
|
@ -1,37 +0,0 @@
|
||||
m = Map("filebrowser", translate("FileBrowser"), translate(
|
||||
"File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"))
|
||||
m:append(Template("filebrowser/status"))
|
||||
|
||||
s = m:section(TypedSection, "global", translate("Global Settings"))
|
||||
s.anonymous = true
|
||||
s.addremove = false
|
||||
|
||||
o = s:option(Flag, "enable", translate("Enable"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "port", translate("Listen port"))
|
||||
o.datatype = "port"
|
||||
o.default = 8088
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "root_path", translate("Root path"), translate(
|
||||
"Point to a path to access your files in the web interface, default is /"))
|
||||
o.default = "/"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "project_directory", translate("Project directory"),
|
||||
translate(
|
||||
"The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory<br />For example, /mnt/sda1<br />For example, /tmp"))
|
||||
o.default = "/tmp"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Button, "_download", translate("Manually download"), translate(
|
||||
"Make sure you have enough space. <br /><font style='color:red'>Be sure to fill out the project storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"))
|
||||
o.template = "filebrowser/download"
|
||||
o.inputstyle = "apply"
|
||||
o.btnclick = "downloadClick(this);"
|
||||
o.id = "download_btn"
|
||||
|
||||
m:append(Template("filebrowser/log"))
|
||||
|
||||
return m
|
@ -1,169 +0,0 @@
|
||||
<%
|
||||
local dsp = require "luci.dispatcher"
|
||||
-%>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var msgInfo;
|
||||
|
||||
var tokenStr = '<%=token%>';
|
||||
var clickToDownloadText = '<%:Click to download%>';
|
||||
var inProgressText = '<%:Downloading...%>';
|
||||
var downloadInProgressNotice = '<%:Download, are you sure to close?%>';
|
||||
var downloadSuccessText = '<%:Download successful%>';
|
||||
var unexpectedErrorText = '<%:Unexpected error%>';
|
||||
|
||||
function addPageNotice() {
|
||||
window.onbeforeunload = function(e) {
|
||||
e.returnValue = downloadInProgressNotice;
|
||||
return downloadInProgressNotice;
|
||||
};
|
||||
}
|
||||
|
||||
function removePageNotice() {
|
||||
window.onbeforeunload = undefined;
|
||||
}
|
||||
|
||||
function onUpdateSuccess(btn) {
|
||||
alert(downloadSuccessText);
|
||||
|
||||
if (btn) {
|
||||
btn.value = downloadSuccessText;
|
||||
btn.placeholder = downloadSuccessText;
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
window.setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function onRequestError(btn, errorMessage) {
|
||||
btn.disabled = false;
|
||||
btn.value = btn.placeholder;
|
||||
|
||||
if (errorMessage) {
|
||||
alert(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function doAjaxGet(url, data, onResult) {
|
||||
new XHR().get(url, data, function(_, json) {
|
||||
var resultJson = json || {
|
||||
'code': 1,
|
||||
'error': unexpectedErrorText
|
||||
};
|
||||
|
||||
if (typeof onResult === 'function') {
|
||||
onResult(resultJson);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function downloadClick(btn) {
|
||||
if (msgInfo === undefined) {
|
||||
checkUpdate(btn);
|
||||
} else {
|
||||
doDownload(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function checkUpdate(btn) {
|
||||
btn.disabled = true;
|
||||
btn.value = inProgressText;
|
||||
|
||||
addPageNotice();
|
||||
|
||||
var ckeckDetailElm = document.getElementById(btn.id + '-detail');
|
||||
|
||||
doAjaxGet('<%=dsp.build_url("admin/nas/filebrowser/check")%>/', {
|
||||
token: tokenStr
|
||||
}, function (json) {
|
||||
removePageNotice();
|
||||
if (json.code) {
|
||||
eval('Info = undefined');
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
eval('Info = json');
|
||||
btn.disabled = false;
|
||||
btn.value = clickToDownloadText;
|
||||
btn.placeholder = clickToDownloadText;
|
||||
}
|
||||
|
||||
if (ckeckDetailElm) {
|
||||
var urlNode = '';
|
||||
if (json.version) {
|
||||
urlNode = '<em style="color:red;"><%:The latest version:%>' + json.version + '</em>';
|
||||
if (json.url && json.url.html) {
|
||||
urlNode = '<a href="' + json.url.html + '" target="_blank">' + urlNode + '</a>';
|
||||
}
|
||||
}
|
||||
ckeckDetailElm.innerHTML = urlNode;
|
||||
}
|
||||
msgInfo = json;
|
||||
});
|
||||
}
|
||||
|
||||
function doDownload(btn) {
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Downloading...%>';
|
||||
|
||||
addPageNotice();
|
||||
|
||||
var UpdateUrl = '<%=dsp.build_url("admin/nas/filebrowser/download")%>';
|
||||
// Download file
|
||||
doAjaxGet(UpdateUrl, {
|
||||
token: tokenStr,
|
||||
url: msgInfo ? msgInfo.url.download : ''
|
||||
}, function (json) {
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
btn.value = '<%:Unpacking...%>';
|
||||
|
||||
// Extract file
|
||||
doAjaxGet(UpdateUrl, {
|
||||
token: tokenStr,
|
||||
task: 'extract',
|
||||
file: json.file
|
||||
}, function (json) {
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
btn.value = '<%:Moving...%>';
|
||||
|
||||
// Move file to target dir
|
||||
doAjaxGet(UpdateUrl, {
|
||||
token: tokenStr,
|
||||
task: 'move',
|
||||
file: json.file
|
||||
}, function (json) {
|
||||
removePageNotice();
|
||||
if (json.code) {
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
onUpdateSuccess(btn);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
//]]></script>
|
||||
|
||||
<%+cbi/valueheader%>
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" type="button"<%=
|
||||
attr("name", cbid) ..
|
||||
attr("id", self.id or cbid) ..
|
||||
attr("value", self.inputtitle or self.title) ..
|
||||
ifattr(self.btnclick, "onclick", self.btnclick) ..
|
||||
ifattr(self.placeholder, "placeholder")
|
||||
%> />
|
||||
<span id="<%=self.id or cbid%>-detail"></span>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
<%+cbi/valuefooter%>
|
@ -1,31 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function clear_log(btn) {
|
||||
XHR.get('<%=url([[admin]], [[nas]], [[filebrowser]], [[clear_log]])%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
var log_textarea = document.getElementById('log_textarea');
|
||||
log_textarea.innerHTML = "";
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
XHR.poll(3, '<%=url([[admin]], [[nas]], [[filebrowser]], [[get_log]])%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
var log_textarea = document.getElementById('log_textarea');
|
||||
log_textarea.innerHTML = x.responseText;
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<fieldset class="cbi-section" id="_log_fieldset">
|
||||
<legend>
|
||||
<%:Logs%>
|
||||
</legend>
|
||||
<input class="cbi-button cbi-input-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>">
|
||||
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="5" wrap="off" readonly="readonly"></textarea>
|
||||
</fieldset>
|
@ -1,29 +0,0 @@
|
||||
<%
|
||||
local dsp = require "luci.dispatcher"
|
||||
-%>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<legend><%:Running Status%></legend>
|
||||
<fieldset class="cbi-section">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title"><%:Status%></label>
|
||||
<div class="cbi-value-field" id="_status"><%:Collecting data...%></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var _status = document.getElementById('_status');
|
||||
XHR.poll(3,'<%=dsp.build_url("admin/nas/filebrowser/status")%>', null,
|
||||
function(x, json) {
|
||||
if (x && x.status == 200) {
|
||||
if (_status)
|
||||
_status.innerHTML = json.status ? '<font color="green"><%:RUNNING%> ✓</font><p><input type="button" class="cbi-button cbi-input-apply" value="<%:Enter interface%>" onclick="openwebui()" /></p>' : '<font color="red"><%:NOT RUNNING%> X</font>';
|
||||
}
|
||||
});
|
||||
|
||||
function openwebui(){
|
||||
var url = window.location.host+":<%=luci.sys.exec("uci get filebrowser.@global[0].port"):gsub("^%s*(.-)%s*$", "%1")%>";
|
||||
window.open('http://'+url,'target','');
|
||||
}
|
||||
//]]></script>
|
14
luci-app-filebrowser/po/ar/filebrowser.po
Normal file
14
luci-app-filebrowser/po/ar/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: ar\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/cs/filebrowser.po
Normal file
14
luci-app-filebrowser/po/cs/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: cs\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/da/filebrowser.po
Normal file
14
luci-app-filebrowser/po/da/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: da\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/de/filebrowser.po
Normal file
14
luci-app-filebrowser/po/de/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: de\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
20
luci-app-filebrowser/po/es/filebrowser.po
Normal file
20
luci-app-filebrowser/po/es/filebrowser.po
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-06-20 20:09+0000\n"
|
||||
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/es/>\n"
|
||||
"Language: es\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.6-rc\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "Explorador de archivos"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Conceder acceso al Explorador de archivos"
|
20
luci-app-filebrowser/po/fa/filebrowser.po
Normal file
20
luci-app-filebrowser/po/fa/filebrowser.po
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-04-13 18:37+0000\n"
|
||||
"Last-Translator: Danial Behzadi <dani.behzi@ubuntu.com>\n"
|
||||
"Language-Team: Persian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/fa/>\n"
|
||||
"Language: fa\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 5.5-dev\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "مرورگر پرونده"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "اعطای دسترسی به مرورگر پرونده"
|
14
luci-app-filebrowser/po/fi/filebrowser.po
Normal file
14
luci-app-filebrowser/po/fi/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: fi\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/fr/filebrowser.po
Normal file
14
luci-app-filebrowser/po/fr/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: fr\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/hu/filebrowser.po
Normal file
14
luci-app-filebrowser/po/hu/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: hu\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
20
luci-app-filebrowser/po/it/filebrowser.po
Normal file
20
luci-app-filebrowser/po/it/filebrowser.po
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-06-30 15:21+0000\n"
|
||||
"Last-Translator: moreno matassini <morenomatassini95@gmail.com>\n"
|
||||
"Language-Team: Italian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/it/>\n"
|
||||
"Language: it\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.7-dev\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "esplora file"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Concedi l'accesso a esplora file"
|
14
luci-app-filebrowser/po/ja/filebrowser.po
Normal file
14
luci-app-filebrowser/po/ja/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: ja\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
22
luci-app-filebrowser/po/lt/filebrowser.po
Normal file
22
luci-app-filebrowser/po/lt/filebrowser.po
Normal file
@ -0,0 +1,22 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-04-30 03:55+0000\n"
|
||||
"Last-Translator: Džiugas J <dziugas1959@hotmail.com>\n"
|
||||
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/lt/>\n"
|
||||
"Language: lt\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n % 10 == 1 && (n % 100 < 11 || n % 100 > "
|
||||
"19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? "
|
||||
"1 : 2);\n"
|
||||
"X-Generator: Weblate 5.5.2\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "Failų naršyklė"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Duoti prieigą prie failų naršyklės"
|
14
luci-app-filebrowser/po/nl/filebrowser.po
Normal file
14
luci-app-filebrowser/po/nl/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: nl\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
21
luci-app-filebrowser/po/pl/filebrowser.po
Normal file
21
luci-app-filebrowser/po/pl/filebrowser.po
Normal file
@ -0,0 +1,21 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-04-13 18:37+0000\n"
|
||||
"Last-Translator: Matthaiks <kitynska@gmail.com>\n"
|
||||
"Language-Team: Polish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/pl/>\n"
|
||||
"Language: pl\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.5-dev\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "Przeglądarka plików"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Udziel dostępu do Przeglądarki plików"
|
14
luci-app-filebrowser/po/pt/filebrowser.po
Normal file
14
luci-app-filebrowser/po/pt/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: pt\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/pt_BR/filebrowser.po
Normal file
14
luci-app-filebrowser/po/pt_BR/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: pt_BR\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/ro/filebrowser.po
Normal file
14
luci-app-filebrowser/po/ro/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: ro\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
21
luci-app-filebrowser/po/ru/filebrowser.po
Normal file
21
luci-app-filebrowser/po/ru/filebrowser.po
Normal file
@ -0,0 +1,21 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-04-13 18:37+0000\n"
|
||||
"Last-Translator: Yurt Page <yurtpage@gmail.com>\n"
|
||||
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/ru/>\n"
|
||||
"Language: ru\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.5-dev\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "Обозреватель Файлов"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Предоставить доступ к Обозревателю Файлов"
|
14
luci-app-filebrowser/po/sk/filebrowser.po
Normal file
14
luci-app-filebrowser/po/sk/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: sk\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
20
luci-app-filebrowser/po/sv/filebrowser.po
Normal file
20
luci-app-filebrowser/po/sv/filebrowser.po
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-05-12 20:34+0000\n"
|
||||
"Last-Translator: Daniel Nilsson <daniel.nilsson94@outlook.com>\n"
|
||||
"Language-Team: Swedish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/sv/>\n"
|
||||
"Language: sv\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.5.4\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "Filbläddrare"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Ge åtkomst till Filbläddrare"
|
11
luci-app-filebrowser/po/templates/filebrowser.pot
Normal file
11
luci-app-filebrowser/po/templates/filebrowser.pot
Normal file
@ -0,0 +1,11 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
20
luci-app-filebrowser/po/tr/filebrowser.po
Normal file
20
luci-app-filebrowser/po/tr/filebrowser.po
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-04-13 18:37+0000\n"
|
||||
"Last-Translator: Oğuz Ersen <oguz@ersen.moe>\n"
|
||||
"Language-Team: Turkish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsfilebrowser/tr/>\n"
|
||||
"Language: tr\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.5-dev\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "Dosya Gezgini"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "Dosya gezginine erişim izni verin"
|
14
luci-app-filebrowser/po/uk/filebrowser.po
Normal file
14
luci-app-filebrowser/po/uk/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: uk\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
14
luci-app-filebrowser/po/vi/filebrowser.po
Normal file
14
luci-app-filebrowser/po/vi/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: vi\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
1
luci-app-filebrowser/po/zh-cn
Symbolic link
1
luci-app-filebrowser/po/zh-cn
Symbolic link
@ -0,0 +1 @@
|
||||
zh_Hans
|
@ -1,107 +0,0 @@
|
||||
msgid "File Browser"
|
||||
msgstr "文件浏览器"
|
||||
|
||||
msgid "File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"
|
||||
msgstr "文件浏览器是一种创建你自己的云的软件,你可以在服务器上安装它,将它指向一个路径,然后通过一个漂亮的web界面访问你的文件。您有许多可用的特性!"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Enter interface"
|
||||
msgstr "进入界面"
|
||||
|
||||
msgid "Global Settings"
|
||||
msgstr "全局设置"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Listen port"
|
||||
msgstr "监听端口"
|
||||
|
||||
msgid "Root path"
|
||||
msgstr "指向路径"
|
||||
|
||||
msgid "Point to a path to access your files in the web interface, default is /"
|
||||
msgstr "指向一个路径,可在web界面访问你的文件,默认为 /"
|
||||
|
||||
msgid "Project directory"
|
||||
msgstr "项目存放目录"
|
||||
|
||||
msgid "The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory<br />For example, /mnt/sda1<br />For example, /tmp"
|
||||
msgstr "文件较大,至少需要32M空间。建议插入U盘或硬盘,或放入tmp目录里使用<br />例如:/mnt/sda1<br />例如:/tmp"
|
||||
|
||||
msgid "Manually download"
|
||||
msgstr "手动下载"
|
||||
|
||||
msgid "Make sure you have enough space. <br /><font style='color:red'>Be sure to fill out the project storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"
|
||||
msgstr "请确保具有足够的空间。<br /><font style='color:red'>第一次运行务必填好项目存放目录,然后保存应用。再手动下载,否则无法使用!</font>"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Clear logs"
|
||||
msgstr "清空日志"
|
||||
|
||||
msgid "It is the latest version"
|
||||
msgstr "已是最新版本"
|
||||
|
||||
msgid "Download successful"
|
||||
msgstr "下载成功"
|
||||
|
||||
msgid "Click to download"
|
||||
msgstr "点击下载"
|
||||
|
||||
msgid "Updating..."
|
||||
msgstr "更新中"
|
||||
|
||||
msgid "Unexpected error"
|
||||
msgstr "意外错误"
|
||||
|
||||
msgid "Download, are you sure to close?"
|
||||
msgstr "正在下载,你确认要关闭吗?"
|
||||
|
||||
msgid "Downloading..."
|
||||
msgstr "下载中"
|
||||
|
||||
msgid "Unpacking..."
|
||||
msgstr "解压中"
|
||||
|
||||
msgid "Moving..."
|
||||
msgstr "移动中"
|
||||
|
||||
msgid "The latest version:"
|
||||
msgstr "最新版本:"
|
||||
|
||||
msgid "Can't determine ARCH, or ARCH not supported."
|
||||
msgstr "无法确认ARCH架构,或是不支持。"
|
||||
|
||||
msgid "Get remote version info failed."
|
||||
msgstr "获取远程版本信息失败。"
|
||||
|
||||
msgid "New version found, but failed to get new version download url."
|
||||
msgstr "发现新版本,但未能获得新版本的下载地址。"
|
||||
|
||||
msgid "Download url is required."
|
||||
msgstr "请指定下载地址。"
|
||||
|
||||
msgid "File download failed or timed out: %s"
|
||||
msgstr "文件下载失败或超时:%s"
|
||||
|
||||
msgid "File path required."
|
||||
msgstr "请指定文件路径。"
|
||||
|
||||
msgid "Can't find client in file: %s"
|
||||
msgstr "无法在文件中找到客户端:%s"
|
||||
|
||||
msgid "Client file is required."
|
||||
msgstr "请指定客户端文件。"
|
||||
|
||||
msgid "The client file is not suitable for current device."
|
||||
msgstr "客户端文件不适合当前设备。"
|
||||
|
||||
msgid "Can't move new file to path: %s"
|
||||
msgstr "无法移动新文件到:%s"
|
@ -1 +0,0 @@
|
||||
zh-cn
|
20
luci-app-filebrowser/po/zh_Hans/filebrowser.po
Normal file
20
luci-app-filebrowser/po/zh_Hans/filebrowser.po
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-04-13 18:37+0000\n"
|
||||
"Last-Translator: try496 <pinghejk@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
|
||||
"openwrt/luciapplicationsfilebrowser/zh_Hans/>\n"
|
||||
"Language: zh_Hans\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.5-dev\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr "文件浏览器"
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr "授予文件浏览器访问权限"
|
14
luci-app-filebrowser/po/zh_Hant/filebrowser.po
Normal file
14
luci-app-filebrowser/po/zh_Hant/filebrowser.po
Normal file
@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: zh_Hant\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
|
||||
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
|
||||
msgid "File Browser"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
|
||||
msgid "Grant access to File Browser"
|
||||
msgstr ""
|
@ -1,7 +0,0 @@
|
||||
|
||||
config global
|
||||
option port '8088'
|
||||
option root_path '/'
|
||||
option project_directory '/tmp'
|
||||
option enable '0'
|
||||
|
@ -1,38 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
|
||||
|
||||
START=99
|
||||
|
||||
LOG_PATH="/var/log/filebrowser.log"
|
||||
|
||||
echolog() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S"): $1" >> $LOG_PATH
|
||||
}
|
||||
|
||||
config_t_get() {
|
||||
local index=0
|
||||
[ -n "$4" ] && index=$4
|
||||
local ret=$(uci get filebrowser.@$1[$index].$2 2>/dev/null)
|
||||
echo ${ret:=$3}
|
||||
}
|
||||
start() {
|
||||
ENABLED=$(config_t_get global enable 0)
|
||||
[ "$ENABLED" = "0" ] && return
|
||||
PORT=$(config_t_get global port 8088)
|
||||
ROOT_PATH=$(config_t_get global root_path /)
|
||||
project_directory=$(config_t_get global project_directory /tmp)
|
||||
[ ! -f "$project_directory/filebrowser" ] && echolog "在$project_directory找不到FileBrowser主程序,请下载。" && exit
|
||||
export HOME="/root"
|
||||
$project_directory/filebrowser -a 0.0.0.0 -p $PORT -r $ROOT_PATH -d ${ROOT_PATH}filebrowser.db -l $LOG_PATH >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
stop() {
|
||||
busybox ps -w | grep -v "grep" | grep "$project_directory/filebrowser -a 0.0.0.0" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 &
|
||||
rm -rf $LOG_PATH
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@filebrowser[-1]
|
||||
add ucitrack filebrowser
|
||||
set ucitrack.@filebrowser[-1].init=filebrowser
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/system/filebrowser": {
|
||||
"title": "File Browser",
|
||||
"order": 80,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "system/filebrowser"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-filebrowser" ]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
{
|
||||
"luci-app-filebrowser": {
|
||||
"description": "Grant UCI access for luci-app-filebrowser",
|
||||
"read": {
|
||||
"uci": [ "filebrowser" ]
|
||||
},
|
||||
"description": "Grant access to File Browser",
|
||||
"write": {
|
||||
"uci": [ "filebrowser" ]
|
||||
"cgi-io": [ "upload", "download" ],
|
||||
"ubus": {
|
||||
"file": [ "*" ]
|
||||
},
|
||||
"file": {
|
||||
"/*": [ "list", "read", "write" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
luci-app-gost/Makefile
Normal file
21
luci-app-gost/Makefile
Normal file
@ -0,0 +1,21 @@
|
||||
# Copyright (C) 2020 Openwrt.org
|
||||
#
|
||||
# This is a free software, use it under GNU General Public License v3.0.
|
||||
#
|
||||
# Created By ImmortalWrt
|
||||
# https://github.com/project-openwrt
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-gost
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=1
|
||||
LUCI_TITLE:=LuCI support for Gost
|
||||
LUCI_DEPENDS:=+gost
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_MAINTAINER:=ImmortalWrt
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
24
luci-app-gost/luasrc/controller/gost.lua
Normal file
24
luci-app-gost/luasrc/controller/gost.lua
Normal file
@ -0,0 +1,24 @@
|
||||
-- This is a free software, use it under GNU General Public License v3.0.
|
||||
-- Created By ImmortalWrt
|
||||
-- https://github.com/immortalwrt
|
||||
|
||||
module("luci.controller.gost", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/gost") then
|
||||
return
|
||||
end
|
||||
|
||||
local page
|
||||
page = entry({"admin", "services", "gost"}, cbi("gost"), _("Gost"), 100)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-gost" }
|
||||
entry({"admin", "services", "gost", "status"},call("act_status")).leaf=true
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e={}
|
||||
e.running=luci.sys.call("pgrep gost >/dev/null")==0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
20
luci-app-gost/luasrc/model/cbi/gost.lua
Normal file
20
luci-app-gost/luasrc/model/cbi/gost.lua
Normal file
@ -0,0 +1,20 @@
|
||||
-- Created By ImmortalWrt
|
||||
-- https://github.com/immortalwrt
|
||||
|
||||
mp = Map("gost", translate("Gost"))
|
||||
mp.description = translate("A simple security tunnel written in Golang.")
|
||||
|
||||
mp:section(SimpleSection).template = "gost/gost_status"
|
||||
|
||||
s = mp:section(TypedSection, "gost")
|
||||
s.anonymous=true
|
||||
s.addremove=false
|
||||
|
||||
enable = s:option(Flag, "enable", translate("Enable"))
|
||||
enable.default = 0
|
||||
enable.rmempty = false
|
||||
|
||||
run_command = s:option(Value, "run_command", translate("Command"))
|
||||
run_command.rmempty = false
|
||||
|
||||
return mp
|
22
luci-app-gost/luasrc/view/gost/gost_status.htm
Normal file
22
luci-app-gost/luasrc/view/gost/gost_status.htm
Normal file
@ -0,0 +1,22 @@
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[gost]], [[status]])%>', null,
|
||||
function(x, data) {
|
||||
var tb = document.getElementById('gost_status');
|
||||
if (data && tb) {
|
||||
if (data.running) {
|
||||
var links = '<em><b><font color=green>Gost <%:RUNNING%></font></b></em>';
|
||||
tb.innerHTML = links;
|
||||
} else {
|
||||
tb.innerHTML = '<em><b><font color=red>Gost <%:NOT RUNNING%></font></b></em>';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
|
||||
<fieldset class="cbi-section">
|
||||
<p id="gost_status">
|
||||
<em><%:Collecting data...%></em>
|
||||
</p>
|
||||
</fieldset>
|
1
luci-app-gost/po/zh-cn
Symbolic link
1
luci-app-gost/po/zh-cn
Symbolic link
@ -0,0 +1 @@
|
||||
zh_Hans
|
17
luci-app-gost/po/zh_Hans/gost.po
Normal file
17
luci-app-gost/po/zh_Hans/gost.po
Normal file
@ -0,0 +1,17 @@
|
||||
msgid "Gost"
|
||||
msgstr "Gost"
|
||||
|
||||
msgid "A simple security tunnel written in Golang."
|
||||
msgstr "GO语言实现的安全隧道。"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Command"
|
||||
msgstr "命令"
|
11
luci-app-gost/root/etc/uci-defaults/gost
Executable file
11
luci-app-gost/root/etc/uci-defaults/gost
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@gost[-1]
|
||||
add ucitrack gost
|
||||
set ucitrack.@gost[-1].init=gost
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
11
luci-app-gost/root/usr/share/rpcd/acl.d/luci-app-gost.json
Normal file
11
luci-app-gost/root/usr/share/rpcd/acl.d/luci-app-gost.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"luci-app-gost": {
|
||||
"description": "Grant UCI access for luci-app-gost",
|
||||
"read": {
|
||||
"uci": [ "gost" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "gost" ]
|
||||
}
|
||||
}
|
||||
}
|
26
luci-app-homeproxy/Makefile
Normal file
26
luci-app-homeproxy/Makefile
Normal file
@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+sing-box \
|
||||
+chinadns-ng \
|
||||
+firewall4 \
|
||||
+kmod-nft-tproxy
|
||||
|
||||
define Package/luci-app-homeproxy/conffiles
|
||||
/etc/config/homeproxy
|
||||
/etc/homeproxy/certs/
|
||||
/etc/homeproxy/ruleset/
|
||||
/etc/homeproxy/resources/direct_list.txt
|
||||
/etc/homeproxy/resources/proxy_list.txt
|
||||
/etc/homeproxy/cache.db
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
273
luci-app-homeproxy/htdocs/luci-static/resources/homeproxy.js
Normal file
273
luci-app-homeproxy/htdocs/luci-static/resources/homeproxy.js
Normal file
@ -0,0 +1,273 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
return baseclass.extend({
|
||||
dns_strategy: {
|
||||
'': _('Default'),
|
||||
'prefer_ipv4': _('Prefer IPv4'),
|
||||
'prefer_ipv6': _('Prefer IPv6'),
|
||||
'ipv4_only': _('IPv4 only'),
|
||||
'ipv6_only': _('IPv6 only')
|
||||
},
|
||||
|
||||
shadowsocks_encrypt_methods: [
|
||||
/* Stream */
|
||||
'none',
|
||||
/* AEAD */
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
/* AEAD 2022 */
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305'
|
||||
],
|
||||
|
||||
tls_cipher_suites: [
|
||||
'TLS_RSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_RSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_AES_128_GCM_SHA256',
|
||||
'TLS_AES_256_GCM_SHA384',
|
||||
'TLS_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256'
|
||||
],
|
||||
|
||||
tls_versions: [
|
||||
'1.0',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3'
|
||||
],
|
||||
|
||||
calcStringMD5: function(e) {
|
||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
||||
function h(a, b) {
|
||||
var c, d, e, f, g;
|
||||
e = a & 2147483648;
|
||||
f = b & 2147483648;
|
||||
c = a & 1073741824;
|
||||
d = b & 1073741824;
|
||||
g = (a & 1073741823) + (b & 1073741823);
|
||||
return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f;
|
||||
}
|
||||
function k(a, b, c, d, e, f, g) { a = h(a, h(h(b & c | ~b & d, e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function l(a, b, c, d, e, f, g) { a = h(a, h(h(b & d | c & ~d, e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function m(a, b, d, c, e, f, g) { a = h(a, h(h(b ^ d ^ c, e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function n(a, b, d, c, e, f, g) { a = h(a, h(h(d ^ (b | ~c), e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function p(a) {
|
||||
var b = '', d = '';
|
||||
for (var c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2);
|
||||
return b;
|
||||
}
|
||||
|
||||
var f = [], q, r, s, t, a, b, c, d;
|
||||
e = function(a) {
|
||||
a = a.replace(/\r\n/g, '\n');
|
||||
for (var b = '', d = 0; d < a.length; d++) {
|
||||
var c = a.charCodeAt(d);
|
||||
128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) :
|
||||
(b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)),
|
||||
b += String.fromCharCode(c & 63 | 128))
|
||||
}
|
||||
return b;
|
||||
}(e);
|
||||
f = function(b) {
|
||||
var c = b.length, a = c + 8;
|
||||
for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;)
|
||||
a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++;
|
||||
a = (g - g % 4) / 4; e[a] |= 128 << g % 4 * 8; e[d - 2] = c << 3; e[d - 1] = c >>> 29;
|
||||
return e;
|
||||
}(e);
|
||||
a = 1732584193;
|
||||
b = 4023233417;
|
||||
c = 2562383102;
|
||||
d = 271733878;
|
||||
|
||||
for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d,
|
||||
a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710),
|
||||
c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966),
|
||||
a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426),
|
||||
c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313),
|
||||
a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879),
|
||||
c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134),
|
||||
a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195),
|
||||
c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329),
|
||||
a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664),
|
||||
c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994),
|
||||
a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083),
|
||||
c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448),
|
||||
a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606),
|
||||
c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501),
|
||||
a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512),
|
||||
c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562),
|
||||
a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833),
|
||||
c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740),
|
||||
a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353),
|
||||
c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656),
|
||||
a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074),
|
||||
c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189),
|
||||
a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461),
|
||||
c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645),
|
||||
a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415),
|
||||
c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241),
|
||||
a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690),
|
||||
c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497),
|
||||
a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552),
|
||||
c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649),
|
||||
a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917),
|
||||
c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745),
|
||||
a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t);
|
||||
return (p(a) + p(b) + p(c) + p(d)).toLowerCase();
|
||||
},
|
||||
|
||||
decodeBase64Str: function(str) {
|
||||
if (!str)
|
||||
return null;
|
||||
|
||||
/* Thanks to luci-app-ssr-plus */
|
||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var padding = (4 - str.length % 4) % 4;
|
||||
if (padding)
|
||||
str = str + Array(padding + 1).join('=');
|
||||
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) =>
|
||||
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
||||
).join(''));
|
||||
},
|
||||
|
||||
getBuiltinFeatures: function() {
|
||||
var callGetSingBoxFeatures = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'singbox_get_features',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.resolveDefault(callGetSingBoxFeatures(), {});
|
||||
},
|
||||
|
||||
generateUUIDv4: function() {
|
||||
/* Thanks to https://stackoverflow.com/a/2117523 */
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
},
|
||||
|
||||
loadDefaultLabel: function(uciconfig, ucisection) {
|
||||
var label = uci.get(uciconfig, ucisection, 'label');
|
||||
if (label) {
|
||||
return label;
|
||||
} else {
|
||||
uci.set(uciconfig, ucisection, 'label', ucisection);
|
||||
return ucisection;
|
||||
}
|
||||
},
|
||||
|
||||
loadModalTitle: function(title, addtitle, uciconfig, ucisection) {
|
||||
var label = uci.get(uciconfig, ucisection, 'label');
|
||||
return label ? title + ' » ' + label : addtitle;
|
||||
},
|
||||
|
||||
renderSectionAdd: function(section, extra_class) {
|
||||
var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
var button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
var uciconfig = section.uciconfig || section.map.config;
|
||||
|
||||
if (!v) {
|
||||
button.disabled = true;
|
||||
return true;
|
||||
} else if (uci.get(uciconfig, v)) {
|
||||
button.disabled = true;
|
||||
return _('Expecting: %s').format(_('unique UCI identifier'));
|
||||
} else {
|
||||
button.disabled = null;
|
||||
return true;
|
||||
}
|
||||
}, 'blur', 'keyup');
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
uploadCertificate: function(option, type, filename, ev) {
|
||||
var callWriteCertificate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'certificate_write',
|
||||
params: ['filename'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target)
|
||||
.then(L.bind((btn, res) => {
|
||||
return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
|
||||
if (ret.result === true)
|
||||
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error)));
|
||||
});
|
||||
}, this, ev.target))
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
validateBase64Key: function(length, section_id, value) {
|
||||
/* Thanks to luci-proto-wireguard */
|
||||
if (section_id && value)
|
||||
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
|
||||
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateUniqueValue: function(uciconfig, ucisection, ucioption, section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
|
||||
var duplicate = false;
|
||||
uci.sections(uciconfig, ucisection, (res) => {
|
||||
if (res['.name'] !== section_id)
|
||||
if (res[ucioption] === value)
|
||||
duplicate = true
|
||||
});
|
||||
if (duplicate)
|
||||
return _('Expecting: %s').format(_('unique value'));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateUUID: function(section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
|
||||
return _('Expecting: %s').format(_('valid uuid'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,740 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
|
||||
var isRunning = false;
|
||||
try {
|
||||
isRunning = res['homeproxy']['instances']['sing-box-s']['running'];
|
||||
} catch (e) { }
|
||||
return isRunning;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning) {
|
||||
var spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
var renderHTML;
|
||||
if (isRunning)
|
||||
renderHTML = spanTemp.format('green', _('HomeProxy Server'), _('RUNNING'));
|
||||
else
|
||||
renderHTML = spanTemp.format('red', _('HomeProxy Server'), _('NOT RUNNING'));
|
||||
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('homeproxy'),
|
||||
hp.getBuiltinFeatures()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o;
|
||||
var features = data[1];
|
||||
|
||||
m = new form.Map('homeproxy', _('HomeProxy Server'),
|
||||
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.render = function () {
|
||||
poll.add(function () {
|
||||
return L.resolveDefault(getServiceStatus()).then((res) => {
|
||||
var view = document.getElementById('service_status');
|
||||
view.innerHTML = renderStatus(res);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings'));
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'auto_firewall', _('Auto configure firewall'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
s = m.section(form.GridSection, 'server', _('Server settings'));
|
||||
s.addremove = true;
|
||||
s.rowcolors = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Server'), _('Add a server'), data[0]);
|
||||
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
s.renderSectionAdd = L.bind(hp.renderSectionAdd, this, s);
|
||||
|
||||
o = s.option(form.Value, 'label', _('Label'));
|
||||
o.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'label');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.ListValue, 'type', _('Type'));
|
||||
o.value('http', _('HTTP'));
|
||||
if (features.with_quic) {
|
||||
o.value('hysteria', _('Hysteria'));
|
||||
o.value('hysteria2', _('Hysteria2'));
|
||||
o.value('naive', _('NaïveProxy'));
|
||||
}
|
||||
o.value('shadowsocks', _('Shadowsocks'));
|
||||
o.value('socks', _('Socks'));
|
||||
o.value('trojan', _('Trojan'));
|
||||
if (features.with_quic)
|
||||
o.value('tuic', _('Tuic'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Listen address'));
|
||||
o.placeholder = '::';
|
||||
o.datatype = 'ipaddr';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Listen port'),
|
||||
_('The port must be unique.'));
|
||||
o.datatype = 'port';
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'port');
|
||||
|
||||
o = s.option(form.Value, 'username', _('Username'));
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'socks');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.depends({'type': /^(http|naive|socks)$/, 'username': /[\s\S]/});
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'tuic');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
|
||||
var required_type = [ 'http', 'naive', 'socks', 'shadowsocks' ];
|
||||
|
||||
if (required_type.includes(type)) {
|
||||
if (type === 'shadowsocks') {
|
||||
var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id);
|
||||
if (encmode === 'none')
|
||||
return true;
|
||||
else if (encmode === '2022-blake3-aes-128-gcm')
|
||||
return hp.validateBase64Key(24, section_id, value);
|
||||
else if (['2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305'].includes(encmode))
|
||||
return hp.validateBase64Key(44, section_id, value);
|
||||
}
|
||||
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* Hysteria (2) config start */
|
||||
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
||||
o.value('udp');
|
||||
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
|
||||
o.value('wechat-video');
|
||||
o.value('faketcp');
|
||||
*/
|
||||
o.default = 'udp';
|
||||
o.depends('type', 'hysteria');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'),
|
||||
_('Max download speed in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'),
|
||||
_('Max upload speed in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('base64', _('Base64'));
|
||||
o.value('string', _('String'));
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload'));
|
||||
o.depends({'type': 'hysteria', 'hysteria_auth_type': /[\s\S]/});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('salamander', _('Salamander'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_obfs_password', _('Obfuscate password'));
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'),
|
||||
_('The QUIC stream-level flow control window for receiving data.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '67108864';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_recv_window_client', _('QUIC connection receive window'),
|
||||
_('The QUIC connection-level flow control window for receiving data.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '15728640';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_max_conn_client', _('QUIC maximum concurrent bidirectional streams'),
|
||||
_('The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1024';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_disable_mtu_discovery', _('Disable Path MTU discovery'),
|
||||
_('Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
|
||||
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'),
|
||||
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
/* Hysteria (2) config end */
|
||||
|
||||
/* Shadowsocks config */
|
||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||
for (var i of hp.shadowsocks_encrypt_methods)
|
||||
o.value(i);
|
||||
o.default = 'aes-128-gcm';
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
|
||||
/* Tuic config start */
|
||||
o = s.option(form.Value, 'uuid', _('UUID'));
|
||||
o.depends('type', 'tuic');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.validate = hp.validateUUID;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tuic_congestion_control', _('Congestion control algorithm'),
|
||||
_('QUIC congestion control algorithm.'));
|
||||
o.value('cubic');
|
||||
o.value('new_reno');
|
||||
o.value('bbr');
|
||||
o.default = 'cubic';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tuic_auth_timeout', _('Auth timeout'),
|
||||
_('How long the server should wait for the client to send the authentication command (in seconds).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tuic_enable_zero_rtt', _('Enable 0-RTT handshake'),
|
||||
_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
|
||||
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tuic_heartbeat', _('Heartbeat interval'),
|
||||
_('Interval for sending heartbeat packets for keeping the connection alive (in seconds).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '10';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
/* Tuic config end */
|
||||
|
||||
/* VLESS / VMess config start */
|
||||
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
||||
o.value('', _('None'));
|
||||
o.value('xtls-rprx-vision');
|
||||
o.depends('type', 'vless');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'vmess_alterid', _('Alter ID'),
|
||||
_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
/* VMess config end */
|
||||
|
||||
/* Transport config start */
|
||||
o = s.option(form.ListValue, 'transport', _('Transport'),
|
||||
_('No TCP transport, plain HTTP is merged into the HTTP transport.'));
|
||||
o.value('', _('None'));
|
||||
o.value('grpc', _('gRPC'));
|
||||
o.value('http', _('HTTP'));
|
||||
o.value('httpupgrade', _('HTTPUpgrade'));
|
||||
o.value('quic', _('QUIC'));
|
||||
o.value('ws', _('WebSocket'));
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
var desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
|
||||
if (value === 'http')
|
||||
desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
|
||||
else if (value === 'quic')
|
||||
desc.innerHTML = _('No additional encryption support: It\'s basically duplicate encryption.');
|
||||
else
|
||||
desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
|
||||
|
||||
var tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
if ((value === 'http' && tls_element.checked) || (value === 'grpc' && !features.with_grpc))
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
|
||||
else if (value === 'grpc' && features.with_grpc)
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
_('If the transport doesn\'t see any activity after a duration of this time (in seconds), it pings the client to check if the connection is still active.');
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config start */
|
||||
o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config end */
|
||||
|
||||
/* HTTP(Upgrade) config start */
|
||||
o = s.option(form.DynamicList, 'http_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'httpupgrade_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_path', _('Path'));
|
||||
o.depends('transport', 'http');
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_method', _('Method'));
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
|
||||
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.depends({'transport': 'http', 'tls': '1'});
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.with_grpc) {
|
||||
o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
|
||||
_('The timeout (in seconds) that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
}
|
||||
/* HTTP config end */
|
||||
|
||||
/* WebSocket config start */
|
||||
o = s.option(form.Value, 'ws_host', _('Host'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'ws_path', _('Path'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'websocket_early_data', _('Early data'),
|
||||
_('Allowed payload size is in the request.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.value('2048');
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'websocket_early_data_header', _('Early data header name'),
|
||||
_('Early data is sent in path instead of header by default.') +
|
||||
'<br/>' +
|
||||
_('To be compatible with Xray-core, set this to <code>Sec-WebSocket-Protocol</code>.'));
|
||||
o.value('Sec-WebSocket-Protocol');
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
/* WebSocket config end */
|
||||
|
||||
/* Transport config end */
|
||||
|
||||
/* Mux config start */
|
||||
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
||||
o.default = o.disabled;
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'multiplex_padding', _('Enable padding'));
|
||||
o.default = o.disabled;
|
||||
o.depends('multiplex', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.hp_has_tcp_brutal) {
|
||||
o = s.option(form.Flag, 'multiplex_brutal', _('Enable TCP Brutal'),
|
||||
_('Enable TCP Brutal congestion control algorithm'));
|
||||
o.default = o.disabled;
|
||||
o.depends('multiplex', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'multiplex_brutal_down', _('Download bandwidth'),
|
||||
_('Download bandwidth in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('multiplex_brutal', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'multiplex_brutal_up', _('Upload bandwidth'),
|
||||
_('Upload bandwidth in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('multiplex_brutal', '1');
|
||||
o.modalonly = true;
|
||||
}
|
||||
/* Mux config end */
|
||||
|
||||
/* TLS config start */
|
||||
o = s.option(form.Flag, 'tls', _('TLS'));
|
||||
o.default = o.disabled;
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
|
||||
var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
|
||||
if (['hysteria', 'hysteria2', 'tuic'].includes(type)) {
|
||||
tls.checked = true;
|
||||
tls.disabled = true;
|
||||
} else {
|
||||
tls.disabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_sni', _('TLS SNI'),
|
||||
_('Used to verify the hostname on the returned certificates unless insecure is given.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'),
|
||||
_('List of supported application level protocols, in order of preference.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
|
||||
_('The minimum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (var i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
|
||||
_('The maximum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (var i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.MultiValue, 'tls_cipher_suites', _('Cipher suites'),
|
||||
_('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
|
||||
for (var i of hp.tls_cipher_suites)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.optional = true;
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.with_acme) {
|
||||
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
|
||||
_('Use ACME TLS certificate issuer.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'),
|
||||
_('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_email', _('Email'),
|
||||
_('The email address to use when creating or selecting an existing ACME server account.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format('non-empty value');
|
||||
else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
|
||||
return _('Expecting: %s').format('valid email address');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_provider', _('CA provider'),
|
||||
_('The ACME CA provider to use.'));
|
||||
o.value('letsencrypt', _('Let\'s Encrypt'));
|
||||
o.value('zerossl', _('ZeroSSL'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_dns01_challenge', _('DNS01 challenge'))
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
||||
o.value('alidns', _('Alibaba Cloud DNS'));
|
||||
o.value('cloudflare', _('Cloudflare'));
|
||||
o.depends('tls_dns01_challenge', '1');
|
||||
o.default = 'cloudflare';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID'));
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret'));
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID'));
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
||||
o.depends('tls_dns01_provider', 'cloudflare');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'),
|
||||
_('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'),
|
||||
_('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'),
|
||||
_('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' +
|
||||
'<br/>External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key'));
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
if (features.with_reality_server) {
|
||||
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'type': 'vless'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'type': 'vless'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
|
||||
_('The maximum time difference between the server and the client.'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
|
||||
_('The server public key, in PEM format.'));
|
||||
o.value('/etc/homeproxy/certs/server_publickey.pem');
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
|
||||
_('<strong>Save your configuration before uploading files!</strong>'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Upload...');
|
||||
o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy/certs/server_publickey.pem'});
|
||||
o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'server_publickey');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_key_path', _('Key path'),
|
||||
_('The server private key, in PEM format.'));
|
||||
o.value('/etc/homeproxy/certs/server_privatekey.pem');
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Button, '_upload_key', _('Upload key'),
|
||||
_('<strong>Save your configuration before uploading files!</strong>'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Upload...');
|
||||
o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy/certs/server_privatekey.pem'});
|
||||
o.onclick = L.bind(hp.uploadCertificate, this, _('private key'), 'server_privatekey');
|
||||
o.modalonly = true;
|
||||
/* TLS config end */
|
||||
|
||||
/* Extra settings start */
|
||||
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
|
||||
_('Enable tcp fast open for listener.'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
|
||||
_('Enable UDP fragmentation.'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'network': 'tcp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'sniff_override', _('Override destination'),
|
||||
_('Override the connection destination address with the sniffed domain.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
|
||||
_('If set, the requested domain name will be resolved to IP before routing.'));
|
||||
for (var i in hp.dns_strategy)
|
||||
o.value(i, hp.dns_strategy[i])
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'network', _('Network'));
|
||||
o.value('tcp', _('TCP'));
|
||||
o.value('udp', _('UDP'));
|
||||
o.value('', _('Both'));
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
/* Extra settings end */
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
@ -0,0 +1,237 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
var css = ' \
|
||||
#log_textarea { \
|
||||
padding: 10px; \
|
||||
text-align: left; \
|
||||
} \
|
||||
#log_textarea pre { \
|
||||
padding: .5rem; \
|
||||
word-break: break-all; \
|
||||
margin: 0; \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
}';
|
||||
|
||||
var hp_dir = '/var/run/homeproxy';
|
||||
|
||||
function getConnStat(self, site) {
|
||||
var callConnStat = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
self.default = E('div', { 'style': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
|
||||
var ele = self.default.firstElementChild.nextElementSibling;
|
||||
if (ret.result) {
|
||||
ele.style.setProperty('color', 'green');
|
||||
ele.innerHTML = _('passed');
|
||||
} else {
|
||||
ele.style.setProperty('color', 'red');
|
||||
ele.innerHTML = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
}, [ _('Check') ]),
|
||||
' ',
|
||||
E('strong', { 'style': 'color:gray' }, _('unchecked')),
|
||||
]);
|
||||
}
|
||||
|
||||
function getResVersion(self, type) {
|
||||
var callResVersion = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'resources_get_version',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callResUpdate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'resources_update',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.resolveDefault(callResVersion(type), {}).then((res) => {
|
||||
var spanTemp = E('div', { 'style': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return L.resolveDefault(callResUpdate(type), {}).then((res) => {
|
||||
switch (res.status) {
|
||||
case 0:
|
||||
self.description = _('Successfully updated.');
|
||||
break;
|
||||
case 1:
|
||||
self.description = _('Update failed.');
|
||||
break;
|
||||
case 2:
|
||||
self.description = _('Already in updating.');
|
||||
break;
|
||||
case 3:
|
||||
self.description = _('Already at the latest version.');
|
||||
break;
|
||||
default:
|
||||
self.description = _('Unknown error.');
|
||||
break;
|
||||
}
|
||||
|
||||
return self.map.reset();
|
||||
});
|
||||
})
|
||||
}, [ _('Check update') ]),
|
||||
' ',
|
||||
E('strong', { 'style': (res.error ? 'color:red' : 'color:green') },
|
||||
[ res.error ? 'not found' : res.version ]
|
||||
),
|
||||
]);
|
||||
|
||||
self.default = spanTemp;
|
||||
});
|
||||
}
|
||||
|
||||
function getRuntimeLog(name, filename) {
|
||||
var callLogClean = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'log_clean',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var log_textarea = E('div', { 'id': 'log_textarea' },
|
||||
E('img', {
|
||||
'src': L.resource(['icons/loading.gif']),
|
||||
'alt': _('Loading'),
|
||||
'style': 'vertical-align:middle'
|
||||
}, _('Collecting data...'))
|
||||
);
|
||||
|
||||
var log;
|
||||
poll.add(L.bind(function() {
|
||||
return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text')
|
||||
.then(function(res) {
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
res.trim() || _('Log is empty.')
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
}).catch(function(err) {
|
||||
if (err.toString().includes('NotFoundError'))
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Log file does not exist.')
|
||||
]);
|
||||
else
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Unknown error: %s').format(err)
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
});
|
||||
}));
|
||||
|
||||
return E([
|
||||
E('style', [ css ]),
|
||||
E('div', {'class': 'cbi-map'}, [
|
||||
E('h3', {'name': 'content'}, [
|
||||
_('%s log').format(name),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return L.resolveDefault(callLogClean(filename), {});
|
||||
})
|
||||
}, [ _('Clean log') ])
|
||||
]),
|
||||
E('div', {'class': 'cbi-section'}, [
|
||||
log_textarea,
|
||||
E('div', {'style': 'text-align:right'},
|
||||
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('homeproxy')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o;
|
||||
var routing_mode = uci.get(data[0], 'config', 'routing_mode') || 'bypass_mainland_china';
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
|
||||
o.cfgvalue = function() { return getConnStat(this, 'baidu') };
|
||||
|
||||
o = s.option(form.DummyValue, '_check_google', _('Google'));
|
||||
o.cfgvalue = function() { return getConnStat(this, 'google') };
|
||||
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'china_ip4') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'china_ip6') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_china_list_version', _('China list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'china_list') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'gfw_list') };
|
||||
o.rawhtml = true;
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_homeproxy_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, _('HomeProxy'), 'homeproxy');
|
||||
|
||||
o = s.option(form.DummyValue, '_sing-box-c_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, _('sing-box client'), 'sing-box-c');
|
||||
|
||||
o = s.option(form.DummyValue, '_sing-box-s_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, _('sing-box server'), 'sing-box-s');
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
2527
luci-app-homeproxy/po/templates/homeproxy.pot
Normal file
2527
luci-app-homeproxy/po/templates/homeproxy.pot
Normal file
File diff suppressed because it is too large
Load Diff
1
luci-app-homeproxy/po/zh-cn
Symbolic link
1
luci-app-homeproxy/po/zh-cn
Symbolic link
@ -0,0 +1 @@
|
||||
zh_Hans
|
2628
luci-app-homeproxy/po/zh_Hans/homeproxy.po
Normal file
2628
luci-app-homeproxy/po/zh_Hans/homeproxy.po
Normal file
File diff suppressed because it is too large
Load Diff
32
luci-app-homeproxy/root/etc/capabilities/homeproxy.json
Normal file
32
luci-app-homeproxy/root/etc/capabilities/homeproxy.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"bounding": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"effective": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"ambient": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"permitted": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"inheritable": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
]
|
||||
}
|
59
luci-app-homeproxy/root/etc/config/homeproxy
Normal file
59
luci-app-homeproxy/root/etc/config/homeproxy
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
config homeproxy 'infra'
|
||||
option __warning 'DO NOT EDIT THIS SECTION, OR YOU ARE ON YOUR OWN!'
|
||||
option common_port '22,53,80,143,443,465,853,873,993,995,8080,8443,9418'
|
||||
option mixed_port '5330'
|
||||
option redirect_port '5331'
|
||||
option tproxy_port '5332'
|
||||
option dns_port '5333'
|
||||
option china_dns_port '5334'
|
||||
option tun_name 'singtun0'
|
||||
option tun_addr4 '172.19.0.1/30'
|
||||
option tun_addr6 'fdfe:dcba:9876::1/126'
|
||||
option tun_mtu '9000'
|
||||
option table_mark '100'
|
||||
option self_mark '100'
|
||||
option tproxy_mark '101'
|
||||
option tun_mark '102'
|
||||
|
||||
config homeproxy 'config'
|
||||
option main_node 'nil'
|
||||
option main_udp_node 'same'
|
||||
option dns_server '8.8.8.8'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option routing_port 'common'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
|
||||
config homeproxy 'control'
|
||||
option lan_proxy_mode 'disabled'
|
||||
list wan_proxy_ipv4_ips '91.108.4.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.8.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.12.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.56.0/22'
|
||||
list wan_proxy_ipv4_ips '95.161.64.0/20'
|
||||
list wan_proxy_ipv4_ips '149.154.160.0/22'
|
||||
list wan_proxy_ipv4_ips '149.154.164.0/22'
|
||||
list wan_proxy_ipv4_ips '149.154.172.0/22'
|
||||
|
||||
config homeproxy 'routing'
|
||||
option sniff_override '1'
|
||||
option default_outbound 'direct-out'
|
||||
|
||||
config homeproxy 'dns'
|
||||
option dns_strategy 'prefer_ipv4'
|
||||
option default_server 'local-dns'
|
||||
option disable_cache '0'
|
||||
option disable_cache_expire '0'
|
||||
|
||||
config homeproxy 'subscription'
|
||||
option auto_update '0'
|
||||
option allow_insecure '0'
|
||||
option packet_encoding 'xudp'
|
||||
option update_via_proxy '0'
|
||||
option filter_nodes 'disabled'
|
||||
|
||||
config homeproxy 'server'
|
||||
option enabled '0'
|
||||
option auto_firewall '0'
|
||||
|
3543
luci-app-homeproxy/root/etc/homeproxy/resources/china_ip4.txt
Normal file
3543
luci-app-homeproxy/root/etc/homeproxy/resources/china_ip4.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
20240708150024
|
1699
luci-app-homeproxy/root/etc/homeproxy/resources/china_ip6.txt
Normal file
1699
luci-app-homeproxy/root/etc/homeproxy/resources/china_ip6.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
20240708150024
|
80780
luci-app-homeproxy/root/etc/homeproxy/resources/china_list.txt
Normal file
80780
luci-app-homeproxy/root/etc/homeproxy/resources/china_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
202407102210
|
6185
luci-app-homeproxy/root/etc/homeproxy/resources/gfw_list.txt
Normal file
6185
luci-app-homeproxy/root/etc/homeproxy/resources/gfw_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
||||
202407102210
|
19
luci-app-homeproxy/root/etc/homeproxy/scripts/clean_log.sh
Executable file
19
luci-app-homeproxy/root/etc/homeproxy/scripts/clean_log.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
log_max_size="10" #KB
|
||||
main_log_file="/var/run/$NAME/$NAME.log"
|
||||
singc_log_file="/var/run/$NAME/sing-box-c.log"
|
||||
sings_log_file="/var/run/$NAME/sing-box-s.log"
|
||||
|
||||
while true; do
|
||||
sleep 180
|
||||
for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do
|
||||
[ -s "$i" ] || continue
|
||||
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
|
||||
done
|
||||
done
|
632
luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_post.ut
Executable file
632
luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_post.ut
Executable file
@ -0,0 +1,632 @@
|
||||
#!/usr/bin/utpl
|
||||
|
||||
{%-
|
||||
'use strict';
|
||||
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
|
||||
|
||||
const fw4 = require('fw4');
|
||||
|
||||
function array_to_nftarr(array) {
|
||||
if (type(array) !== 'array')
|
||||
return null;
|
||||
|
||||
return `{ ${join(', ', uniq(array))} }`;
|
||||
}
|
||||
|
||||
function resolve_ipv6(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
let ipv6 = fw4.parse_subnet(str)?.[0];
|
||||
if (!ipv6 || ipv6.family !== 6)
|
||||
return null;
|
||||
|
||||
if (ipv6.bits > -1)
|
||||
return `${ipv6.addr}/${ipv6.bits}`;
|
||||
else
|
||||
return `& ${ipv6.mask} == ${ipv6.addr}`;
|
||||
}
|
||||
|
||||
/* Misc config */
|
||||
const resources_dir = '/etc/homeproxy/resources';
|
||||
|
||||
/* UCI config start */
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
|
||||
let outbound_node, outbound_udp_node, china_dns_server, bypass_cn_traffic;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
|
||||
china_dns_server = uci.get(cfgname, 'config', 'china_dns_server');
|
||||
} else {
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0';
|
||||
}
|
||||
|
||||
let routing_port = uci.get(cfgname, 'config', 'routing_port') || 'common';
|
||||
if (routing_port === 'common')
|
||||
routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418';
|
||||
|
||||
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
|
||||
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
|
||||
|
||||
let self_mark, redirect_port,
|
||||
tproxy_port, tproxy_mark,
|
||||
tun_name, tun_mark;
|
||||
|
||||
if (match(proxy_mode, /redirect/)) {
|
||||
self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100';
|
||||
redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331';
|
||||
}
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
if (outbound_udp_node !== 'nil' || routing_mode === 'custom') {
|
||||
tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332';
|
||||
tproxy_mark = uci.get(cfgname, 'infra', 'tproxy_mark') || '101';
|
||||
}
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
tun_mark = uci.get(cfgname, 'infra', 'tun_mark') || '102';
|
||||
}
|
||||
|
||||
const control_options = [
|
||||
"listen_interfaces", "lan_proxy_mode",
|
||||
"lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips",
|
||||
"lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips",
|
||||
"lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips",
|
||||
"lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips",
|
||||
"wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips",
|
||||
"wan_direct_ipv4_ips", "wan_direct_ipv6_ips"
|
||||
];
|
||||
const control_info = {};
|
||||
|
||||
for (let i in control_options)
|
||||
control_info[i] = uci.get(cfgname, 'control', i);
|
||||
/* UCI config end */
|
||||
-%}
|
||||
|
||||
{# Reserved addresses -#}
|
||||
set homeproxy_local_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
0.0.0.0/8,
|
||||
10.0.0.0/8,
|
||||
100.64.0.0/10,
|
||||
127.0.0.0/8,
|
||||
169.254.0.0/16,
|
||||
172.16.0.0/12,
|
||||
192.0.0.0/24,
|
||||
192.0.2.0/24,
|
||||
192.31.196.0/24,
|
||||
192.52.193.0/24,
|
||||
192.88.99.0/24,
|
||||
192.168.0.0/16,
|
||||
192.175.48.0/24,
|
||||
198.18.0.0/15,
|
||||
198.51.100.0/24,
|
||||
203.0.113.0/24,
|
||||
224.0.0.0/4,
|
||||
240.0.0.0/4
|
||||
}
|
||||
}
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_local_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
::/128,
|
||||
::1/128,
|
||||
::ffff:0:0/96,
|
||||
100::/64,
|
||||
64:ff9b::/96,
|
||||
2001::/32,
|
||||
2001:10::/28,
|
||||
2001:20::/28,
|
||||
2001:db8::/28,
|
||||
2002::/16,
|
||||
fc00::/7,
|
||||
fe80::/10,
|
||||
ff00::/8
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
set homeproxy_gfw_list_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_gfw_list_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %}
|
||||
set homeproxy_mainland_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %}
|
||||
{{ cnip4 }},
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% if ((ipv6_support === '1') || china_dns_server): %}
|
||||
set homeproxy_mainland_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %}
|
||||
{{ cnip6 }},
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{# WAN ACL addresses #}
|
||||
set homeproxy_wan_proxy_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_proxy_ipv4_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} }
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_wan_proxy_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_proxy_ipv6_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} }
|
||||
{% endif /* wan_proxy_ipv6_ips*/ %}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
set homeproxy_wan_direct_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_direct_ipv4_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} }
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_wan_direct_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_direct_ipv6_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} }
|
||||
{% endif /* wan_direct_ipv6_ips */ %}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% if (routing_port !== 'all'): %}
|
||||
set homeproxy_routing_port {
|
||||
type inet_service
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = { {{ join(', ', split(routing_port, ',')) }} }
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# TCP redirect #}
|
||||
{% if (match(proxy_mode, /redirect/)): %}
|
||||
chain homeproxy_redirect_proxy {
|
||||
meta l4proto tcp counter redirect to :{{ redirect_port }}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect_proxy_port {
|
||||
{% if (routing_port !== 'all'): %}
|
||||
tcp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
goto homeproxy_redirect_proxy
|
||||
}
|
||||
|
||||
chain homeproxy_redirect_lanac {
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
|
||||
{% endif %}
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_redirect
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_redirect_proxy_port
|
||||
}
|
||||
|
||||
chain homeproxy_output_redir {
|
||||
type nat hook output priority filter -105; policy accept
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
|
||||
}
|
||||
|
||||
chain dstnat {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# UDP tproxy #}
|
||||
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %}
|
||||
chain homeproxy_mangle_tproxy {
|
||||
meta l4proto udp mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
|
||||
{% if (ipv6_support === '1'): %}
|
||||
meta l4proto udp mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tproxy_port {
|
||||
{% if (routing_port !== 'all'): %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
goto homeproxy_mangle_tproxy
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_mark {
|
||||
{% if (routing_port !== 'all'): %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
meta l4proto udp mark set {{ tproxy_mark }} counter accept
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_lanac {
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(split(join(' ', control_info.listen_interfaces) + ' lo', ' ')) }} counter return
|
||||
{% endif %}
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_mangle_prerouting
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_prerouting {
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% endif /* routing_mode */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_mangle_tproxy_port
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_output {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
counter goto homeproxy_mangle_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# TUN #}
|
||||
{% if (match(proxy_mode, /tun/)): %}
|
||||
chain homeproxy_mangle_lanac {
|
||||
iifname {{ tun_name }} counter return
|
||||
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_mangle_tun
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tun_mark {
|
||||
{% if (routing_port !== 'all'): %}
|
||||
{% if (proxy_mode === 'tun'): %}
|
||||
tcp dport != @homeproxy_routing_port counter return
|
||||
{% endif /* proxy_mode */ %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif /* routing_port */ %}
|
||||
|
||||
counter mark set {{ tun_mark }}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tun {
|
||||
iifname {{ tun_name }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (control_info.wan_direct_ipv4_ips): %}
|
||||
ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* wan_direct_ipv4_ips */ %}
|
||||
{% if (control_info.wan_direct_ipv6_ips): %}
|
||||
ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return
|
||||
{% endif /* wan_direct_ipv6_ips */ %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% endif /* routing_mode */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter mark set {{ tun_mark }}
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter mark set {{ tun_mark }}
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter mark set {{ tun_mark }}
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_mangle_tun_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
|
||||
}
|
||||
{% endif %}
|
54
luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_pre.ut
Executable file
54
luci-app-homeproxy/root/etc/homeproxy/scripts/firewall_pre.ut
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/utpl -S
|
||||
|
||||
{%-
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china',
|
||||
proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy';
|
||||
|
||||
let outbound_node, tun_name;
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
if (routing_mode === 'custom')
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
else
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
|
||||
if (outbound_node !== 'nil')
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
}
|
||||
|
||||
const server_enabled = uci.get(cfgname, 'server', 'enabled');
|
||||
let auto_firewall = '0';
|
||||
if (server_enabled === '1')
|
||||
auto_firewall = uci.get(cfgname, 'server', 'auto_firewall') || '0';
|
||||
|
||||
-%}
|
||||
|
||||
{% if (tun_name): %}
|
||||
chain forward {
|
||||
oifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun forward"
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if (tun_name || auto_firewall === '1'): %}
|
||||
chain input {
|
||||
{% if (tun_name): %}
|
||||
iifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun input"
|
||||
{% endif %}
|
||||
{%
|
||||
if (auto_firewall === '1')
|
||||
uci.foreach(cfgname, 'server', (s) => {
|
||||
if (s.enabled !== '1')
|
||||
return;
|
||||
|
||||
let proto = s.network || '{ tcp, udp }';
|
||||
printf(' meta l4proto %s th dport %s counter accept comment "!%s: accept server %s"\n',
|
||||
proto, s.port, cfgname, s['.name']);
|
||||
});
|
||||
%}
|
||||
}
|
||||
{% endif %}
|
661
luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
Executable file
661
luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
Executable file
@ -0,0 +1,661 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
import { isnan } from 'math';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import {
|
||||
executeCommand, isEmpty, strToBool, strToInt,
|
||||
removeBlankAttrs, validateHostname, validation,
|
||||
HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimain = 'config',
|
||||
uciexp = 'experimental',
|
||||
ucicontrol = 'control';
|
||||
|
||||
const ucidnssetting = 'dns',
|
||||
ucidnsserver = 'dns_server',
|
||||
ucidnsrule = 'dns_rule';
|
||||
|
||||
const uciroutingsetting = 'routing',
|
||||
uciroutingnode = 'routing_node',
|
||||
uciroutingrule = 'routing_rule';
|
||||
|
||||
const ucinode = 'node';
|
||||
const uciruleset = 'ruleset';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
||||
|
||||
let wan_dns = executeCommand('ifstatus wan | jsonfilter -e \'@["dns-server"][0]\'');
|
||||
if (wan_dns.exitcode === 0 && trim(wan_dns.stdout))
|
||||
wan_dns = trim(wan_dns.stdout);
|
||||
else
|
||||
wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '208.67.222.222' : '114.114.114.114';
|
||||
|
||||
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
|
||||
|
||||
let main_node, main_udp_node, dedicated_udp_node, default_outbound, sniff_override = '1',
|
||||
dns_server, dns_default_strategy, dns_default_server, dns_disable_cache, dns_disable_cache_expire,
|
||||
dns_independent_cache, dns_client_subnet, direct_domain_list;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||
dedicated_udp_node = !isEmpty(main_udp_node) && !(main_udp_node in ['same', main_node]);
|
||||
|
||||
dns_server = uci.get(uciconfig, ucimain, 'dns_server');
|
||||
if (isEmpty(dns_server) || dns_server === 'wan')
|
||||
dns_server = wan_dns;
|
||||
|
||||
direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt'));
|
||||
if (direct_domain_list)
|
||||
direct_domain_list = split(direct_domain_list, /[\r\n]/);
|
||||
} else {
|
||||
/* DNS settings */
|
||||
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
|
||||
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
|
||||
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
|
||||
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
|
||||
dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache');
|
||||
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
|
||||
|
||||
/* Routing settings */
|
||||
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
||||
sniff_override = uci.get(uciconfig, uciroutingsetting, 'sniff_override');
|
||||
}
|
||||
|
||||
const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy',
|
||||
ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0',
|
||||
default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface');
|
||||
|
||||
const cache_file_store_rdrc = uci.get(uciconfig, uciexp, 'cache_file_store_rdrc'),
|
||||
cache_file_rdrc_timeout = uci.get(uciconfig, uciexp, 'cache_file_rdrc_timeout');
|
||||
|
||||
const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330';
|
||||
let self_mark, redirect_port, tproxy_port,
|
||||
tun_name, tun_addr4, tun_addr6, tun_mtu, tun_gso,
|
||||
tcpip_stack, endpoint_independent_nat;
|
||||
if (match(proxy_mode, /redirect/)) {
|
||||
self_mark = uci.get(uciconfig, 'infra', 'self_mark') || '100';
|
||||
redirect_port = uci.get(uciconfig, 'infra', 'redirect_port') || '5331';
|
||||
}
|
||||
if (match(proxy_mode), /tproxy/)
|
||||
if (main_udp_node !== 'nil' || routing_mode === 'custom')
|
||||
tproxy_port = uci.get(uciconfig, 'infra', 'tproxy_port') || '5332';
|
||||
if (match(proxy_mode), /tun/) {
|
||||
tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0';
|
||||
tun_addr4 = uci.get(uciconfig, uciinfra, 'tun_addr4') || '172.19.0.1/30';
|
||||
tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126';
|
||||
tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000';
|
||||
tun_gso = '0';
|
||||
tcpip_stack = 'system';
|
||||
if (routing_mode === 'custom') {
|
||||
tun_gso = uci.get(uciconfig, uciroutingsetting, 'tun_gso') || '0';
|
||||
tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system';
|
||||
endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
|
||||
}
|
||||
}
|
||||
/* UCI config end */
|
||||
|
||||
/* Config helper start */
|
||||
function parse_port(strport) {
|
||||
if (type(strport) !== 'array' || isEmpty(strport))
|
||||
return null;
|
||||
|
||||
let ports = [];
|
||||
for (let i in strport)
|
||||
push(ports, int(i));
|
||||
|
||||
return ports;
|
||||
|
||||
}
|
||||
|
||||
function parse_dnsquery(strquery) {
|
||||
if (type(strquery) !== 'array' || isEmpty(strquery))
|
||||
return null;
|
||||
|
||||
let querys = [];
|
||||
for (let i in strquery)
|
||||
isnan(int(i)) ? push(querys, i) : push(querys, int(i));
|
||||
|
||||
return querys;
|
||||
|
||||
}
|
||||
|
||||
function generate_outbound(node) {
|
||||
if (type(node) !== 'object' || isEmpty(node))
|
||||
return null;
|
||||
|
||||
const outbound = {
|
||||
type: node.type,
|
||||
tag: 'cfg-' + node['.name'] + '-out',
|
||||
routing_mark: strToInt(self_mark),
|
||||
|
||||
server: node.address,
|
||||
server_port: strToInt(node.port),
|
||||
|
||||
username: (node.type !== 'ssh') ? node.username : null,
|
||||
user: (node.type === 'ssh') ? node.username : null,
|
||||
password: node.password,
|
||||
|
||||
/* Direct */
|
||||
override_address: node.override_address,
|
||||
override_port: strToInt(node.override_port),
|
||||
/* Hysteria (2) */
|
||||
up_mbps: strToInt(node.hysteria_up_mbps),
|
||||
down_mbps: strToInt(node.hysteria_down_mbps),
|
||||
obfs: node.hysteria_obfs_type ? {
|
||||
type: node.hysteria_obfs_type,
|
||||
password: node.hysteria_obfs_password
|
||||
} : node.hysteria_obfs_password,
|
||||
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
|
||||
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
|
||||
recv_window_conn: strToInt(node.hysteria_recv_window_conn),
|
||||
recv_window: strToInt(node.hysteria_revc_window),
|
||||
disable_mtu_discovery: strToBool(node.hysteria_disable_mtu_discovery),
|
||||
/* Shadowsocks */
|
||||
method: node.shadowsocks_encrypt_method,
|
||||
plugin: node.shadowsocks_plugin,
|
||||
plugin_opts: node.shadowsocks_plugin_opts,
|
||||
/* ShadowTLS / Socks */
|
||||
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null),
|
||||
/* SSH */
|
||||
client_version: node.ssh_client_version,
|
||||
host_key: node.ssh_host_key,
|
||||
host_key_algorithms: node.ssh_host_key_algo,
|
||||
private_key: node.ssh_priv_key,
|
||||
private_key_passphrase: node.ssh_priv_key_pp,
|
||||
/* Tuic */
|
||||
uuid: node.uuid,
|
||||
congestion_control: node.tuic_congestion_control,
|
||||
udp_relay_mode: node.tuic_udp_relay_mode,
|
||||
udp_over_stream: strToBool(node.tuic_udp_over_stream),
|
||||
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
|
||||
heartbeat: node.tuic_heartbeat ? (node.tuic_heartbeat + 's') : null,
|
||||
/* VLESS / VMess */
|
||||
flow: node.vless_flow,
|
||||
alter_id: strToInt(node.vmess_alterid),
|
||||
security: node.vmess_encrypt,
|
||||
global_padding: node.vmess_global_padding ? (node.vmess_global_padding === '1') : null,
|
||||
authenticated_length: node.vmess_authenticated_length ? (node.vmess_authenticated_length === '1') : null,
|
||||
packet_encoding: node.packet_encoding,
|
||||
/* WireGuard */
|
||||
system_interface: (node.type === 'wireguard') || null,
|
||||
gso: (node.wireguard_gso === '1') || null,
|
||||
interface_name: (node.type === 'wireguard') ? 'wg-' + node['.name'] + '-out' : null,
|
||||
local_address: node.wireguard_local_address,
|
||||
private_key: node.wireguard_private_key,
|
||||
peer_public_key: node.wireguard_peer_public_key,
|
||||
pre_shared_key: node.wireguard_pre_shared_key,
|
||||
reserved: parse_port(node.wireguard_reserved),
|
||||
mtu: strToInt(node.wireguard_mtu),
|
||||
|
||||
multiplex: (node.multiplex === '1') ? {
|
||||
enabled: true,
|
||||
protocol: node.multiplex_protocol,
|
||||
max_connections: strToInt(node.multiplex_max_connections),
|
||||
min_streams: strToInt(node.multiplex_min_streams),
|
||||
max_streams: strToInt(node.multiplex_max_streams),
|
||||
padding: (node.multiplex_padding === '1'),
|
||||
brutal: (node.multiplex_brutal === '1') ? {
|
||||
enabled: true,
|
||||
up_mbps: strToInt(node.multiplex_brutal_up),
|
||||
down_mbps: strToInt(node.multiplex_brutal_down)
|
||||
} : null
|
||||
} : null,
|
||||
tls: (node.tls === '1') ? {
|
||||
enabled: true,
|
||||
server_name: node.tls_sni,
|
||||
insecure: (node.tls_insecure === '1'),
|
||||
alpn: node.tls_alpn,
|
||||
min_version: node.tls_min_version,
|
||||
max_version: node.tls_max_version,
|
||||
cipher_suites: node.tls_cipher_suites,
|
||||
certificate_path: node.tls_cert_path,
|
||||
ech: (node.tls_ech === '1') ? {
|
||||
enabled: true,
|
||||
dynamic_record_sizing_disabled: (node.tls_ech_tls_disable_drs === '1'),
|
||||
pq_signature_schemes_enabled: (node.tls_ech_enable_pqss === '1'),
|
||||
config: node.tls_ech_config
|
||||
} : null,
|
||||
utls: !isEmpty(node.tls_utls) ? {
|
||||
enabled: true,
|
||||
fingerprint: node.tls_utls
|
||||
} : null,
|
||||
reality: (node.tls_reality === '1') ? {
|
||||
enabled: true,
|
||||
public_key: node.tls_reality_public_key,
|
||||
short_id: node.tls_reality_short_id
|
||||
} : null
|
||||
} : null,
|
||||
transport: !isEmpty(node.transport) ? {
|
||||
type: node.transport,
|
||||
host: node.http_host || node.httpupgrade_host,
|
||||
path: node.http_path || node.ws_path,
|
||||
headers: node.ws_host ? {
|
||||
Host: node.ws_host
|
||||
} : null,
|
||||
method: node.http_method,
|
||||
max_early_data: strToInt(node.websocket_early_data),
|
||||
early_data_header_name: node.websocket_early_data_header,
|
||||
service_name: node.grpc_servicename,
|
||||
idle_timeout: node.http_idle_timeout ? (node.http_idle_timeout + 's') : null,
|
||||
ping_timeout: node.http_ping_timeout ? (node.http_ping_timeout + 's') : null,
|
||||
permit_without_stream: strToBool(node.grpc_permit_without_stream)
|
||||
} : null,
|
||||
udp_over_tcp: (node.udp_over_tcp === '1') ? {
|
||||
enabled: true,
|
||||
version: strToInt(node.udp_over_tcp_version)
|
||||
} : null,
|
||||
tcp_fast_open: strToBool(node.tcp_fast_open),
|
||||
tcp_multi_path: strToBool(node.tcp_multi_path),
|
||||
udp_fragment: strToBool(node.udp_fragment)
|
||||
};
|
||||
|
||||
return outbound;
|
||||
}
|
||||
|
||||
function get_outbound(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
if (type(cfg) === 'array') {
|
||||
if ('any-out' in cfg)
|
||||
return 'any';
|
||||
|
||||
let outbounds = [];
|
||||
for (let i in cfg)
|
||||
push(outbounds, get_outbound(i));
|
||||
return outbounds;
|
||||
} else {
|
||||
if (cfg in ['direct-out', 'block-out']) {
|
||||
return cfg;
|
||||
} else {
|
||||
const node = uci.get(uciconfig, cfg, 'node');
|
||||
if (isEmpty(node))
|
||||
die(sprintf("%s's node is missing, please check your configuration.", cfg));
|
||||
else
|
||||
return 'cfg-' + node + '-out';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_resolver(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
if (cfg in ['default-dns', 'system-dns', 'block-dns'])
|
||||
return cfg;
|
||||
else
|
||||
return 'cfg-' + cfg + '-dns';
|
||||
}
|
||||
|
||||
function get_ruleset(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
let rules = [];
|
||||
for (let i in cfg)
|
||||
push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule');
|
||||
return rules;
|
||||
}
|
||||
/* Config helper end */
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: 'warn',
|
||||
output: RUN_DIR + '/sing-box-c.log',
|
||||
timestamp: true
|
||||
};
|
||||
|
||||
/* DNS start */
|
||||
/* Default settings */
|
||||
config.dns = {
|
||||
servers: [
|
||||
{
|
||||
tag: 'default-dns',
|
||||
address: wan_dns,
|
||||
detour: 'direct-out'
|
||||
},
|
||||
{
|
||||
tag: 'system-dns',
|
||||
address: 'local',
|
||||
detour: 'direct-out'
|
||||
},
|
||||
{
|
||||
tag: 'block-dns',
|
||||
address: 'rcode://name_error'
|
||||
}
|
||||
],
|
||||
rules: [],
|
||||
strategy: dns_default_strategy,
|
||||
disable_cache: (dns_disable_cache === '1'),
|
||||
disable_expire: (dns_disable_cache_expire === '1'),
|
||||
independent_cache: (dns_independent_cache === '1'),
|
||||
client_subnet: dns_client_subnet
|
||||
};
|
||||
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Avoid DNS loop */
|
||||
const main_node_addr = uci.get(uciconfig, main_node, 'address');
|
||||
if (validateHostname(main_node_addr))
|
||||
push(config.dns.rules, {
|
||||
domain: main_node_addr,
|
||||
server: 'default-dns'
|
||||
});
|
||||
|
||||
if (dedicated_udp_node) {
|
||||
const main_udp_node_addr = uci.get(uciconfig, main_udp_node, 'address');
|
||||
if (validateHostname(main_udp_node_addr))
|
||||
push(config.dns.rules, {
|
||||
domain: main_udp_node_addr,
|
||||
server: 'default-dns'
|
||||
});
|
||||
}
|
||||
|
||||
if (direct_domain_list)
|
||||
push(config.dns.rules, {
|
||||
domain_keyword: direct_domain_list,
|
||||
server: 'default-dns'
|
||||
});
|
||||
|
||||
if (isEmpty(config.dns.rules))
|
||||
config.dns.rules = null;
|
||||
|
||||
let default_final_dns = 'default-dns';
|
||||
/* Main DNS */
|
||||
if (dns_server !== wan_dns) {
|
||||
push(config.dns.servers, {
|
||||
tag: 'main-dns',
|
||||
address: 'tcp://' + (validation('ip6addr', dns_server) ? `[${dns_server}]` : dns_server),
|
||||
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
|
||||
detour: 'main-out'
|
||||
});
|
||||
|
||||
default_final_dns = 'main-dns';
|
||||
}
|
||||
|
||||
config.dns.final = default_final_dns;
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
/* DNS servers */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.dns.servers, {
|
||||
tag: 'cfg-' + cfg['.name'] + '-dns',
|
||||
address: cfg.address,
|
||||
address: cfg.address,
|
||||
address_resolver: get_resolver(cfg.address_resolver),
|
||||
address_strategy: cfg.address_strategy,
|
||||
strategy: cfg.resolve_strategy,
|
||||
detour: get_outbound(cfg.outbound),
|
||||
client_subnet: cfg.client_subnet
|
||||
});
|
||||
});
|
||||
|
||||
/* DNS rules */
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.dns.rules, {
|
||||
ip_version: strToInt(cfg.ip_version),
|
||||
query_type: parse_dnsquery(cfg.query_type),
|
||||
network: cfg.network,
|
||||
protocol: cfg.protocol,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
domain_regex: cfg.domain_regex,
|
||||
port: parse_port(cfg.port),
|
||||
port_range: cfg.port_range,
|
||||
source_ip_cidr: cfg.source_ip_cidr,
|
||||
source_ip_is_private: (cfg.source_ip_is_private === '1') || null,
|
||||
ip_cidr: cfg.ip_cidr,
|
||||
ip_is_private: (cfg.ip_is_private === '1') || null,
|
||||
source_port: parse_port(cfg.source_port),
|
||||
source_port_range: cfg.source_port_range,
|
||||
process_name: cfg.process_name,
|
||||
process_path: cfg.process_path,
|
||||
user: cfg.user,
|
||||
rule_set: get_ruleset(cfg.rule_set),
|
||||
rule_set_ipcidr_match_source: (cfg.rule_set_ipcidr_match_source === '1') || null,
|
||||
invert: (cfg.invert === '1') || null,
|
||||
outbound: get_outbound(cfg.outbound),
|
||||
server: get_resolver(cfg.server),
|
||||
disable_cache: (cfg.dns_disable_cache === '1') || null,
|
||||
rewrite_ttl: strToInt(cfg.rewrite_ttl),
|
||||
client_subnet: cfg.client_subnet
|
||||
});
|
||||
});
|
||||
|
||||
if (isEmpty(config.dns.rules))
|
||||
config.dns.rules = null;
|
||||
|
||||
config.dns.final = get_resolver(dns_default_server);
|
||||
}
|
||||
/* DNS end */
|
||||
|
||||
/* Inbound start */
|
||||
config.inbounds = [];
|
||||
|
||||
push(config.inbounds, {
|
||||
type: 'direct',
|
||||
tag: 'dns-in',
|
||||
listen: '::',
|
||||
listen_port: int(dns_port)
|
||||
});
|
||||
|
||||
push(config.inbounds, {
|
||||
type: 'mixed',
|
||||
tag: 'mixed-in',
|
||||
listen: '::',
|
||||
listen_port: int(mixed_port),
|
||||
sniff: true,
|
||||
sniff_override_destination: (sniff_override === '1'),
|
||||
set_system_proxy: false
|
||||
});
|
||||
|
||||
if (match(proxy_mode, /redirect/))
|
||||
push(config.inbounds, {
|
||||
type: 'redirect',
|
||||
tag: 'redirect-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: int(redirect_port),
|
||||
sniff: true,
|
||||
sniff_override_destination: (sniff_override === '1')
|
||||
});
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
push(config.inbounds, {
|
||||
type: 'tproxy',
|
||||
tag: 'tproxy-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: int(tproxy_port),
|
||||
network: 'udp',
|
||||
sniff: true,
|
||||
sniff_override_destination: (sniff_override === '1')
|
||||
});
|
||||
if (match(proxy_mode, /tun/))
|
||||
push(config.inbounds, {
|
||||
type: 'tun',
|
||||
tag: 'tun-in',
|
||||
|
||||
interface_name: tun_name,
|
||||
inet4_address: tun_addr4,
|
||||
inet6_address: (ipv6_support === '1') ? tun_addr6 : null,
|
||||
mtu: strToInt(tun_mtu),
|
||||
gso: (tun_gso === '1'),
|
||||
auto_route: false,
|
||||
endpoint_independent_nat: strToBool(endpoint_independent_nat),
|
||||
stack: tcpip_stack,
|
||||
sniff: true,
|
||||
sniff_override_destination: (sniff_override === '1'),
|
||||
});
|
||||
/* Inbound end */
|
||||
|
||||
/* Outbound start */
|
||||
/* Default outbounds */
|
||||
config.outbounds = [
|
||||
{
|
||||
type: 'direct',
|
||||
tag: 'direct-out',
|
||||
routing_mark: strToInt(self_mark)
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
tag: 'block-out'
|
||||
},
|
||||
{
|
||||
type: 'dns',
|
||||
tag: 'dns-out'
|
||||
}
|
||||
];
|
||||
|
||||
/* Main outbounds */
|
||||
if (!isEmpty(main_node)) {
|
||||
const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
|
||||
push(config.outbounds, generate_outbound(main_node_cfg));
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'main-out';
|
||||
|
||||
if (dedicated_udp_node) {
|
||||
const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
|
||||
push(config.outbounds, generate_outbound(main_udp_node_cfg));
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
|
||||
}
|
||||
} else if (!isEmpty(default_outbound))
|
||||
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
const outbound = uci.get_all(uciconfig, cfg.node) || {};
|
||||
push(config.outbounds, generate_outbound(outbound));
|
||||
config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy;
|
||||
config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
|
||||
config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
|
||||
});
|
||||
/* Outbound end */
|
||||
|
||||
/* Routing rules start */
|
||||
/* Default settings */
|
||||
config.route = {
|
||||
rules: [
|
||||
{
|
||||
inbound: 'dns-in',
|
||||
outbound: 'dns-out'
|
||||
},
|
||||
{
|
||||
protocol: 'dns',
|
||||
outbound: 'dns-out'
|
||||
}
|
||||
],
|
||||
rule_set: [],
|
||||
auto_detect_interface: isEmpty(default_interface) ? true : null,
|
||||
default_interface: default_interface
|
||||
};
|
||||
|
||||
/* Routing rules */
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Direct list */
|
||||
if (length(direct_domain_list))
|
||||
push(config.route.rules, {
|
||||
domain_keyword: direct_domain_list,
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
|
||||
/* Main UDP out */
|
||||
if (dedicated_udp_node)
|
||||
push(config.route.rules, {
|
||||
network: 'udp',
|
||||
outbound: 'main-udp-out'
|
||||
});
|
||||
|
||||
config.route.final = 'main-out';
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
|
||||
push(config.route.rules, {
|
||||
ip_version: strToInt(cfg.ip_version),
|
||||
protocol: cfg.protocol,
|
||||
network: cfg.network,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
domain_regex: cfg.domain_regex,
|
||||
source_ip_cidr: cfg.source_ip_cidr,
|
||||
source_ip_is_private: (cfg.source_ip_is_private === '1') || null,
|
||||
ip_cidr: cfg.ip_cidr,
|
||||
ip_is_private: (cfg.ip_is_private === '1') || null,
|
||||
source_port: parse_port(cfg.source_port),
|
||||
source_port_range: cfg.source_port_range,
|
||||
port: parse_port(cfg.port),
|
||||
port_range: cfg.port_range,
|
||||
process_name: cfg.process_name,
|
||||
process_path: cfg.process_path,
|
||||
user: cfg.user,
|
||||
rule_set: get_ruleset(cfg.rule_set),
|
||||
rule_set_ipcidr_match_source: (cfg.rule_set_ipcidr_match_source === '1') || null,
|
||||
invert: (cfg.invert === '1') || null,
|
||||
outbound: get_outbound(cfg.outbound)
|
||||
});
|
||||
});
|
||||
|
||||
config.route.final = get_outbound(default_outbound);
|
||||
};
|
||||
|
||||
/* Rule set */
|
||||
if (routing_mode === 'custom') {
|
||||
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
|
||||
push(config.route.rule_set, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-rule',
|
||||
format: cfg.format,
|
||||
path: cfg.path,
|
||||
url: cfg.url,
|
||||
download_detour: get_outbound(cfg.outbound),
|
||||
update_interval: cfg.update_interval
|
||||
});
|
||||
});
|
||||
}
|
||||
/* Routing rules end */
|
||||
|
||||
/* Experimental start */
|
||||
if (routing_mode === 'custom') {
|
||||
config.experimental = {
|
||||
cache_file: {
|
||||
enabled: true,
|
||||
path: HP_DIR + '/cache.db',
|
||||
store_rdrc: (cache_file_store_rdrc === '1') || null,
|
||||
rdrc_timeout: cache_file_rdrc_timeout
|
||||
}
|
||||
};
|
||||
}
|
||||
/* Experimental end */
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config)));
|
175
luci-app-homeproxy/root/etc/homeproxy/scripts/generate_server.uc
Executable file
175
luci-app-homeproxy/root/etc/homeproxy/scripts/generate_server.uc
Executable file
@ -0,0 +1,175 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import {
|
||||
executeCommand, isEmpty, strToBool, strToInt,
|
||||
removeBlankAttrs, validateHostname, validation,
|
||||
HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const uciserver = 'server';
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: 'warn',
|
||||
output: RUN_DIR + '/sing-box-s.log',
|
||||
timestamp: true
|
||||
};
|
||||
|
||||
config.inbounds = [];
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.inbounds, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-in',
|
||||
|
||||
listen: cfg.address || '::',
|
||||
listen_port: strToInt(cfg.port),
|
||||
tcp_fast_open: strToBool(cfg.tcp_fast_open),
|
||||
tcp_multi_path: strToBool(cfg.tcp_multi_path),
|
||||
udp_fragment: strToBool(cfg.udp_fragment),
|
||||
sniff: true,
|
||||
sniff_override_destination: (cfg.sniff_override === '1'),
|
||||
domain_strategy: cfg.domain_strategy,
|
||||
network: cfg.network,
|
||||
|
||||
/* Hysteria */
|
||||
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
||||
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
||||
obfs: cfg.hysteria_obfs_type ? {
|
||||
type: cfg.hysteria_obfs_type,
|
||||
password: cfg.hysteria_obfs_password
|
||||
} : cfg.hysteria_obfs_password,
|
||||
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
|
||||
recv_window_client: strToInt(cfg.hysteria_revc_window_client),
|
||||
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
|
||||
disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
|
||||
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||
masquerade: cfg.hysteria_masquerade,
|
||||
|
||||
/* Shadowsocks */
|
||||
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
|
||||
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
|
||||
|
||||
/* Tuic */
|
||||
congestion_control: cfg.tuic_congestion_control,
|
||||
auth_timeout: cfg.tuic_auth_timeout ? (cfg.tuic_auth_timeout + 's') : null,
|
||||
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
|
||||
heartbeat: cfg.tuic_heartbeat ? (cfg.tuic_heartbeat + 's') : null,
|
||||
|
||||
/* HTTP / Hysteria (2) / Socks / Trojan / Tuic / VLESS / VMess */
|
||||
users: (cfg.type !== 'shadowsocks') ? [
|
||||
{
|
||||
name: !(cfg.type in ['http', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
|
||||
/* Hysteria */
|
||||
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
|
||||
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
|
||||
|
||||
/* Tuic */
|
||||
uuid: cfg.uuid,
|
||||
|
||||
/* VLESS / VMess */
|
||||
flow: cfg.vless_flow,
|
||||
alterId: strToInt(cfg.vmess_alterid)
|
||||
}
|
||||
] : null,
|
||||
|
||||
multiplex: (cfg.multiplex === '1') ? {
|
||||
enabled: true,
|
||||
padding: (cfg.multiplex_padding === '1'),
|
||||
brutal: (cfg.multiplex_brutal === '1') ? {
|
||||
enabled: true,
|
||||
up_mbps: strToInt(cfg.multiplex_brutal_up),
|
||||
down_mbps: strToInt(cfg.multiplex_brutal_down)
|
||||
} : null
|
||||
} : null,
|
||||
|
||||
tls: (cfg.tls === '1') ? {
|
||||
enabled: true,
|
||||
server_name: cfg.tls_sni,
|
||||
alpn: cfg.tls_alpn,
|
||||
min_version: cfg.tls_min_version,
|
||||
max_version: cfg.tls_max_version,
|
||||
cipher_suites: cfg.tls_cipher_suites,
|
||||
certificate_path: cfg.tls_cert_path,
|
||||
key_path: cfg.tls_key_path,
|
||||
acme: (cfg.tls_acme === '1') ? {
|
||||
domain: cfg.tls_acme_domains,
|
||||
data_directory: HP_DIR + '/certs',
|
||||
default_server_name: cfg.tls_acme_dsn,
|
||||
email: cfg.tls_acme_email,
|
||||
provider: cfg.tls_acme_provider,
|
||||
disable_http_challenge: (cfg.tls_acme_dhc === '1'),
|
||||
disable_tls_alpn_challenge: (cfg.tls_acme_dtac === '1'),
|
||||
alternative_http_port: strToInt(cfg.tls_acme_ahp),
|
||||
alternative_tls_port: strToInt(cfg.tls_acme_atp),
|
||||
external_account: (cfg.tls_acme_external_account === '1') ? {
|
||||
key_id: cfg.tls_acme_ea_keyid,
|
||||
mac_key: cfg.tls_acme_ea_mackey
|
||||
} : null,
|
||||
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
|
||||
provider: cfg.tls_dns01_provider,
|
||||
access_key_id: cfg.tls_dns01_ali_akid,
|
||||
access_key_secret: cfg.tls_dns01_ali_aksec,
|
||||
region_id: cfg.tls_dns01_ali_rid,
|
||||
api_token: cfg.tls_dns01_cf_api_token
|
||||
} : null
|
||||
} : null,
|
||||
reality: (cfg.tls_reality === '1') ? {
|
||||
enabled: true,
|
||||
private_key: cfg.tls_reality_private_key,
|
||||
short_id: cfg.tls_reality_short_id,
|
||||
max_time_difference: cfg.tls_reality_max_time_difference ? (cfg.max_time_difference + 's') : null,
|
||||
handshake: {
|
||||
server: cfg.tls_reality_server_addr,
|
||||
server_port: strToInt(cfg.tls_reality_server_port)
|
||||
}
|
||||
} : null
|
||||
} : null,
|
||||
|
||||
transport: !isEmpty(cfg.transport) ? {
|
||||
type: cfg.transport,
|
||||
host: cfg.http_host || cfg.httpupgrade_host,
|
||||
path: cfg.http_path || cfg.ws_path,
|
||||
headers: cfg.ws_host ? {
|
||||
Host: cfg.ws_host
|
||||
} : null,
|
||||
method: cfg.http_method,
|
||||
max_early_data: strToInt(cfg.websocket_early_data),
|
||||
early_data_header_name: cfg.websocket_early_data_header,
|
||||
service_name: cfg.grpc_servicename,
|
||||
idle_timeout: cfg.http_idle_timeout ? (cfg.http_idle_timeout + 's') : null,
|
||||
ping_timeout: cfg.http_ping_timeout ? (cfg.http_ping_timeout + 's') : null
|
||||
} : null
|
||||
});
|
||||
});
|
||||
|
||||
if (length(config.inbounds) === 0)
|
||||
exit(1);
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config)));
|
231
luci-app-homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
Normal file
231
luci-app-homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
import { mkstemp } from 'fs';
|
||||
import { urldecode, urldecode_params } from 'luci.http';
|
||||
|
||||
/* Global variables start */
|
||||
export const HP_DIR = '/etc/homeproxy';
|
||||
export const RUN_DIR = '/var/run/homeproxy';
|
||||
/* Global variables end */
|
||||
|
||||
/* Utilities start */
|
||||
/* Kanged from luci-app-commands */
|
||||
export function shellQuote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
};
|
||||
|
||||
export function isBinary(str) {
|
||||
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
|
||||
if (byte <= 8 || (byte >= 14 && byte <= 31))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function executeCommand(...args) {
|
||||
let outfd = mkstemp();
|
||||
let errfd = mkstemp();
|
||||
|
||||
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
|
||||
|
||||
outfd.seek(0);
|
||||
errfd.seek(0);
|
||||
|
||||
const stdout = outfd.read(1024 * 512) ?? '';
|
||||
const stderr = errfd.read(1024 * 512) ?? '';
|
||||
|
||||
outfd.close();
|
||||
errfd.close();
|
||||
|
||||
const binary = isBinary(stdout);
|
||||
|
||||
return {
|
||||
command: join(' ', args),
|
||||
stdout: binary ? null : stdout,
|
||||
stderr,
|
||||
exitcode,
|
||||
binary
|
||||
};
|
||||
};
|
||||
|
||||
export function calcStringMD5(str) {
|
||||
if (!str || type(str) !== 'string')
|
||||
return null;
|
||||
|
||||
const output = executeCommand(`/bin/echo -n ${shellQuote(str)} | /usr/bin/md5sum | /usr/bin/awk '{print $1}'`) || {};
|
||||
return trim(output.stdout);
|
||||
};
|
||||
|
||||
export function getTime(epoch) {
|
||||
const local_time = localtime(epoch);
|
||||
return replace(replace(sprintf(
|
||||
'%d-%2d-%2d@%2d:%2d:%2d',
|
||||
local_time.year,
|
||||
local_time.mon,
|
||||
local_time.mday,
|
||||
local_time.hour,
|
||||
local_time.min,
|
||||
local_time.sec
|
||||
), ' ', '0'), '@', ' ');
|
||||
|
||||
};
|
||||
|
||||
export function wGET(url) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -qO- --user-agent 'Wget/1.21 (HomeProxy, like v2rayN)' --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return trim(output.stdout);
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
/* String helper start */
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export function strToBool(str) {
|
||||
return (str === '1') || null;
|
||||
};
|
||||
|
||||
export function strToInt(str) {
|
||||
return !isEmpty(str) ? (int(str) || null) : null;
|
||||
};
|
||||
|
||||
export function removeBlankAttrs(res) {
|
||||
let content;
|
||||
|
||||
if (type(res) === 'object') {
|
||||
content = {};
|
||||
map(keys(res), (k) => {
|
||||
if (type(res[k]) in ['array', 'object'])
|
||||
content[k] = removeBlankAttrs(res[k]);
|
||||
else if (res[k] !== null && res[k] !== '')
|
||||
content[k] = res[k];
|
||||
});
|
||||
} else if (type(res) === 'array') {
|
||||
content = [];
|
||||
map(res, (k, i) => {
|
||||
if (type(k) in ['array', 'object'])
|
||||
push(content, removeBlankAttrs(k));
|
||||
else if (k !== null && k !== '')
|
||||
push(content, k);
|
||||
});
|
||||
} else
|
||||
return res;
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export function validateHostname(hostname) {
|
||||
return (match(hostname, /^[a-zA-Z0-9_]+$/) != null ||
|
||||
(match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) &&
|
||||
match(hostname, /[^0-9.]/)));
|
||||
};
|
||||
|
||||
export function validation(datatype, data) {
|
||||
if (!datatype || !data)
|
||||
return null;
|
||||
|
||||
const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
|
||||
return (ret === 0);
|
||||
};
|
||||
/* String helper end */
|
||||
|
||||
/* String parser start */
|
||||
export function decodeBase64Str(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
str = trim(str);
|
||||
str = replace(str, '_', '/');
|
||||
str = replace(str, '-', '+');
|
||||
|
||||
const padding = length(str) % 4;
|
||||
if (padding)
|
||||
str = str + substr('====', padding);
|
||||
|
||||
return b64dec(str);
|
||||
};
|
||||
|
||||
export function parseURL(url) {
|
||||
if (type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
const services = {
|
||||
http: '80',
|
||||
https: '443'
|
||||
};
|
||||
|
||||
const objurl = {};
|
||||
|
||||
objurl.href = url;
|
||||
|
||||
url = replace(url, /#(.+)$/, (_, val) => {
|
||||
objurl.hash = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => {
|
||||
objurl.protocol = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /\?(.+)/, (_, val) => {
|
||||
objurl.search = val;
|
||||
objurl.searchParams = urldecode_params(val);
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^\/\/([^\/]+)/, (_, val) => {
|
||||
val = replace(val, /^([^@]+)@/, (_, val) => {
|
||||
objurl.userinfo = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
val = replace(val, /:(\d+)$/, (_, val) => {
|
||||
objurl.port = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (validation('ip4addr', val) ||
|
||||
validation('ip6addr', replace(val, /\[|\]/g, '')) ||
|
||||
validation('hostname', val))
|
||||
objurl.hostname = val;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
objurl.pathname = url || '/';
|
||||
|
||||
if (!objurl.protocol || !objurl.hostname)
|
||||
return null;
|
||||
|
||||
if (objurl.userinfo) {
|
||||
objurl.userinfo = replace(objurl.userinfo, /:([^:]+)$/, (_, val) => {
|
||||
objurl.password = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) {
|
||||
objurl.username = objurl.userinfo;
|
||||
delete objurl.userinfo;
|
||||
} else {
|
||||
delete objurl.userinfo;
|
||||
delete objurl.password;
|
||||
}
|
||||
};
|
||||
|
||||
if (!objurl.port)
|
||||
objurl.port = services[objurl.protocol];
|
||||
|
||||
objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : '');
|
||||
objurl.origin = `${objurl.protocol}://${objurl.host}`;
|
||||
|
||||
return objurl;
|
||||
};
|
||||
/* String parser end */
|
12
luci-app-homeproxy/root/etc/homeproxy/scripts/update_crond.sh
Executable file
12
luci-app-homeproxy/root/etc/homeproxy/scripts/update_crond.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
||||
|
||||
for i in "china_ip4" "china_ip6" "gfw_list" "china_list"; do
|
||||
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
||||
done
|
||||
|
||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
105
luci-app-homeproxy/root/etc/homeproxy/scripts/update_resources.sh
Executable file
105
luci-app-homeproxy/root/etc/homeproxy/scripts/update_resources.sh
Executable file
@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
RESOURCES_DIR="/etc/$NAME/resources"
|
||||
mkdir -p "$RESOURCES_DIR"
|
||||
|
||||
RUN_DIR="/var/run/$NAME"
|
||||
LOG_PATH="$RUN_DIR/$NAME.log"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
set_lock() {
|
||||
local act="$1"
|
||||
local type="$2"
|
||||
|
||||
local lock="$RUN_DIR/update_resources-$type.lock"
|
||||
if [ "$act" = "set" ]; then
|
||||
if [ -e "$lock" ]; then
|
||||
log "[$(to_upper "$type")] A task is already running."
|
||||
exit 2
|
||||
else
|
||||
touch "$lock"
|
||||
fi
|
||||
elif [ "$act" = "remove" ]; then
|
||||
rm -f "$lock"
|
||||
fi
|
||||
}
|
||||
|
||||
to_upper() {
|
||||
echo -e "$1" | tr "[a-z]" "[A-Z]"
|
||||
}
|
||||
|
||||
check_list_update() {
|
||||
local listtype="$1"
|
||||
local listrepo="$2"
|
||||
local listref="$3"
|
||||
local listname="$4"
|
||||
local wget="wget --timeout=10 -q"
|
||||
|
||||
set_lock "set" "$listtype"
|
||||
|
||||
local list_info="$($wget -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname")"
|
||||
local list_sha="$(echo -e "$list_info" | jsonfilter -e "@[0].sha")"
|
||||
local list_ver="$(echo -e "$list_info" | jsonfilter -e "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')"
|
||||
if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then
|
||||
log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")"
|
||||
if [ "$local_list_ver" = "$list_ver" ]; then
|
||||
log "[$(to_upper "$listtype")] Current version: $list_ver."
|
||||
log "[$(to_upper "$listtype")] You're already at the latest version."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 3
|
||||
else
|
||||
log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver."
|
||||
fi
|
||||
|
||||
$wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname"
|
||||
if [ ! -s "$RUN_DIR/$listname" ]; then
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
log "[$(to_upper "$listtype")] Update failed."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"
|
||||
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
|
||||
log "[$(to_upper "$listtype")] Successfully updated."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"china_ip4")
|
||||
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
|
||||
;;
|
||||
"china_ip6")
|
||||
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
|
||||
;;
|
||||
"gfw_list")
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
|
||||
;;
|
||||
"china_list")
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
|
||||
sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
|
||||
;;
|
||||
*)
|
||||
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
616
luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
Executable file
616
luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
Executable file
@ -0,0 +1,616 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { open } from 'fs';
|
||||
import { connect } from 'ubus';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import { urldecode, urlencode, urldecode_params } from 'luci.http';
|
||||
import { init_action } from 'luci.sys';
|
||||
|
||||
import {
|
||||
calcStringMD5, wGET, executeCommand, decodeBase64Str,
|
||||
getTime, isEmpty, parseURL, validation,
|
||||
HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const ucimain = 'config',
|
||||
ucinode = 'node',
|
||||
ucisubscription = 'subscription';
|
||||
|
||||
const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '0',
|
||||
filter_mode = uci.get(uciconfig, ucisubscription, 'filter_nodes') || 'disabled',
|
||||
filter_keywords = uci.get(uciconfig, ucisubscription, 'filter_keywords') || [],
|
||||
packet_encoding = uci.get(uciconfig, ucisubscription, 'packet_encoding') || 'xudp',
|
||||
subscription_urls = uci.get(uciconfig, ucisubscription, 'subscription_url') || [],
|
||||
via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainalnd_china';
|
||||
let main_node, main_udp_node;
|
||||
if (routing_mode !== 'custom') {
|
||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||
}
|
||||
/* UCI config end */
|
||||
|
||||
/* String helper start */
|
||||
function filter_check(name) {
|
||||
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
|
||||
return false;
|
||||
|
||||
let ret = false;
|
||||
for (let i in filter_keywords) {
|
||||
const patten = regexp(i);
|
||||
if (match(name, patten))
|
||||
ret = true;
|
||||
}
|
||||
if (filter_mode === 'whitelist')
|
||||
ret = !ret;
|
||||
|
||||
return ret;
|
||||
}
|
||||
/* String helper end */
|
||||
|
||||
/* Common var start */
|
||||
const node_cache = {},
|
||||
node_result = [];
|
||||
|
||||
const ubus = connect();
|
||||
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
||||
/* Common var end */
|
||||
|
||||
/* Log */
|
||||
system(`mkdir -p ${RUN_DIR}`);
|
||||
function log(...args) {
|
||||
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
|
||||
logfile.write(`${getTime()} [SUBSCRIBE] ${join(' ', args)}\n`);
|
||||
logfile.close();
|
||||
}
|
||||
|
||||
function parse_uri(uri) {
|
||||
let config, url, params;
|
||||
|
||||
if (type(uri) === 'object') {
|
||||
if (uri.nodetype === 'sip008') {
|
||||
/* https://shadowsocks.org/guide/sip008.html */
|
||||
config = {
|
||||
label: uri.remarks,
|
||||
type: 'shadowsocks',
|
||||
address: uri.server,
|
||||
port: uri.server_port,
|
||||
shadowsocks_encrypt_method: uri.method,
|
||||
password: uri.password,
|
||||
shadowsocks_plugin: uri.plugin,
|
||||
shadowsocks_plugin_opts: uri.plugin_opts
|
||||
};
|
||||
}
|
||||
} else if (type(uri) === 'string') {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
switch (uri[0]) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
url = parseURL('http://' + uri[1]);
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'http',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
username: url.username ? urldecode(url.username) : null,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
tls: (uri[0] === 'https') ? '1' : '0'
|
||||
};
|
||||
|
||||
break;
|
||||
case 'hysteria':
|
||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
||||
url = parseURL('http://' + uri[1]);
|
||||
params = url.searchParams;
|
||||
|
||||
if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'hysteria', urldecode(url.hash) || url.hostname));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'hysteria',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
hysteria_protocol: params.protocol || 'udp',
|
||||
hysteria_auth_type: params.auth ? 'string' : null,
|
||||
hysteria_auth_payload: params.auth,
|
||||
hysteria_obfs_password: params.obfsParam,
|
||||
hysteria_down_mbps: params.downmbps,
|
||||
hysteria_up_mbps: params.upmbps,
|
||||
tls: '1',
|
||||
tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
|
||||
tls_sni: params.peer,
|
||||
tls_alpn: params.alpn
|
||||
};
|
||||
|
||||
break;
|
||||
case 'hysteria2':
|
||||
case 'hy2':
|
||||
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
||||
url = parseURL('http://' + uri[1]);
|
||||
params = url.searchParams;
|
||||
|
||||
if (!sing_features.with_quic) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'hysteria2', urldecode(url.hash) || url.hostname));
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'hysteria2',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: url.username ? (
|
||||
urldecode(url.username + (url.password ? (':' + url.password) : ''))
|
||||
) : null,
|
||||
hysteria_obfs_type: params.obfs,
|
||||
hysteria_obfs_password: params['obfs-password'],
|
||||
tls: '1',
|
||||
tls_insecure: params.insecure ? '1' : '0',
|
||||
tls_sni: params.sni
|
||||
};
|
||||
|
||||
break;
|
||||
case 'socks':
|
||||
case 'socks4':
|
||||
case 'socks4a':
|
||||
case 'socsk5':
|
||||
case 'socks5h':
|
||||
url = parseURL('http://' + uri[1]);
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'socks',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
username: url.username ? urldecode(url.username) : null,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
socks_version: (match(uri[0], /4/)) ? '4' : '5'
|
||||
};
|
||||
|
||||
break;
|
||||
case 'ss':
|
||||
/* "Lovely" Shadowrocket format */
|
||||
const ss_suri = split(uri[1], '#');
|
||||
let ss_slabel = '';
|
||||
if (length(ss_suri) <= 2) {
|
||||
if (length(ss_suri) === 2)
|
||||
ss_slabel = '#' + urlencode(ss_suri[1]);
|
||||
if (decodeBase64Str(ss_suri[0]))
|
||||
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
|
||||
}
|
||||
|
||||
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
|
||||
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
||||
|
||||
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
||||
url = parseURL('http://' + uri[1]);
|
||||
|
||||
let ss_userinfo = {};
|
||||
if (url.username && url.password)
|
||||
/* User info encoded with URIComponent */
|
||||
ss_userinfo = [url.username, urldecode(url.password)];
|
||||
else if (url.username)
|
||||
/* User info encoded with base64 */
|
||||
ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':');
|
||||
|
||||
let ss_plugin, ss_plugin_opts;
|
||||
if (url.search && url.searchParams.plugin) {
|
||||
const ss_plugin_info = split(url.searchParams.plugin, ';');
|
||||
ss_plugin = ss_plugin_info[0];
|
||||
if (ss_plugin === 'simple-obfs')
|
||||
/* Fix non-standard plugin name */
|
||||
ss_plugin = 'obfs-local';
|
||||
ss_plugin_opts = slice(ss_plugin_info, 1) ? join(';', slice(ss_plugin_info, 1)) : null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'shadowsocks',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
shadowsocks_encrypt_method: ss_userinfo[0],
|
||||
password: ss_userinfo[1],
|
||||
shadowsocks_plugin: ss_plugin,
|
||||
shadowsocks_plugin_opts: ss_plugin_opts
|
||||
};
|
||||
|
||||
break;
|
||||
case 'trojan':
|
||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
||||
url = parseURL('http://' + uri[1]);
|
||||
params = url.searchParams || {};
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'trojan',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: urldecode(url.username),
|
||||
transport: (params.type !== 'tcp') ? params.type : null,
|
||||
tls: '1',
|
||||
tls_sni: params.sni
|
||||
};
|
||||
switch(params.type) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = params.serviceName;
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = params.host ? urldecode(params.host) : null;
|
||||
config.ws_path = params.path ? urldecode(params.path) : null;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'tuic':
|
||||
/* https://github.com/daeuniverse/dae/discussions/182 */
|
||||
url = parseURL('http://' + uri[1]);
|
||||
params = url.searchParams || {};
|
||||
|
||||
if (!sing_features.with_quic) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'TUIC', urldecode(url.hash) || url.hostname));
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'tuic',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
uuid: url.username,
|
||||
password: url.password ? urldecode(url.password) : null,
|
||||
tuic_congestion_control: params.congestion_control,
|
||||
tuic_udp_relay_mode: params.udp_relay_mode,
|
||||
tls: '1',
|
||||
tls_sni: params.sni,
|
||||
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
||||
};
|
||||
|
||||
break;
|
||||
case 'vless':
|
||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
||||
url = parseURL('http://' + uri[1]);
|
||||
params = url.searchParams;
|
||||
|
||||
/* Unsupported protocol */
|
||||
if (params.type === 'kcp') {
|
||||
log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(url.hash) || url.hostname));
|
||||
return null;
|
||||
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) {
|
||||
log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(url.hash) || url.hostname));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'vless',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
uuid: url.username,
|
||||
transport: (params.type !== 'tcp') ? params.type : null,
|
||||
tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
|
||||
tls_sni: params.sni,
|
||||
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
||||
tls_reality: (params.security === 'reality') ? '1' : '0',
|
||||
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
||||
tls_reality_short_id: params.sid,
|
||||
tls_utls: sing_features.with_utls ? params.fp : null,
|
||||
vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
|
||||
};
|
||||
switch(params.type) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = params.serviceName;
|
||||
break;
|
||||
case 'http':
|
||||
case 'tcp':
|
||||
if (params.type === 'http' || params.headerType === 'http') {
|
||||
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
|
||||
config.http_path = params.path ? urldecode(params.path) : null;
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = params.host ? urldecode(params.host) : null;
|
||||
config.ws_path = params.path ? urldecode(params.path) : null;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'vmess':
|
||||
/* "Lovely" shadowrocket format */
|
||||
if (match(uri, /&/)) {
|
||||
log(sprintf('Skipping unsupported %s format.', 'VMess'));
|
||||
return null;
|
||||
}
|
||||
|
||||
/* https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) */
|
||||
try {
|
||||
uri = json(decodeBase64Str(uri[1]));
|
||||
} catch(e) {
|
||||
log(sprintf('Skipping unsupported %s format.', 'VMess'));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uri.v != '2') {
|
||||
log(sprintf('Skipping unsupported %s format.', 'VMess'));
|
||||
return null;
|
||||
/* Unsupported protocol */
|
||||
} else if (uri.net === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
|
||||
return null;
|
||||
} else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
|
||||
* else if (uri.aid && int(uri.aid) !== 0) {
|
||||
* log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
|
||||
* return null;
|
||||
* }
|
||||
*/
|
||||
|
||||
config = {
|
||||
label: uri.ps ? urldecode(uri.ps) : null,
|
||||
type: 'vmess',
|
||||
address: uri.add,
|
||||
port: uri.port,
|
||||
uuid: uri.id,
|
||||
vmess_alterid: uri.aid,
|
||||
vmess_encrypt: uri.scy || 'auto',
|
||||
vmess_global_padding: '1',
|
||||
transport: (uri.net !== 'tcp') ? uri.net : null,
|
||||
tls: (uri.tls === 'tls') ? '1' : '0',
|
||||
tls_sni: uri.sni || uri.host,
|
||||
tls_alpn: uri.alpn ? split(uri.alpn, ',') : null
|
||||
};
|
||||
switch (uri.net) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = uri.path;
|
||||
break;
|
||||
case 'h2':
|
||||
case 'tcp':
|
||||
if (uri.net === 'h2' || uri.type === 'http') {
|
||||
config.transport = 'http';
|
||||
config.http_host = uri.host ? uri.host.split(',') : null;
|
||||
config.http_path = uri.path;
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = uri.host;
|
||||
config.ws_path = uri.path;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(config)) {
|
||||
if (config.address)
|
||||
config.address = replace(config.address, /\[|\]/g, '');
|
||||
|
||||
if (!validation('host', config.address) || !validation('port', config.port)) {
|
||||
log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
|
||||
return null;
|
||||
} else if (!config.label)
|
||||
config.label = (validation('ip6addr', config.address) ?
|
||||
`[${config.address}]` : config.address) + ':' + config.port;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (via_proxy !== '1') {
|
||||
log('Stopping service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
}
|
||||
|
||||
for (let url in subscription_urls) {
|
||||
const groupHash = calcStringMD5(url);
|
||||
node_cache[groupHash] = {};
|
||||
|
||||
const res = wGET(url);
|
||||
if (isEmpty(res)) {
|
||||
log(sprintf('Failed to fetch resources from %s.', url));
|
||||
continue;
|
||||
}
|
||||
|
||||
let nodes;
|
||||
try {
|
||||
nodes = json(res).servers || json(res);
|
||||
|
||||
/* Shadowsocks SIP008 format */
|
||||
if (nodes[0].server && nodes[0].method)
|
||||
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
||||
} catch(e) {
|
||||
nodes = decodeBase64Str(res);
|
||||
nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : {};
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (let node in nodes) {
|
||||
let config;
|
||||
if (!isEmpty(node))
|
||||
config = parse_uri(node);
|
||||
if (isEmpty(config))
|
||||
continue;
|
||||
|
||||
const label = config.label;
|
||||
config.label = null;
|
||||
const confHash = calcStringMD5(sprintf('%J', config)),
|
||||
nameHash = calcStringMD5(label);
|
||||
config.label = label;
|
||||
|
||||
if (filter_check(config.label))
|
||||
log(sprintf('Skipping blacklist node: %s.', config.label));
|
||||
else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash])
|
||||
log(sprintf('Skipping duplicate node: %s.', config.label));
|
||||
else {
|
||||
if (config.tls === '1' && allow_insecure === '1')
|
||||
config.tls_insecure = '1';
|
||||
if (config.type in ['vless', 'vmess'])
|
||||
config.packet_encoding = packet_encoding;
|
||||
|
||||
config.grouphash = groupHash;
|
||||
push(node_result, []);
|
||||
push(node_result[length(node_result)-1], config);
|
||||
node_cache[groupHash][confHash] = config;
|
||||
node_cache[groupHash][nameHash] = config;
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
log(sprintf('No valid node found in %s.', url));
|
||||
else
|
||||
log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url));
|
||||
}
|
||||
|
||||
if (isEmpty(node_result)) {
|
||||
log('Failed to update subscriptions: no valid node found.');
|
||||
|
||||
if (via_proxy !== '1') {
|
||||
log('Starting service...');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let added = 0, removed = 0;
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* Nodes created by the user */
|
||||
if (!cfg.grouphash)
|
||||
return null;
|
||||
|
||||
/* Empty object - failed to fetch nodes */
|
||||
if (length(node_cache[cfg.grouphash]) === 0)
|
||||
return null;
|
||||
|
||||
if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
removed++;
|
||||
|
||||
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
||||
} else {
|
||||
map(keys(node_cache[cfg.grouphash][cfg['.name']]), (v) => {
|
||||
uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
|
||||
});
|
||||
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
|
||||
}
|
||||
});
|
||||
for (let nodes in node_result)
|
||||
map(nodes, (node) => {
|
||||
if (node.isExisting)
|
||||
return null;
|
||||
|
||||
const nameHash = calcStringMD5(node.label);
|
||||
uci.set(uciconfig, nameHash, 'node');
|
||||
map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v]));
|
||||
|
||||
added++;
|
||||
log(sprintf('Adding node: %s.', node.label));
|
||||
});
|
||||
uci.commit(uciconfig);
|
||||
|
||||
let need_restart = (via_proxy !== '1');
|
||||
if (!isEmpty(main_node)) {
|
||||
const first_server = uci.get_first(uciconfig, ucinode);
|
||||
if (first_server) {
|
||||
if (!uci.get(uciconfig, main_node)) {
|
||||
uci.set(uciconfig, ucimain, 'main_node', first_server);
|
||||
uci.commit(uciconfig);
|
||||
need_restart = true;
|
||||
|
||||
log('Main node is gone, switching to the first node.');
|
||||
}
|
||||
|
||||
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
|
||||
if (!uci.get(uciconfig, main_udp_node)) {
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', first_server);
|
||||
uci.commit(uciconfig);
|
||||
need_restart = true;
|
||||
|
||||
log('Main UDP node is gone, switching to the first node.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uci.set(uciconfig, ucimain, 'main_node', 'nil');
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
|
||||
uci.commit(uciconfig);
|
||||
need_restart = true;
|
||||
|
||||
log('No available node, disable tproxy.');
|
||||
}
|
||||
}
|
||||
|
||||
if (need_restart) {
|
||||
log('Restarting service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
|
||||
log(sprintf('%s nodes added, %s removed.', added, removed));
|
||||
log('Successfully updated subscriptions.');
|
||||
}
|
||||
|
||||
if (!isEmpty(subscription_urls))
|
||||
try {
|
||||
call(main);
|
||||
} catch(e) {
|
||||
log('[FATAL ERROR] An error occurred during updating subscriptions:');
|
||||
log(sprintf('%s: %s', e.type, e.message));
|
||||
log(e.stacktrace[0].context);
|
||||
|
||||
log('Restarting service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
378
luci-app-homeproxy/root/etc/init.d/homeproxy
Executable file
378
luci-app-homeproxy/root/etc/init.d/homeproxy
Executable file
@ -0,0 +1,378 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
CONF="homeproxy"
|
||||
PROG="/usr/bin/sing-box"
|
||||
|
||||
HP_DIR="/etc/homeproxy"
|
||||
RUN_DIR="/var/run/homeproxy"
|
||||
LOG_PATH="$RUN_DIR/homeproxy.log"
|
||||
DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local routing_mode proxy_mode
|
||||
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
|
||||
config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
|
||||
|
||||
local outbound_node
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
config_get outbound_node "config" "main_node" "nil"
|
||||
else
|
||||
config_get outbound_node "routing" "default_outbound" "nil"
|
||||
fi
|
||||
|
||||
local server_enabled
|
||||
config_get_bool server_enabled "server" "enabled" "0"
|
||||
|
||||
if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
# Generate/Validate client config
|
||||
ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"
|
||||
|
||||
if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then
|
||||
log "Error: failed to generate client configuration."
|
||||
return 1
|
||||
elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then
|
||||
log "Error: wrong client configuration detected."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Auto update
|
||||
local auto_update auto_update_time
|
||||
config_get_bool auto_update "subscription" "auto_update" "0"
|
||||
if [ "$auto_update" = "1" ]; then
|
||||
config_get auto_update_time "subscription" "auto_update_time" "2"
|
||||
echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh" >> "/etc/crontabs/root"
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
|
||||
# DNSMasq rules
|
||||
local ipv6_support
|
||||
config_get_bool ipv6_support "config" "ipv6_support" "0"
|
||||
local dns_port china_dns_server china_dns_port
|
||||
config_get dns_port "infra" "dns_port" "5333"
|
||||
mkdir -p "$DNSMASQ_DIR"
|
||||
echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
|
||||
case "$routing_mode" in
|
||||
"gfwlist")
|
||||
[ "$ipv6_support" -eq "0" ] || local gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6"
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$gfw_nftset_v6/g" \
|
||||
"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
|
||||
;;
|
||||
"bypass_mainland_china")
|
||||
config_get china_dns_server "config" "china_dns_server"
|
||||
config_get china_dns_port "infra" "china_dns_port" "5334"
|
||||
|
||||
if [ -e "/usr/bin/chinadns-ng" ] && [ -n "$china_dns_server" ]; then
|
||||
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
no-poll
|
||||
no-resolv
|
||||
server=127.0.0.1#$china_dns_port
|
||||
EOF
|
||||
else
|
||||
china_dns_server=""
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
|
||||
"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
|
||||
fi
|
||||
;;
|
||||
"proxy_mainland_china")
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
|
||||
"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
|
||||
;;
|
||||
"custom"|"global")
|
||||
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
no-poll
|
||||
no-resolv
|
||||
server=127.0.0.1#$dns_port
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then
|
||||
[ "$ipv6_support" -eq "0" ] || local wan_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6"
|
||||
sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \
|
||||
"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
|
||||
fi
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
# Setup routing table
|
||||
local table_mark
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
case "$proxy_mode" in
|
||||
"redirect_tproxy")
|
||||
local outbound_udp_node
|
||||
config_get outbound_udp_node "config" "main_udp_node" "nil"
|
||||
if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then
|
||||
local tproxy_mark
|
||||
config_get tproxy_mark "infra" "tproxy_mark" "101"
|
||||
|
||||
ip rule add fwmark "$tproxy_mark" table "$table_mark"
|
||||
ip route add local 0.0.0.0/0 dev lo table "$table_mark"
|
||||
|
||||
if [ "$ipv6_support" -eq "1" ]; then
|
||||
ip -6 rule add fwmark "$tproxy_mark" table "$table_mark"
|
||||
ip -6 route add local ::/0 dev lo table "$table_mark"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"redirect_tun"|"tun")
|
||||
local tun_name tun_mark
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
config_get tun_mark "infra" "tun_mark" "102"
|
||||
|
||||
ip tuntap add mode tun user root name "$tun_name"
|
||||
sleep 1s
|
||||
ip link set "$tun_name" up
|
||||
|
||||
ip route replace default dev "$tun_name" table "$table_mark"
|
||||
ip rule add fwmark "$tun_mark" lookup "$table_mark"
|
||||
|
||||
ip -6 route replace default dev "$tun_name" table "$table_mark"
|
||||
ip -6 rule add fwmark "$tun_mark" lookup "$table_mark"
|
||||
;;
|
||||
esac
|
||||
|
||||
# sing-box (client)
|
||||
procd_open_instance "sing-box-c"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box-c.json"
|
||||
|
||||
if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then
|
||||
procd_add_jail "sing-box-c" log procfs
|
||||
procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
|
||||
procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
|
||||
procd_add_jail_mount "$HP_DIR/certs/"
|
||||
procd_add_jail_mount "/etc/ssl/"
|
||||
procd_add_jail_mount "/etc/localtime"
|
||||
procd_add_jail_mount "/etc/TZ"
|
||||
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
|
||||
procd_set_param no_new_privs 1
|
||||
procd_set_param user sing-box
|
||||
procd_set_param group sing-box
|
||||
fi
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
|
||||
# chinadns-ng
|
||||
if [ -n "$china_dns_server" ]; then
|
||||
local wandns="$(ifstatus wan | jsonfilter -e '@["dns-server"][0]' || echo "119.29.29.29")"
|
||||
china_dns_server="${china_dns_server/wan/$wandns}"
|
||||
china_dns_server="${china_dns_server// /,}"
|
||||
|
||||
for i in $(seq 1 "$(grep -c "processor" "/proc/cpuinfo")"); do
|
||||
procd_open_instance "chinadns-ng-$i"
|
||||
|
||||
procd_set_param command "/usr/bin/chinadns-ng"
|
||||
procd_append_param command --bind-port "$china_dns_port"
|
||||
procd_append_param command --china-dns "$china_dns_server"
|
||||
procd_append_param command --trust-dns "127.0.0.1#$dns_port"
|
||||
procd_append_param command --ipset-name4 "inet@fw4@homeproxy_mainland_addr_v4"
|
||||
procd_append_param command --ipset-name6 "inet@fw4@homeproxy_mainland_addr_v6"
|
||||
procd_append_param command --chnlist-file "$HP_DIR/resources/china_list.txt"
|
||||
procd_append_param command --gfwlist-file "$HP_DIR/resources/gfw_list.txt"
|
||||
[ "$ipv6_support" -eq "1" ] || procd_append_param command --no-ipv6=tC
|
||||
procd_append_param command --reuse-port
|
||||
|
||||
if chinadns-ng --version | grep -q "target:"; then
|
||||
procd_append_param command --cache 10000
|
||||
procd_append_param command --cache-stale 3600
|
||||
procd_append_param command --verdict-cache 10000
|
||||
fi
|
||||
|
||||
if [ -x "/sbin/ujail" ]; then
|
||||
procd_add_jail "chinadns-ng" log
|
||||
procd_add_jail_mount "$HP_DIR/resources/china_list.txt"
|
||||
procd_add_jail_mount "$HP_DIR/resources/gfw_list.txt"
|
||||
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
|
||||
procd_set_param no_new_privs 1
|
||||
procd_set_param user sing-box
|
||||
procd_set_param group sing-box
|
||||
fi
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
# Generate/Validate server config
|
||||
ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"
|
||||
|
||||
if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then
|
||||
log "Error: failed to generate server configuration."
|
||||
return 1
|
||||
elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then
|
||||
log "Error: wrong server configuration detected."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# sing-box (server)
|
||||
procd_open_instance "sing-box-s"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box-s.json"
|
||||
|
||||
if [ -x "/sbin/ujail" ]; then
|
||||
procd_add_jail "sing-box-s" log procfs
|
||||
procd_add_jail_mount "$RUN_DIR/sing-box-s.json"
|
||||
procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log"
|
||||
procd_add_jail_mount "$HP_DIR/certs/"
|
||||
procd_add_jail_mount "/etc/localtime"
|
||||
procd_add_jail_mount "/etc/TZ"
|
||||
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
|
||||
procd_set_param no_new_privs 1
|
||||
procd_set_param user sing-box
|
||||
procd_set_param group sing-box
|
||||
fi
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
fi
|
||||
|
||||
# log-cleaner
|
||||
procd_open_instance "log-cleaner"
|
||||
procd_set_param command "$HP_DIR/scripts/clean_log.sh"
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
|
||||
# Prepare ruleset directory for custom routing mode
|
||||
if [ "$routing_mode" = "custom" ]; then
|
||||
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
|
||||
fi
|
||||
|
||||
# Update permissions for ujail
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
echo > "$RUN_DIR/sing-box-c.log"
|
||||
chown sing-box:sing-box "$RUN_DIR/sing-box-c.log"
|
||||
chown sing-box:sing-box "$RUN_DIR/sing-box-c.json"
|
||||
chmod 0644 "$HP_DIR/resources/gfw_list.txt"
|
||||
fi
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
echo > "$RUN_DIR/sing-box-s.log"
|
||||
chown sing-box:sing-box "$RUN_DIR/sing-box-s.log"
|
||||
chown sing-box:sing-box "$RUN_DIR/sing-box-s.json"
|
||||
fi
|
||||
|
||||
# Setup firewall
|
||||
utpl -S "$HP_DIR/scripts/firewall_pre.ut" > "$RUN_DIR/fw4_pre.nft"
|
||||
[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
|
||||
log "$(sing-box version | awk 'NR==1{print $1,$3}') started."
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
sed -i "/$CONF/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1
|
||||
|
||||
# Setup firewall
|
||||
# Load config
|
||||
config_load "$CONF"
|
||||
local table_mark tproxy_mark tun_mark tun_name
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
config_get tproxy_mark "infra" "tproxy_mark" "101"
|
||||
config_get tun_mark "infra" "tun_mark" "102"
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
|
||||
# Tproxy
|
||||
ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
|
||||
ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null"
|
||||
ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
|
||||
ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"
|
||||
|
||||
# TUN
|
||||
ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
|
||||
ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
|
||||
|
||||
ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
|
||||
ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
|
||||
|
||||
# Nftables rules
|
||||
for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \
|
||||
"homeproxy_redirect" "homeproxy_redirect_proxy" \
|
||||
"homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \
|
||||
"homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
|
||||
"homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \
|
||||
"homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \
|
||||
"homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do
|
||||
nft flush chain inet fw4 "$i"
|
||||
nft delete chain inet fw4 "$i"
|
||||
done 2>"/dev/null"
|
||||
for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \
|
||||
"homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \
|
||||
"homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \
|
||||
"homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \
|
||||
"homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \
|
||||
"homeproxy_routing_port"; do
|
||||
nft flush set inet fw4 "$i"
|
||||
nft delete set inet fw4 "$i"
|
||||
done 2>"/dev/null"
|
||||
echo > "$RUN_DIR/fw4_pre.nft" 2>"/dev/null"
|
||||
echo > "$RUN_DIR/fw4_post.nft" 2>"/dev/null"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
|
||||
# Remove DNS hijack
|
||||
rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \
|
||||
"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log"
|
||||
|
||||
log "Service stopped."
|
||||
}
|
||||
|
||||
service_stopped() {
|
||||
# Load config
|
||||
config_load "$CONF"
|
||||
local tun_name
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
|
||||
# TUN
|
||||
ip link set "$tun_name" down 2>"/dev/null"
|
||||
ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
log "Reloading service..."
|
||||
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload
|
||||
}
|
18
luci-app-homeproxy/root/etc/uci-defaults/luci-homeproxy
Normal file
18
luci-app-homeproxy/root/etc/uci-defaults/luci-homeproxy
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >"/dev/null"
|
||||
delete firewall.homeproxy_pre
|
||||
set firewall.homeproxy_pre=include
|
||||
set firewall.homeproxy_pre.type=nftables
|
||||
set firewall.homeproxy_pre.path="/var/run/homeproxy/fw4_pre.nft"
|
||||
set firewall.homeproxy_pre.position="table-pre"
|
||||
|
||||
delete firewall.homeproxy_post
|
||||
set firewall.homeproxy_post=include
|
||||
set firewall.homeproxy_post.type=nftables
|
||||
set firewall.homeproxy_post.path="/var/run/homeproxy/fw4_post.nft"
|
||||
set firewall.homeproxy_post.position="table-post"
|
||||
commit firewall
|
||||
EOF
|
||||
|
||||
exit 0
|
@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
china_dns_server="$(uci -q get "homeproxy.config.china_dns_server")"
|
||||
if [ "$china_dns_server" = "wan_114" ]; then
|
||||
uci -q delete "homeproxy.config.china_dns_server"
|
||||
uci -q add_list "homeproxy.config.china_dns_server"="wan"
|
||||
uci -q add_list "homeproxy.config.china_dns_server"="114.114.114.114"
|
||||
elif echo "$china_dns_server" | grep -q ","; then
|
||||
uci -q delete "homeproxy.config.china_dns_server"
|
||||
for dns in ${china_dns_server//,/ }; do
|
||||
uci -q add_list "homeproxy.config.china_dns_server"="$dns"
|
||||
done
|
||||
fi
|
||||
[ -z "$(uci -q changes "homeproxy")" ] || uci -q commit "homeproxy"
|
||||
|
||||
exit 0
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"admin/services/homeproxy": {
|
||||
"title": "HomeProxy",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-homeproxy" ],
|
||||
"uci": { "homeproxy": true }
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/client": {
|
||||
"title": "Client Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/client"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/node": {
|
||||
"title": "Node Settings",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/node"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/server": {
|
||||
"title": "Server Settings",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/server"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/status": {
|
||||
"title": "Service Status",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/status"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
{
|
||||
"luci-app-homeproxy": {
|
||||
"description": "Grant access to homeproxy configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
|
||||
"/var/run/homeproxy/homeproxy.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box-c.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box-s.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ],
|
||||
"luci.homeproxy": [ "*" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
}
|
||||
}
|
||||
}
|
207
luci-app-homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy
Executable file
207
luci-app-homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy
Executable file
@ -0,0 +1,207 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { access, error, lstat, mkstemp, popen, readfile, writefile } from 'fs';
|
||||
|
||||
/* Kanged from ucode/luci */
|
||||
function shellquote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function hasKernelModule(kmod) {
|
||||
return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0);
|
||||
}
|
||||
|
||||
const HP_DIR = '/etc/homeproxy';
|
||||
const RUN_DIR = '/var/run/homeproxy';
|
||||
|
||||
const methods = {
|
||||
acllist_read: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
|
||||
return { content: null, error: 'illegal type' };
|
||||
|
||||
const filecontent = readfile(`${HP_DIR}/resources/${req.args?.type}.txt`);
|
||||
return { content: filecontent };
|
||||
}
|
||||
},
|
||||
acllist_write: {
|
||||
args: { type: 'type', content: 'content' },
|
||||
call: function(req) {
|
||||
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
|
||||
return { result: false, error: 'illegal type' };
|
||||
|
||||
const file = `${HP_DIR}/resources/${req.args?.type}.txt`;
|
||||
let content = req.args?.content;
|
||||
|
||||
/* Sanitize content */
|
||||
if (content) {
|
||||
content = trim(content);
|
||||
content = replace(content, /\r\n?/g, '\n');
|
||||
if (!match(content, /\n$/))
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
system(`mkdir -p ${HP_DIR}/resources`);
|
||||
writefile(file, content);
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
},
|
||||
|
||||
certificate_write: {
|
||||
args: { filename: 'filename' },
|
||||
call: function(req) {
|
||||
const writeCertificate = function(filename, priv) {
|
||||
const tmpcert = '/tmp/homeproxy_certificate.tmp';
|
||||
const filestat = lstat(tmpcert);
|
||||
|
||||
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'empty certificate file' };
|
||||
}
|
||||
|
||||
let filecontent = readfile(tmpcert);
|
||||
if (is_binary(filecontent)) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'illegal file type: binary' };
|
||||
}
|
||||
|
||||
/* Kanged from luci-proto-openconnect */
|
||||
const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
|
||||
end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
|
||||
lines = split(trim(filecontent), /[\r\n]/);
|
||||
let start = false, i;
|
||||
|
||||
for (i = 0; i < length(lines); i++) {
|
||||
if (match(lines[i], beg))
|
||||
start = true;
|
||||
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'this does not look like a correct PEM file' };
|
||||
}
|
||||
|
||||
/* Sanitize certificate */
|
||||
filecontent = trim(filecontent);
|
||||
filecontent = replace(filecontent, /\r\n?/g, '\n');
|
||||
if (!match(filecontent, /\n$/))
|
||||
filecontent += '\n';
|
||||
|
||||
system(`mkdir -p ${HP_DIR}/certs`);
|
||||
writefile(`${HP_DIR}/certs/${filename}.pem`, filecontent);
|
||||
system(`rm -f ${tmpcert}`);
|
||||
|
||||
return { result: true };
|
||||
};
|
||||
|
||||
const filename = req.args?.filename;
|
||||
switch (filename) {
|
||||
case 'client_ca':
|
||||
case 'server_publickey':
|
||||
return writeCertificate(filename, false);
|
||||
break;
|
||||
case 'server_privatekey':
|
||||
return writeCertificate(filename, true);
|
||||
break;
|
||||
default:
|
||||
return { result: false, error: 'illegal cerificate filename' };
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
connection_check: {
|
||||
args: { site: 'site' },
|
||||
call: function(req) {
|
||||
let url;
|
||||
switch(req.args?.site) {
|
||||
case 'baidu':
|
||||
url = 'https://www.baidu.com';
|
||||
break;
|
||||
case 'google':
|
||||
url = 'https://www.google.com';
|
||||
break;
|
||||
default:
|
||||
return { result: false, error: 'illegal site' };
|
||||
break;
|
||||
}
|
||||
|
||||
return { result: (system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100) === 0) };
|
||||
}
|
||||
},
|
||||
|
||||
log_clean: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
if (!(req.args?.type in ['homeproxy', 'sing-box-c', 'sing-box-s']))
|
||||
return { result: false, error: 'illegal type' };
|
||||
|
||||
const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`);
|
||||
if (filestat)
|
||||
writefile(`${RUN_DIR}/${req.args?.type}.log`, '');
|
||||
return { result: true };
|
||||
}
|
||||
},
|
||||
|
||||
singbox_get_features: {
|
||||
call: function() {
|
||||
let features = {};
|
||||
|
||||
const fd = popen('/usr/bin/sing-box version');
|
||||
if (fd) {
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
||||
let tags = match(trim(line), /Tags: (.*)/);
|
||||
if (!tags)
|
||||
continue;
|
||||
|
||||
for (let i in split(tags[1], ','))
|
||||
features[i] = true;
|
||||
}
|
||||
|
||||
fd.close();
|
||||
}
|
||||
|
||||
features.hp_has_chinadns_ng = access('/usr/bin/chinadns-ng');
|
||||
if (features.hp_has_chinadns_ng)
|
||||
features.hp_has_chinadns_ng_v2 = (system('/usr/bin/chinadns-ng --version | grep -q "target:"') === 0);
|
||||
features.hp_has_ip_full = access('/usr/libexec/ip-full');
|
||||
features.hp_has_tcp_brutal = hasKernelModule('brutal.ko');
|
||||
features.hp_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy');
|
||||
features.hp_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun');
|
||||
|
||||
return features;
|
||||
}
|
||||
},
|
||||
|
||||
resources_get_version: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
const version = trim(readfile(`${HP_DIR}/resources/${req.args?.type}.ver`));
|
||||
return { version: version, error: error() };
|
||||
}
|
||||
},
|
||||
resources_update: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
if (req.args?.type) {
|
||||
const type = shellquote(req.args?.type);
|
||||
const exit_code = system(`${HP_DIR}/scripts/update_resources.sh ${type}`);
|
||||
return { status: exit_code };
|
||||
} else
|
||||
return { status: 255, error: 'illegal type' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.homeproxy': methods };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user