🗽 Sync 2022-02-07 20:36:22

This commit is contained in:
github-actions[bot] 2022-02-07 20:36:22 +08:00
parent 5edeea1757
commit 98fb59de1b
309 changed files with 82326 additions and 0 deletions

77
aliyundrive-fuse/Makefile Normal file
View File

@ -0,0 +1,77 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=aliyundrive-fuse
PKG_VERSION:=0.1.6
PKG_RELEASE:=
PKG_LICENSE:=MIT
PKG_MAINTAINER:=messense <messense@icloud.com>
PKG_LIBC:=musl
ifeq ($(ARCH),arm)
PKG_LIBC:=musleabi
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifneq ($(filter $(ARM_CPU_FEATURES),vfp vfpv2),)
PKG_LIBC:=musleabihf
endif
endif
PKG_ARCH=$(ARCH)
ifeq ($(ARCH),i386)
PKG_ARCH:=i686
endif
PKG_SOURCE:=aliyundrive-fuse-v$(PKG_VERSION).$(PKG_ARCH)-unknown-linux-$(PKG_LIBC).tar.gz
PKG_SOURCE_URL:=https://github.com/messense/aliyundrive-fuse/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=multimedia
CATEGORY:=Multimedia
DEPENDS:=+fuse-utils
TITLE:=FUSE for AliyunDrive
URL:=https://github.com/messense/aliyundrive-fuse
endef
define Package/$(PKG_NAME)/description
FUSE for AliyunDrive.
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/aliyundrive-fuse
endef
define Download/sha256sum
FILE:=$(PKG_SOURCE).sha256
URL_FILE:=$(FILE)
URL:=$(PKG_SOURCE_URL)
HASH:=skip
endef
$(eval $(call Download,sha256sum))
define Build/Prepare
mv $(DL_DIR)/$(PKG_SOURCE).sha256 .
cp $(DL_DIR)/$(PKG_SOURCE) .
shasum -a 256 -c $(PKG_SOURCE).sha256
rm $(PKG_SOURCE).sha256 $(PKG_SOURCE)
tar -C $(PKG_BUILD_DIR)/ -zxf $(DL_DIR)/$(PKG_SOURCE)
endef
define Build/Compile
echo "$(PKG_NAME) using precompiled binary."
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/aliyundrive-fuse $(1)/usr/bin/aliyundrive-fuse
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/aliyundrive-fuse.init $(1)/etc/init.d/aliyundrive-fuse
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/aliyundrive-fuse.config $(1)/etc/config/aliyundrive-fuse
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -0,0 +1,7 @@
config default
option enable '0'
option debug '0'
option refresh_token ''
option domain_id ''
option mount_point '/mnt/aliyundrive'
option read_buffer_size '10485760'

View File

@ -0,0 +1,48 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
STOP=15
NAME=aliyundrive-fuse
uci_get_by_type() {
local ret=$(uci get $NAME.@$1[0].$2 2>/dev/null)
echo ${ret:=$3}
}
start_service() {
local enable=$(uci_get_by_type default enable)
case "$enable" in
1|on|true|yes|enabled)
local refresh_token=$(uci_get_by_type default refresh_token)
local domain_id=$(uci_get_by_type default domain_id)
local mount_point=$(uci_get_by_type default mount_point)
local read_buf_size=$(uci_get_by_type default read_buffer_size 10485760)
local extra_options=""
if [[ ! -z "$domain_id" ]]; then
extra_options="$extra_options --domain-id $domain_id"
fi
mkdir -p "$mount_point"
procd_open_instance
procd_set_param command /bin/sh -c "/usr/bin/$NAME $extra_options -S $read_buf_size --workdir /var/run/$NAME $mount_point >>/var/log/$NAME.log 2>&1"
procd_set_param pidfile /var/run/$NAME.pid
procd_set_param env REFRESH_TOKEN="$refresh_token"
case $(uci_get_by_type default debug) in
1|on|true|yes|enabled)
procd_append_param env RUST_LOG="aliyundrive_fuse=debug" ;;
*) ;;
esac
procd_close_instance ;;
*)
stop_service ;;
esac
}
service_triggers() {
procd_add_reload_trigger "aliyundrive-fuse"
}

View File

@ -0,0 +1,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-aliyundrive-fuse
PKG_VERSION:=0.1.6
PKG_RELEASE:=
PKG_PO_VERSION:=$(PKG_VERSION)-$(PKG_RELEASE)
PKG_LICENSE:=MIT
PKG_MAINTAINER:=messense <messense@icloud.com>
LUCI_TITLE:=LuCI Support for aliyundrive-fuse
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+aliyundrive-fuse +lua +libuci-lua
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,35 @@
module("luci.controller.aliyundrive-fuse", package.seeall)
function index()
if not nixio.fs.access("/etc/config/aliyundrive-fuse") then
return
end
entry({"admin", "services", "aliyundrive-fuse"}, alias("admin", "services", "aliyundrive-fuse", "client"),_("AliyunDrive FUSE"), 10).dependent = true -- 首页
entry({"admin", "services", "aliyundrive-fuse", "client"}, cbi("aliyundrive-fuse/client"),_("Settings"), 10).leaf = true -- 客户端配置
entry({"admin", "services", "aliyundrive-fuse", "log"}, form("aliyundrive-fuse/log"),_("Log"), 30).leaf = true -- 日志页面
entry({"admin", "services", "aliyundrive-fuse", "status"}, call("action_status")).leaf = true
entry({"admin", "services", "aliyundrive-fuse", "logtail"}, call("action_logtail")).leaf = true
end
function action_status()
local e = {}
e.running = luci.sys.call("pidof aliyundrive-fuse >/dev/null") == 0
e.application = luci.sys.exec("aliyundrive-fuse --version")
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function action_logtail()
local fs = require "nixio.fs"
local log_path = "/var/log/aliyundrive-fuse.log"
local e = {}
e.running = luci.sys.call("pidof aliyundrive-fuse >/dev/null") == 0
if fs.access(log_path) then
e.log = luci.sys.exec("tail -n 100 %s | sed 's/\\x1b\\[[0-9;]*m//g'" % log_path)
else
e.log = ""
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end

View File

@ -0,0 +1,32 @@
local uci = luci.model.uci.cursor()
local m, e
m = Map("aliyundrive-fuse")
m.title = translate("AliyunDrive FUSE")
m.description = translate("<a href=\"https://github.com/messense/aliyundrive-fuse\" target=\"_blank\">Project GitHub URL</a>")
m:section(SimpleSection).template = "aliyundrive-fuse/aliyundrive-fuse_status"
e = m:section(TypedSection, "default")
e.anonymous = true
enable = e:option(Flag, "enable", translate("Enable"))
enable.rmempty = false
refresh_token = e:option(Value, "refresh_token", translate("Refresh Token"))
refresh_token.description = translate("<a href=\"https://github.com/messense/aliyundrive-webdav#%E8%8E%B7%E5%8F%96-refresh_token\" target=\"_blank\">How to get refresh token</a>")
mount_point = e:option(Value, "mount_point", translate("Mount Point"))
mount_point.default = "/mnt/aliyundrive"
read_buffer_size = e:option(Value, "read_buffer_size", translate("Read Buffer Size"))
read_buffer_size.default = "10485760"
read_buffer_size.datatype = "uinteger"
domain_id = e:option(Value, "domain_id", translate("Domain ID"))
domain_id.description = translate("Input domain_id option will use <a href=\"https://www.aliyun.com/product/storage/pds\" target=\"_blank\">Aliyun PDS</a> instead of <a href=\"https://www.aliyundrive.com\" target=\"_blank\">AliyunDrive</a>")
debug = e:option(Flag, "debug", translate("Debug Mode"))
debug.rmempty = false
return m

View File

@ -0,0 +1,9 @@
log = SimpleForm("logview")
log.submit = false
log.reset = false
t = log:field(DummyValue, '', '')
t.rawhtml = true
t.template = 'aliyundrive-fuse/aliyundrive-fuse_log'
return log

View File

@ -0,0 +1,15 @@
<%+cbi/valueheader%>
<textarea id="logview" class="cbi-input-textarea" style="width: 100%" rows="30" readonly="readonly"></textarea>
<script type="text/javascript">
const LOG_URL = '<%=luci.dispatcher.build_url("admin", "services", "aliyundrive-fuse", "logtail")%>';
XHR.poll(1, LOG_URL, null, (x, d) => {
let logview = document.getElementById("logview");
if (!d.running) {
XHR.halt();
}
logview.value = d.log;
logview.scrollTop = logview.scrollHeight;
});
</script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,21 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[services]], [[aliyundrive-fuse]], [[status]])%>', null,
function(x, data) {
var tb = document.getElementById('aliyundrive-fuse_status');
if (data && tb) {
if (data.running) {
tb.innerHTML = '<em><b style=color:green>' + data.application + '<%:RUNNING%></b></em>';
} else {
tb.innerHTML = '<em><b style=color:red>' + data.application + '<%:NOT RUNNING%></b></em>';
}
}
}
);
//]]>
</script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="aliyundrive-fuse_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>

View File

@ -0,0 +1,50 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "AliyunDrive"
msgstr "阿里云盘"
msgid "AliyunDrive FUSE"
msgstr "阿里云盘 FUSE"
msgid "Enable"
msgstr "启用"
msgid "Refresh Token"
msgstr "Refresh Token"
msgid "Mount Point"
msgstr "挂载点"
msgid "Read Buffer Size"
msgstr "下载缓冲大小(bytes)"
msgid "Collecting data..."
msgstr "获取数据中..."
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Settings"
msgstr "设置"
msgid "Log"
msgstr "日志"
msgid "Debug Mode"
msgstr "调试模式"
msgid "<a href=\"https://github.com/messense/aliyundrive-fuse\" target=\"_blank\">Project GitHub URL</a>"
msgstr "<a href=\"https://github.com/messense/aliyundrive-fuse\" target=\"_blank\">GitHub 项目地址</a>"
msgid "<a href=\"https://github.com/messense/aliyundrive-webdav#%E8%8E%B7%E5%8F%96-refresh_token\" target=\"_blank\">How to get refresh token</a>"
msgstr "<a href=\"https://github.com/messense/aliyundrive-webdav#%E8%8E%B7%E5%8F%96-refresh_token\" target=\"_blank\">查看获取 refresh token 的方法</a>"
msgid "Domain ID"
msgstr "阿里云相册与云盘服务 domainId"
msgid "Input domain_id option will use <a href=\"https://www.aliyun.com/product/storage/pds\" target=\"_blank\">Aliyun PDS</a> instead of <a href=\"https://www.aliyundrive.com\" target=\"_blank\">AliyunDrive</a>"
msgstr "填写此选项将使用<a href=\"https://www.aliyun.com/product/storage/pds\" target=\"_blank\">阿里云相册与网盘服务</a>而不是<a href=\"https://www.aliyundrive.com\" target=\"_blank\">阿里云盘</a>"

View File

@ -0,0 +1 @@
zh-cn

View File

@ -0,0 +1,11 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@aliyundrive-fuse[-1]
add ucitrack aliyundrive-fuse
set ucitrack.@aliyundrive-fuse[-1].init=aliyundrive-fuse
commit ucitrack
EOF
rm -f /tmp/luci-indexcache
exit 0

View File

@ -0,0 +1,21 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Support for docker
LUCI_DEPENDS:=@(aarch64||arm||x86_64) \
+luci-compat \
+luci-lib-docker \
+luci-lib-ip \
+docker \
+dockerd \
+ttyd
LUCI_PKGARCH:=all
PKG_LICENSE:=AGPL-3.0
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com> \
Florian Eckert <fe@dev.tdt.de>
PKG_VERSION:=v0.5.25
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1 @@
ttyd docker-cli

View File

@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>Docker icon</title>
<path d="M4.82 17.275c-.684 0-1.304-.56-1.304-1.24s.56-1.243 1.305-1.243c.748 0 1.31.56 1.31 1.242s-.622 1.24-1.305 1.24zm16.012-6.763c-.135-.992-.75-1.8-1.56-2.42l-.315-.25-.254.31c-.494.56-.69 1.553-.63 2.295.06.562.24 1.12.554 1.554-.254.13-.568.25-.81.377-.57.187-1.124.25-1.68.25H.097l-.06.37c-.12 1.182.06 2.42.562 3.54l.244.435v.06c1.5 2.483 4.17 3.6 7.078 3.6 5.594 0 10.182-2.42 12.357-7.633 1.425.062 2.864-.31 3.54-1.676l.18-.31-.3-.187c-.81-.494-1.92-.56-2.85-.31l-.018.002zm-8.008-.992h-2.428v2.42h2.43V9.518l-.002.003zm0-3.043h-2.428v2.42h2.43V6.48l-.002-.003zm0-3.104h-2.428v2.42h2.43v-2.42h-.002zm2.97 6.147H13.38v2.42h2.42V9.518l-.007.003zm-8.998 0H4.383v2.42h2.422V9.518l-.01.003zm3.03 0h-2.4v2.42H9.84V9.518l-.015.003zm-6.03 0H1.4v2.42h2.428V9.518l-.03.003zm6.03-3.043h-2.4v2.42H9.84V6.48l-.015-.003zm-3.045 0H4.387v2.42H6.8V6.48l-.016-.003z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,91 @@
.fb-container {
margin-top: 1rem;
}
.fb-container .cbi-button {
height: 1.8rem;
}
.fb-container .cbi-input-text {
margin-bottom: 1rem;
width: 100%;
}
.fb-container .panel-title {
padding-bottom: 0;
width: 50%;
border-bottom: none;
}
.fb-container .panel-container {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.fb-container .upload-container {
display: none;
margin: 1rem 0;
}
.fb-container .upload-file {
margin-right: 2rem;
}
.fb-container .cbi-value-field {
text-align: left;
}
.fb-container .parent-icon strong {
margin-left: 1rem;
}
.fb-container td[class$="-icon"] {
cursor: pointer;
}
.fb-container .file-icon, .fb-container .folder-icon, .fb-container .link-icon {
position: relative;
}
.fb-container .file-icon:before, .fb-container .folder-icon:before, .fb-container .link-icon:before {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
content: '';
background-size: contain;
margin: 0 0.5rem 0 1rem;
vertical-align: middle;
}
.fb-container .file-icon:before {
background-image: url(file-icon.png);
}
.fb-container .folder-icon:before {
background-image: url(folder-icon.png);
}
.fb-container .link-icon:before {
background-image: url(link-icon.png);
}
@media screen and (max-width: 480px) {
.fb-container .upload-file {
width: 14.6rem;
}
.fb-container .cbi-value-owner,
.fb-container .cbi-value-perm {
display: none;
}
}
.cbi-section-table {
width: 100%;
}
.cbi-section-table-cell {
text-align: right;
}
.cbi-button-install {
border-color: #c44;
color: #c44;
margin-left: 3px;
}
.cbi-value-field {
padding: 10px 0;
}
.parent-icon {
height: 1.8rem;
padding: 10px 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" id="icon-hub" viewBox="0 -4 42 50" stroke-width="2" fill-rule="nonzero" width="100%" height="100%">
<path d="M37.176371,36.2324812 C37.1920117,36.8041095 36.7372743,37.270685 36.1684891,37.270685 L3.74335204,37.2703476 C3.17827583,37.2703476 2.72400056,36.8091818 2.72400056,36.2397767 L2.72400056,19.6131383 C1.4312007,18.4881431 0.662551336,16.8884326 0.662551336,15.1618249 L0.664207893,14.69503 C0.63774183,14.4532127 0.650524255,14.2942438 0.711604827,14.1238231 L5.10793246,1.20935468 C5.24853286,0.797020623 5.63848594,0.511627907 6.06681069,0.511627907 L34.0728364,0.511627907 C34.5091607,0.511627907 34.889927,0.793578201 35.0316653,1.20921034 L39.4428567,14.1234095 C39.4871296,14.273204 39.5020782,14.4249444 39.4884726,14.5493649 L39.4884726,15.1505835 C39.4884726,16.9959517 38.6190601,18.6883031 37.1764746,19.7563084 L37.176371,36.2324812 Z M35.1376208,35.209311 L35.1376208,20.7057152 C34.7023924,20.8097593 34.271333,20.8633641 33.8336069,20.8633641 C32.0046019,20.8633641 30.3013756,19.9547008 29.2437221,18.4771538 C28.1860473,19.954695 26.4828515,20.8633641 24.6538444,20.8633641 C22.824803,20.8633641 21.1216155,19.9547157 20.0639591,18.4771544 C19.0062842,19.9546953 17.3030887,20.8633641 15.4740818,20.8633641 C13.6450404,20.8633641 11.9418529,19.9547157 10.8841965,18.4771544 C9.82652161,19.9546953 8.12332608,20.8633641 6.29431919,20.8633641 C5.76735555,20.8633641 5.24095778,20.7883418 4.73973398,20.644674 L4.73973398,35.209311 L35.1376208,35.209311 Z M30.2720226,15.6557626 C30.5154632,17.4501192 32.0503909,18.8018554 33.845083,18.8018554 C35.7286794,18.8018554 37.285413,17.3395134 37.4474599,15.4751932 L30.2280765,15.4751932 C30.2470638,15.532987 30.2617919,15.5932958 30.2720226,15.6557626 Z M21.0484306,15.4751932 C21.0674179,15.532987 21.0821459,15.5932958 21.0923767,15.6557626 C21.3358173,17.4501192 22.8707449,18.8018554 24.665437,18.8018554 C26.4601001,18.8018554 27.9950169,17.4501481 28.2378191,15.6611556 C28.2451225,15.5981318 28.2590045,15.5358056 28.2787375,15.4751932 L21.0484306,15.4751932 Z M11.9238102,15.6557626 C12.1672508,17.4501192 13.7021785,18.8018554 15.4968705,18.8018554 C17.2915336,18.8018554 18.8264505,17.4501481 19.0692526,15.6611556 C19.0765561,15.5981318 19.0904381,15.5358056 19.110171,15.4751932 L11.8798641,15.4751932 C11.8988514,15.532987 11.9135795,15.5932958 11.9238102,15.6557626 Z M6.31682805,18.8018317 C8.11149114,18.8018317 9.64640798,17.4501244 9.88921012,15.6611319 C9.89651357,15.5981081 9.91039559,15.5357819 9.93012856,15.4751696 L2.70318796,15.4751696 C2.86612006,17.3346852 4.42809696,18.8018317 6.31682805,18.8018317 Z M3.09670082,13.4139924 L37.04257,13.4139924 L33.3489482,2.57204736 L6.80119239,2.57204736 L3.09670082,13.4139924 Z"
id="Fill-1"></path>
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect>
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 48.723 48.723" xml:space="preserve">
<path d="M7.452,24.152h3.435v5.701h0.633c0.001,0,0.001,0,0.002,0h0.636v-5.701h3.51v-1.059h17.124v1.104h3.178v5.656h0.619 c0,0,0,0,0.002,0h0.619v-5.656h3.736v-0.856c0-0.012,0.006-0.021,0.006-0.032c0-0.072,0-0.143,0-0.215h5.721v-1.316h-5.721 c0-0.054,0-0.108,0-0.164c0-0.011-0.006-0.021-0.006-0.032v-0.832h-8.154v1.028h-7.911v-2.652h-0.689c-0.001,0-0.001,0-0.002,0 h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" />
<path d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" />
<rect x="18.682" y="16.898" width="10.809" height="0.537" />
<path d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" />
<rect x="4.095" y="46.631" width="10.808" height="0.537" />
<path d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" />
<rect x="33.584" y="46.338" width="10.809" height="0.537" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,185 @@
// https://github.com/thiscouldbebetter/TarFileExplorer
class TarFileTypeFlag
{constructor(value,name)
{this.value=value;this.id="_"+this.value;this.name=name;}
static _instances;static Instances()
{if(TarFileTypeFlag._instances==null)
{TarFileTypeFlag._instances=new TarFileTypeFlag_Instances();}
return TarFileTypeFlag._instances;}}
class TarFileTypeFlag_Instances
{constructor()
{this.Normal=new TarFileTypeFlag("0","Normal");this.HardLink=new TarFileTypeFlag("1","Hard Link");this.SymbolicLink=new TarFileTypeFlag("2","Symbolic Link");this.CharacterSpecial=new TarFileTypeFlag("3","Character Special");this.BlockSpecial=new TarFileTypeFlag("4","Block Special");this.Directory=new TarFileTypeFlag("5","Directory");this.FIFO=new TarFileTypeFlag("6","FIFO");this.ContiguousFile=new TarFileTypeFlag("7","Contiguous File");this.LongFilePath=new TarFileTypeFlag("L","././@LongLink");this._All=[this.Normal,this.HardLink,this.SymbolicLink,this.CharacterSpecial,this.BlockSpecial,this.Directory,this.FIFO,this.ContiguousFile,this.LongFilePath,];for(var i=0;i<this._All.length;i++)
{var item=this._All[i];this._All[item.id]=item;}}}
class TarFileEntryHeader
{constructor
(fileName,fileMode,userIDOfOwner,userIDOfGroup,fileSizeInBytes,timeModifiedInUnixFormat,checksum,typeFlag,nameOfLinkedFile,uStarIndicator,uStarVersion,userNameOfOwner,groupNameOfOwner,deviceNumberMajor,deviceNumberMinor,filenamePrefix)
{this.fileName=fileName;this.fileMode=fileMode;this.userIDOfOwner=userIDOfOwner;this.userIDOfGroup=userIDOfGroup;this.fileSizeInBytes=fileSizeInBytes;this.timeModifiedInUnixFormat=timeModifiedInUnixFormat;this.checksum=checksum;this.typeFlag=typeFlag;this.nameOfLinkedFile=nameOfLinkedFile;this.uStarIndicator=uStarIndicator;this.uStarVersion=uStarVersion;this.userNameOfOwner=userNameOfOwner;this.groupNameOfOwner=groupNameOfOwner;this.deviceNumberMajor=deviceNumberMajor;this.deviceNumberMinor=deviceNumberMinor;this.filenamePrefix=filenamePrefix;}
static FileNameMaxLength=99;static SizeInBytes=500;static default()
{var now=new Date();var unixEpoch=new Date(1970,1,1);var millisecondsSinceUnixEpoch=now-unixEpoch;var secondsSinceUnixEpoch=Math.floor
(millisecondsSinceUnixEpoch/1000);var secondsSinceUnixEpochAsStringOctal=secondsSinceUnixEpoch.toString(8).padRight(12,"\0");var timeModifiedInUnixFormat=[];for(var i=0;i<secondsSinceUnixEpochAsStringOctal.length;i++)
{var digitAsASCIICode=secondsSinceUnixEpochAsStringOctal.charCodeAt(i);timeModifiedInUnixFormat.push(digitAsASCIICode);}
var returnValue=new TarFileEntryHeader
("".padRight(100,"\0"),"0100777","0000000","0000000",0,timeModifiedInUnixFormat,0,TarFileTypeFlag.Instances().Normal,"","ustar","00","","","","","");return returnValue;};static directoryNew(directoryName)
{var header=TarFileEntryHeader.default();header.fileName=directoryName;header.typeFlag=TarFileTypeFlag.Instances().Directory;header.fileSizeInBytes=0;header.checksumCalculate();return header;};static fileNew(fileName,fileContentsAsBytes)
{var header=TarFileEntryHeader.default();header.fileName=fileName;header.typeFlag=TarFileTypeFlag.Instances().Normal;header.fileSizeInBytes=fileContentsAsBytes.length;header.checksumCalculate();return header;};static fromBytes(bytes)
{var reader=new ByteStream(bytes);var fileName=reader.readString(100).trim();var fileMode=reader.readString(8);var userIDOfOwner=reader.readString(8);var userIDOfGroup=reader.readString(8);var fileSizeInBytesAsStringOctal=reader.readString(12);var timeModifiedInUnixFormat=reader.readBytes(12);var checksumAsStringOctal=reader.readString(8);var typeFlagValue=reader.readString(1);var nameOfLinkedFile=reader.readString(100);var uStarIndicator=reader.readString(6);var uStarVersion=reader.readString(2);var userNameOfOwner=reader.readString(32);var groupNameOfOwner=reader.readString(32);var deviceNumberMajor=reader.readString(8);var deviceNumberMinor=reader.readString(8);var filenamePrefix=reader.readString(155);var reserved=reader.readBytes(12);var fileSizeInBytes=parseInt
(fileSizeInBytesAsStringOctal.trim(),8);var checksum=parseInt
(checksumAsStringOctal,8);var typeFlags=TarFileTypeFlag.Instances()._All;var typeFlagID="_"+typeFlagValue;var typeFlag=typeFlags[typeFlagID];var returnValue=new TarFileEntryHeader
(fileName,fileMode,userIDOfOwner,userIDOfGroup,fileSizeInBytes,timeModifiedInUnixFormat,checksum,typeFlag,nameOfLinkedFile,uStarIndicator,uStarVersion,userNameOfOwner,groupNameOfOwner,deviceNumberMajor,deviceNumberMinor,filenamePrefix);return returnValue;};checksumCalculate()
{var thisAsBytes=this.toBytes();var offsetOfChecksumInBytes=148;var numberOfBytesInChecksum=8;var presumedValueOfEachChecksumByte=" ".charCodeAt(0);for(var i=0;i<numberOfBytesInChecksum;i++)
{var offsetOfByte=offsetOfChecksumInBytes+i;thisAsBytes[offsetOfByte]=presumedValueOfEachChecksumByte;}
var checksumSoFar=0;for(var i=0;i<thisAsBytes.length;i++)
{var byteToAdd=thisAsBytes[i];checksumSoFar+=byteToAdd;}
this.checksum=checksumSoFar;return this.checksum;};toBytes()
{var headerAsBytes=[];var writer=new ByteStream(headerAsBytes);var fileSizeInBytesAsStringOctal=(this.fileSizeInBytes.toString(8)+"\0").padLeft(12,"0")
var checksumAsStringOctal=(this.checksum.toString(8)+"\0 ").padLeft(8,"0");writer.writeString(this.fileName,100);writer.writeString(this.fileMode,8);writer.writeString(this.userIDOfOwner,8);writer.writeString(this.userIDOfGroup,8);writer.writeString(fileSizeInBytesAsStringOctal,12);writer.writeBytes(this.timeModifiedInUnixFormat);writer.writeString(checksumAsStringOctal,8);writer.writeString(this.typeFlag.value,1);writer.writeString(this.nameOfLinkedFile,100);writer.writeString(this.uStarIndicator,6);writer.writeString(this.uStarVersion,2);writer.writeString(this.userNameOfOwner,32);writer.writeString(this.groupNameOfOwner,32);writer.writeString(this.deviceNumberMajor,8);writer.writeString(this.deviceNumberMinor,8);writer.writeString(this.filenamePrefix,155);writer.writeString("".padRight(12,"\0"));return headerAsBytes;};toString()
{var newline="\n";var returnValue="[TarFileEntryHeader "
+"fileName='"+this.fileName+"' "
+"typeFlag='"+(this.typeFlag==null?"err":this.typeFlag.name)+"' "
+"fileSizeInBytes='"+this.fileSizeInBytes+"' "
+"]"
+newline;return returnValue;};}
class TarFileEntry
{constructor(header,dataAsBytes)
{this.header=header;this.dataAsBytes=dataAsBytes;}
static directoryNew(directoryName)
{var header=TarFileEntryHeader.directoryNew(directoryName);var entry=new TarFileEntry(header,[]);return entry;};static fileNew(fileName,fileContentsAsBytes)
{var header=TarFileEntryHeader.fileNew(fileName,fileContentsAsBytes);var entry=new TarFileEntry(header,fileContentsAsBytes);return entry;};static fromBytes(chunkAsBytes,reader)
{var chunkSize=TarFile.ChunkSize;var header=TarFileEntryHeader.fromBytes
(chunkAsBytes);var sizeOfDataEntryInBytesUnpadded=header.fileSizeInBytes;var numberOfChunksOccupiedByDataEntry=Math.ceil
(sizeOfDataEntryInBytesUnpadded/chunkSize)
var sizeOfDataEntryInBytesPadded=numberOfChunksOccupiedByDataEntry*chunkSize;var dataAsBytes=reader.readBytes
(sizeOfDataEntryInBytesPadded).slice
(0,sizeOfDataEntryInBytesUnpadded);var entry=new TarFileEntry(header,dataAsBytes);return entry;};static manyFromByteArrays
(fileNamePrefix,fileNameSuffix,entriesAsByteArrays)
{var returnValues=[];for(var i=0;i<entriesAsByteArrays.length;i++)
{var entryAsBytes=entriesAsByteArrays[i];var entry=TarFileEntry.fileNew
(fileNamePrefix+i+fileNameSuffix,entryAsBytes);returnValues.push(entry);}
return returnValues;};download(event)
{FileHelper.saveBytesAsFile
(this.dataAsBytes,this.header.fileName);};remove(event)
{alert("Not yet implemented!");};toBytes()
{var entryAsBytes=[];var chunkSize=TarFile.ChunkSize;var headerAsBytes=this.header.toBytes();entryAsBytes=entryAsBytes.concat(headerAsBytes);entryAsBytes=entryAsBytes.concat(this.dataAsBytes);var sizeOfDataEntryInBytesUnpadded=this.header.fileSizeInBytes;var numberOfChunksOccupiedByDataEntry=Math.ceil
(sizeOfDataEntryInBytesUnpadded/chunkSize)
var sizeOfDataEntryInBytesPadded=numberOfChunksOccupiedByDataEntry*chunkSize;var numberOfBytesOfPadding=sizeOfDataEntryInBytesPadded-sizeOfDataEntryInBytesUnpadded;for(var i=0;i<numberOfBytesOfPadding;i++)
{entryAsBytes.push(0);}
return entryAsBytes;};toString()
{var newline="\n";headerAsString=this.header.toString();var dataAsHexadecimalString=ByteHelper.bytesToStringHexadecimal
(this.dataAsBytes);var returnValue="[TarFileEntry]"+newline
+headerAsString
+"[Data]"
+dataAsHexadecimalString
+"[/Data]"+newline
+"[/TarFileEntry]"
+newline;return returnValue}}
class TarFile
{constructor(fileName,entries)
{this.fileName=fileName;this.entries=entries;}
static ChunkSize=512;static fromBytes(fileName,bytes)
{var reader=new ByteStream(bytes);var entries=[];var chunkSize=TarFile.ChunkSize;var numberOfConsecutiveZeroChunks=0;while(reader.hasMoreBytes()==true)
{var chunkAsBytes=reader.readBytes(chunkSize);var areAllBytesInChunkZeroes=true;for(var b=0;b<chunkAsBytes.length;b++)
{if(chunkAsBytes[b]!=0)
{areAllBytesInChunkZeroes=false;break;}}
if(areAllBytesInChunkZeroes==true)
{numberOfConsecutiveZeroChunks++;if(numberOfConsecutiveZeroChunks==2)
{break;}}
else
{numberOfConsecutiveZeroChunks=0;var entry=TarFileEntry.fromBytes(chunkAsBytes,reader);entries.push(entry);}}
var returnValue=new TarFile(fileName,entries);returnValue.consolidateLongPathEntries();return returnValue;}
static create(fileName)
{return new TarFile
(fileName,[]);}
consolidateLongPathEntries()
{var typeFlagLongPathName=TarFileTypeFlag.Instances().LongFilePath.name;var entries=this.entries;for(var i=0;i<entries.length;i++)
{var entry=entries[i];if(entry.header.typeFlag.name==typeFlagLongPathName)
{var entryNext=entries[i+1];entryNext.header.fileName=entry.dataAsBytes.reduce
((a,b)=>a+=String.fromCharCode(b),"");entryNext.header.fileName=entryNext.header.fileName.replace(/\0/g,"");entries.splice(i,1);i--;}}}
downloadAs(fileNameToSaveAs)
{return FileHelper.saveBytesAsFile
(this.toBytes(),fileNameToSaveAs)}
entriesForDirectories()
{return this.entries.filter(x=>x.header.typeFlag.name==TarFileTypeFlag.Instances().Directory);}
toBytes()
{this.toBytes_PrependLongPathEntriesAsNeeded();var fileAsBytes=[];var entriesAsByteArrays=this.entries.map(x=>x.toBytes());this.consolidateLongPathEntries();for(var i=0;i<entriesAsByteArrays.length;i++)
{var entryAsBytes=entriesAsByteArrays[i];fileAsBytes=fileAsBytes.concat(entryAsBytes);}
var chunkSize=TarFile.ChunkSize;var numberOfZeroChunksToWrite=2;for(var i=0;i<numberOfZeroChunksToWrite;i++)
{for(var b=0;b<chunkSize;b++)
{fileAsBytes.push(0);}}
return fileAsBytes;}
toBytes_PrependLongPathEntriesAsNeeded()
{var typeFlagLongPath=TarFileTypeFlag.Instances().LongFilePath;var maxLength=TarFileEntryHeader.FileNameMaxLength;var entries=this.entries;for(var i=0;i<entries.length;i++)
{var entry=entries[i];var entryHeader=entry.header;var entryFileName=entryHeader.fileName;if(entryFileName.length>maxLength)
{var entryFileNameAsBytes=entryFileName.split("").map(x=>x.charCodeAt(0));var entryContainingLongPathToPrepend=TarFileEntry.fileNew
(typeFlagLongPath.name,entryFileNameAsBytes);entryContainingLongPathToPrepend.header.typeFlag=typeFlagLongPath;entryContainingLongPathToPrepend.header.timeModifiedInUnixFormat=entryHeader.timeModifiedInUnixFormat;entryContainingLongPathToPrepend.header.checksumCalculate();entryHeader.fileName=entryFileName.substr(0,maxLength)+String.fromCharCode(0);entries.splice(i,0,entryContainingLongPathToPrepend);i++;}}}
toString()
{var newline="\n";var returnValue="[TarFile]"+newline;for(var i=0;i<this.entries.length;i++)
{var entry=this.entries[i];var entryAsString=entry.toString();returnValue+=entryAsString;}
returnValue+="[/TarFile]"+newline;return returnValue;}}
function StringExtensions()
{}
{String.prototype.padLeft=function(lengthToPadTo,charToPadWith)
{var returnValue=this;while(returnValue.length<lengthToPadTo)
{returnValue=charToPadWith+returnValue;}
return returnValue;}
String.prototype.padRight=function(lengthToPadTo,charToPadWith)
{var returnValue=this;while(returnValue.length<lengthToPadTo)
{returnValue+=charToPadWith;}
return returnValue;}}
class Globals
{static Instance=new Globals();}
class FileHelper
{static loadFileAsBytes(fileToLoad,callback)
{var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var fileLoadedAsBinaryString=fileLoadedEvent.target.result;var fileLoadedAsBytes=ByteHelper.stringUTF8ToBytes(fileLoadedAsBinaryString);callback(fileToLoad.name,fileLoadedAsBytes);}
fileReader.readAsBinaryString(fileToLoad);}
static loadFileAsText(fileToLoad,callback)
{var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var textFromFileLoaded=fileLoadedEvent.target.result;callback(fileToLoad.name,textFromFileLoaded);};fileReader.readAsText(fileToLoad);}
static saveBytesAsFile(bytesToWrite,fileNameToSaveAs)
{var bytesToWriteAsArrayBuffer=new ArrayBuffer(bytesToWrite.length);var bytesToWriteAsUIntArray=new Uint8Array(bytesToWriteAsArrayBuffer);for(var i=0;i<bytesToWrite.length;i++)
{bytesToWriteAsUIntArray[i]=bytesToWrite[i];}
var bytesToWriteAsBlob=new Blob
([bytesToWriteAsArrayBuffer],{type:"application/type"});
return bytesToWriteAsBlob
// var downloadLink=document.createElement("a");downloadLink.download=fileNameToSaveAs;downloadLink.href=window.URL.createObjectURL(bytesToWriteAsBlob);downloadLink.click();
}
static saveTextAsFile(textToSave,fileNameToSaveAs)
{var textToSaveAsBlob=new Blob([textToSave],{type:"text/plain"});var textToSaveAsURL=window.URL.createObjectURL(textToSaveAsBlob);var downloadLink=document.createElement("a");downloadLink.download=fileNameToSaveAs;downloadLink.href=textToSaveAsURL;downloadLink.click();}}
class ByteStream
{constructor(bytes)
{this.bytes=bytes;this.byteIndexCurrent=0;}
static BitsPerByte=8;static BitsPerByteTimesTwo=ByteStream.BitsPerByte*2;static BitsPerByteTimesThree=ByteStream.BitsPerByte*3;hasMoreBytes()
{return(this.byteIndexCurrent<this.bytes.length);}
readBytes(numberOfBytesToRead)
{var returnValue=new Array(numberOfBytesToRead);for(var b=0;b<numberOfBytesToRead;b++)
{returnValue[b]=this.readByte();}
return returnValue;}
readByte()
{var returnValue=this.bytes[this.byteIndexCurrent];this.byteIndexCurrent++;return returnValue;}
readString(lengthOfString)
{var returnValue="";for(var i=0;i<lengthOfString;i++)
{var byte=this.readByte();if(byte!=0)
{var byteAsChar=String.fromCharCode(byte);returnValue+=byteAsChar;}}
return returnValue;}
writeBytes(bytesToWrite)
{for(var b=0;b<bytesToWrite.length;b++)
{this.bytes.push(bytesToWrite[b]);}
this.byteIndexCurrent=this.bytes.length;}
writeByte(byteToWrite)
{this.bytes.push(byteToWrite);this.byteIndexCurrent++;}
writeString(stringToWrite,lengthPadded)
{for(var i=0;i<stringToWrite.length;i++)
{var charAsByte=stringToWrite.charCodeAt(i);this.writeByte(charAsByte);}
var numberOfPaddingChars=lengthPadded-stringToWrite.length;for(var i=0;i<numberOfPaddingChars;i++)
{this.writeByte(0);}}}
class ByteHelper
{static stringUTF8ToBytes(stringToConvert)
{var bytes=[];for(var i=0;i<stringToConvert.length;i++)
{var byte=stringToConvert.charCodeAt(i);bytes.push(byte);}
return bytes;}
static bytesToStringUTF8(bytesToConvert)
{var returnValue="";for(var i=0;i<bytesToConvert.length;i++)
{var byte=bytesToConvert[i];var byteAsChar=String.fromCharCode(byte);returnValue+=byteAsChar}
return returnValue;}}
function ArrayExtensions()
{}
{Array.prototype.remove=function(elementToRemove)
{this.splice(this.indexOf(elementToRemove),1);}}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,614 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
-- local uci = (require "luci.model.uci").cursor()
module("luci.controller.dockerman",package.seeall)
function index()
entry({"admin", "docker"},
alias("admin", "docker", "config"),
_("Docker"),
40).acl_depends = { "luci-app-dockerman" }
entry({"admin", "docker", "config"},cbi("dockerman/configuration"),_("Configuration"), 8).leaf=true
-- local uci = (require "luci.model.uci").cursor()
-- if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
-- local host = uci:get("dockerd", "dockerman", "remote_host")
-- local port = uci:get("dockerd", "dockerman", "remote_port")
-- if not host or not port then
-- return
-- end
-- else
-- local socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
-- if socket and not nixio.fs.access(socket) then
-- return
-- end
-- end
-- if (require "luci.model.docker").new():_ping().code ~= 200 then
-- return
-- end
entry({"admin", "docker", "overview"}, form("dockerman/overview"),_("Overview"), 2).leaf=true
entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"), 3).leaf=true
entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"), 4).leaf=true
entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"), 5).leaf=true
entry({"admin", "docker", "volumes"}, form("dockerman/volumes"), _("Volumes"), 6).leaf=true
entry({"admin", "docker", "events"}, call("action_events"), _("Events"), 7)
entry({"admin", "docker", "newcontainer"}, form("dockerman/newcontainer")).leaf=true
entry({"admin", "docker", "newnetwork"}, form("dockerman/newnetwork")).leaf=true
entry({"admin", "docker", "container"}, form("dockerman/container")).leaf=true
entry({"admin", "docker", "container_stats"}, call("action_get_container_stats")).leaf=true
entry({"admin", "docker", "containers_stats"}, call("action_get_containers_stats")).leaf=true
entry({"admin", "docker", "get_system_df"}, call("action_get_system_df")).leaf=true
entry({"admin", "docker", "container_get_archive"}, call("download_archive")).leaf=true
entry({"admin", "docker", "container_put_archive"}, call("upload_archive")).leaf=true
entry({"admin", "docker", "container_list_file"}, call("list_file")).leaf=true
entry({"admin", "docker", "container_remove_file"}, call("remove_file")).leaf=true
entry({"admin", "docker", "container_rename_file"}, call("rename_file")).leaf=true
entry({"admin", "docker", "container_export"}, call("export_container")).leaf=true
entry({"admin", "docker", "images_save"}, call("save_images")).leaf=true
entry({"admin", "docker", "images_load"}, call("load_images")).leaf=true
entry({"admin", "docker", "images_import"}, call("import_images")).leaf=true
entry({"admin", "docker", "images_get_tags"}, call("get_image_tags")).leaf=true
entry({"admin", "docker", "images_tag"}, call("tag_image")).leaf=true
entry({"admin", "docker", "images_untag"}, call("untag_image")).leaf=true
entry({"admin", "docker", "confirm"}, call("action_confirm")).leaf=true
end
function action_get_system_df()
local res = docker.new():df()
luci.http.status(res.code, res.message)
luci.http.prepare_content("application/json")
luci.http.write_json(res.body)
end
function scandir(id, directory)
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
if not cmd_docker or cmd_docker:match("^%s+$") then
return
end
local i, t, popen = 0, {}, io.popen
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket_path then
hosts = "unix://" .. socket_path
else
return
end
local pfile = popen(cmd_docker .. ' -H "'.. hosts ..'" exec ' ..id .." ls -lh \""..directory.."\" | egrep -v '^total'")
for fileinfo in pfile:lines() do
i = i + 1
t[i] = fileinfo
end
pfile:close()
return t
end
function list_response(id, path, success)
luci.http.prepare_content("application/json")
local result
if success then
local rv = scandir(id, path)
result = {
ec = 0,
data = rv
}
else
result = {
ec = 1
}
end
luci.http.write_json(result)
end
function list_file(id)
local path = luci.http.formvalue("path")
list_response(id, path, true)
end
function rename_file(id)
local filepath = luci.http.formvalue("filepath")
local newpath = luci.http.formvalue("newpath")
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
if not cmd_docker or cmd_docker:match("^%s+$") then
return
end
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket_path then
hosts = "unix://" .. socket_path
else
return
end
local success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' mv "'..filepath..'" "'..newpath..'"')
list_response(nixio.fs.dirname(filepath), success)
end
function remove_file(id)
local path = luci.http.formvalue("path")
local isdir = luci.http.formvalue("isdir")
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
if not cmd_docker or cmd_docker:match("^%s+$") then
return
end
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket_path then
hosts = "unix://" .. socket_path
else
return
end
path = path:gsub("<>", "/")
path = path:gsub(" ", "\ ")
local success
if isdir then
success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' rm -r "'..path..'"')
else
success = os.remove(path)
end
list_response(nixio.fs.dirname(path), success)
end
function action_events()
local logs = ""
local query ={}
local dk = docker.new()
query["until"] = os.time()
local events = dk:events({query = query})
if events.code == 200 then
for _, v in ipairs(events.body) do
local date = "unknown"
if v and v.time then
date = os.date("%Y-%m-%d %H:%M:%S", v.time)
end
local name = v.Actor.Attributes.name or "unknown"
local action = v.Action or "unknown"
if v and v.Type == "container" then
local id = v.Actor.ID or "unknown"
logs = logs .. string.format("[%s] %s %s Container ID: %s Container Name: %s\n", date, v.Type, action, id, name)
elseif v.Type == "network" then
local container = v.Actor.Attributes.container or "unknown"
local network = v.Actor.Attributes.type or "unknown"
logs = logs .. string.format("[%s] %s %s Container ID: %s Network Name: %s Network type: %s\n", date, v.Type, action, container, name, network)
elseif v.Type == "image" then
local id = v.Actor.ID or "unknown"
logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name)
end
end
end
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
end
local calculate_cpu_percent = function(d)
if type(d) ~= "table" then
return
end
local cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
local cpu_percent = 0.0
local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
local system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) -- tonumber(d["precpu_stats"]["system_cpu_usage"])
if system_delta > 0.0 then
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
end
return cpu_percent
end
local get_memory = function(d)
if type(d) ~= "table" then
return
end
-- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024)
-- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024)
-- return usage .. "MB / " .. limit.. "MB"
local limit =tonumber(d["memory_stats"]["limit"])
local usage = tonumber(d["memory_stats"]["usage"])
-- - tonumber(d["memory_stats"]["stats"]["total_cache"])
return usage, limit
end
local get_rx_tx = function(d)
if type(d) ~="table" then
return
end
local data = {}
if type(d["networks"]) == "table" then
for e, v in pairs(d["networks"]) do
data[e] = {
bw_tx = tonumber(v.tx_bytes),
bw_rx = tonumber(v.rx_bytes)
}
end
end
return data
end
local function get_stat(container_id)
if container_id then
local dk = docker.new()
local response = dk.containers:inspect({id = container_id})
if response.code == 200 and response.body.State.Running then
response = dk.containers:stats({id = container_id, query = {stream = false, ["one-shot"] = true}})
if response.code == 200 then
local container_stats = response.body
local cpu_percent = calculate_cpu_percent(container_stats)
local mem_useage, mem_limit = get_memory(container_stats)
local bw_rxtx = get_rx_tx(container_stats)
return response.code, response.body.message, {
cpu_percent = cpu_percent,
memory = {
mem_useage = mem_useage,
mem_limit = mem_limit
},
bw_rxtx = bw_rxtx
}
else
return response.code, response.body.message
end
else
if response.code == 200 then
return 500, "container "..container_id.." not running"
else
return response.code, response.body.message
end
end
else
return 404, "No container name or id"
end
end
function action_get_container_stats(container_id)
local code, msg, res = get_stat(container_id)
luci.http.status(code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json(res)
end
function action_get_containers_stats()
local res = luci.http.formvalue(containers) or ""
local stats = {}
res = luci.jsonc.parse(res.containers)
if res and type(res) == "table" then
for i, v in ipairs(res) do
_,_,stats[v] = get_stat(v)
end
end
luci.http.status(200, "OK")
luci.http.prepare_content("application/json")
luci.http.write_json(stats)
end
function action_confirm()
local data = docker:read_status()
if data then
data = data:gsub("\n","<br>"):gsub(" ","&nbsp;")
code = 202
msg = data
else
code = 200
msg = "finish"
data = "finish"
end
luci.http.status(code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({info = data})
end
function export_container(id)
local dk = docker.new()
local first
local cb = function(res, chunk)
if res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="'.. id ..'.tar"')
luci.http.header('Content-Type', 'application\/x-tar')
end
luci.ltn12.pump.all(chunk, luci.http.write)
else
if not first then
first = true
luci.http.prepare_content("text/plain")
end
luci.ltn12.pump.all(chunk, luci.http.write)
end
end
local res = dk.containers:export({id = id}, cb)
end
function download_archive()
local id = luci.http.formvalue("id")
local path = luci.http.formvalue("path")
local filename = luci.http.formvalue("filename") or "archive"
local dk = docker.new()
local first
local cb = function(res, chunk)
if res and res.code and res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="'.. filename .. '.tar"')
luci.http.header('Content-Type', 'application\/x-tar')
end
luci.ltn12.pump.all(chunk, luci.http.write)
else
if not first then
first = true
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("text/plain")
end
luci.ltn12.pump.all(chunk, luci.http.write)
end
end
local res = dk.containers:get_archive({
id = id,
query = {
path = luci.http.urlencode(path)
}
}, cb)
end
function upload_archive(container_id)
local path = luci.http.formvalue("upload-path")
local dk = docker.new()
local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end
end)
end
local res = dk.containers:put_archive({
id = container_id,
query = {
path = luci.http.urlencode(path)
},
body = rec_send
})
local msg = res and res.message or res.body and res.body.message or nil
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
-- function save_images()
-- local names = luci.http.formvalue("names")
-- local dk = docker.new()
-- local first
-- local cb = function(res, chunk)
-- if res.code == 200 then
-- if not first then
-- first = true
-- luci.http.status(res.code, res.message)
-- luci.http.header('Content-Disposition', 'inline; filename="'.. "images" ..'.tar"')
-- luci.http.header('Content-Type', 'application\/x-tar')
-- end
-- luci.ltn12.pump.all(chunk, luci.http.write)
-- else
-- if not first then
-- first = true
-- luci.http.prepare_content("text/plain")
-- end
-- luci.ltn12.pump.all(chunk, luci.http.write)
-- end
-- end
-- docker:write_status("Images: saving" .. " " .. names .. "...")
-- local res = dk.images:get({
-- query = {
-- names = luci.http.urlencode(names)
-- }
-- }, cb)
-- docker:clear_status()
-- local msg = res and res.body and res.body.message or nil
-- luci.http.status(res.code, msg)
-- luci.http.prepare_content("application/json")
-- luci.http.write_json({message = msg})
-- end
function load_images()
local archive = luci.http.formvalue("upload-archive")
local dk = docker.new()
local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end
end)
end
docker:write_status("Images: loading...")
local res = dk.images:load({body = rec_send})
local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error ) or nil
if res and res.code == 200 and msg and msg:match("Loaded image ID") then
docker:clear_status()
else
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
end
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
function import_images()
local src = luci.http.formvalue("src")
local itag = luci.http.formvalue("tag")
local dk = docker.new()
local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end
end)
end
docker:write_status("Images: importing".. " ".. itag .."...\n")
local repo = itag and itag:match("^([^:]+)")
local tag = itag and itag:match("^[^:]-:([^:]+)")
local res = dk.images:create({
query = {
fromSrc = luci.http.urlencode(src or "-"),
repo = repo or nil,
tag = tag or nil
},
body = not src and rec_send or nil
}, docker.import_image_show_status_cb)
local msg = res and res.body and ( res.body.message )or nil
if not msg and #res.body == 0 then
msg = res.body.status or res.body.error
elseif not msg and #res.body >= 1 then
msg = res.body[#res.body].status or res.body[#res.body].error
end
if res.code == 200 and msg and msg:match("sha256:") then
docker:clear_status()
else
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
end
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
function get_image_tags(image_id)
if not image_id then
luci.http.status(400, "no image id")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "no image id"})
return
end
local dk = docker.new()
local res = dk.images:inspect({
id = image_id
})
local msg = res and res.body and res.body.message or nil
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
if res.code == 200 then
local tags = res.body.RepoTags
luci.http.write_json({tags = tags})
else
local msg = res and res.body and res.body.message or nil
luci.http.write_json({message = msg or "unknow"})
end
end
function tag_image(image_id)
local src = luci.http.formvalue("tag")
local image_id = image_id or luci.http.formvalue("id")
if type(src) ~= "string" or not image_id then
luci.http.status(400, "no image id or tag")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "no image id or tag"})
return
end
local repo = src:match("^([^:]+)")
local tag = src:match("^[^:]-:([^:]+)")
local dk = docker.new()
local res = dk.images:tag({
id = image_id,
query={
repo=repo,
tag=tag
}
})
local msg = res and res.body and res.body.message or nil
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
if res.code == 201 then
local tags = res.body.RepoTags
luci.http.write_json({tags = tags})
else
local msg = res and res.body and res.body.message or nil
luci.http.write_json({message = msg or "unknow"})
end
end
function untag_image(tag)
local tag = tag or luci.http.formvalue("tag")
if not tag then
luci.http.status(400, "no tag name")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "no tag name"})
return
end
local dk = docker.new()
local res = dk.images:inspect({name = tag})
if res.code == 200 then
local tags = res.body.RepoTags
if #tags > 1 then
local r = dk.images:remove({name = tag})
local msg = r and r.body and r.body.message or nil
luci.http.status(r.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
else
luci.http.status(500, "Cannot remove the last tag")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "Cannot remove the last tag"})
end
else
local msg = res and res.body and res.body.message or nil
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
end

View File

@ -0,0 +1,152 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2021 Florian Eckert <fe@dev.tdt.de>
Copyright 2021 lisaac <lisaac.cn@gmail.com>
]]--
local uci = (require "luci.model.uci").cursor()
local m, s, o
m = Map("dockerd",
translate("Docker - Configuration"),
translate("DockerMan is a simple docker manager client for LuCI"))
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
s = m:section(NamedSection, "globals", "section", translate("Docker Daemon settings"))
o = s:option(Flag, "auto_start", translate("Auto start"))
o.rmempty = false
o.write = function(self, section, value)
if value == "1" then
luci.util.exec("/etc/init.d/dockerd enable")
else
luci.util.exec("/etc/init.d/dockerd disable")
end
m.uci:set("dockerd", "globals", "auto_start", value)
end
o = s:option(Value, "data_root",
translate("Docker Root Dir"))
o.placeholder = "/opt/docker/"
o:depends("remote_endpoint", 0)
o = s:option(Value, "bip",
translate("Default bridge"),
translate("Configure the default bridge network"))
o.placeholder = "172.17.0.1/16"
o.datatype = "ipaddr"
o:depends("remote_endpoint", 0)
o = s:option(DynamicList, "registry_mirrors",
translate("Registry Mirrors"),
translate("It replaces the daemon registry mirrors with a new set of registry mirrors"))
o:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
o:depends("remote_endpoint", 0)
o.forcewrite = true
o = s:option(ListValue, "log_level",
translate("Log Level"),
translate('Set the logging level'))
o:value("debug", translate("Debug"))
o:value("", translate("Info")) -- This is the default debug level from the deamon is optin is not set
o:value("warn", translate("Warning"))
o:value("error", translate("Error"))
o:value("fatal", translate("Fatal"))
o.rmempty = true
o:depends("remote_endpoint", 0)
o = s:option(DynamicList, "hosts",
translate("Client connection"),
translate('Specifies where the Docker daemon will listen for client connections (default: unix:///var/run/docker.sock)'))
o:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock")
o:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
o.rmempty = true
o:depends("remote_endpoint", 0)
end
s = m:section(NamedSection, "dockerman", "section", translate("DockerMan settings"))
s:tab("ac", translate("Access Control"))
s:tab("dockerman", translate("DockerMan"))
o = s:taboption("dockerman", Flag, "remote_endpoint",
translate("Remote Endpoint"),
translate("Connect to remote docker endpoint"))
o.rmempty = false
o.validate = function(self, value, sid)
local res = luci.http.formvaluetable("cbid.dockerd")
if res["dockerman.remote_endpoint"] == "1" then
if res["dockerman.remote_port"] and res["dockerman.remote_port"] ~= "" and res["dockerman.remote_host"] and res["dockerman.remote_host"] ~= "" then
return 1
else
return nil, translate("Please input the PORT or HOST IP of remote docker instance!")
end
else
if not res["dockerman.socket_path"] then
return nil, translate("Please input the SOCKET PATH of docker daemon!")
end
end
return 0
end
o = s:taboption("dockerman", Value, "socket_path",
translate("Docker Socket Path"))
o.default = "/var/run/docker.sock"
o.placeholder = "/var/run/docker.sock"
o:depends("remote_endpoint", 0)
o = s:taboption("dockerman", Value, "remote_host",
translate("Remote Host"),
translate("Host or IP Address for the connection to a remote docker instance"))
o.datatype = "host"
o.placeholder = "10.1.1.2"
o:depends("remote_endpoint", 1)
o = s:taboption("dockerman", Value, "remote_port",
translate("Remote Port"))
o.placeholder = "2375"
o.datatype = "port"
o:depends("remote_endpoint", 1)
-- o = s:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
-- o = s:taboption("dockerman", Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path"))
-- o.enabled="true"
-- o.disabled="false"
-- o = s:taboption("dockerman", Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile"))
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
o = s:taboption("ac", DynamicList, "ac_allowed_interface", translate("Allowed access interfaces"), translate("Which interface(s) can access containers under the bridge network, fill-in Interface Name"))
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
for i, v in ipairs(interfaces) do
o:value(v, v)
end
o = s:taboption("ac", DynamicList, "ac_allowed_ports", translate("Ports allowed to be accessed"), translate("Which Port(s) can be accessed, it's not restricted by the Allowed Access interfaces configuration. Use this configuration with caution!"))
o.placeholder = "8080/tcp"
local docker = require "luci.model.docker"
local containers, res, lost_state
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
else
lost_state = false
res = dk.containers:list()
if res and res.code and res.code < 300 then
containers = res.body
end
end
-- allowed_container.placeholder = "container name_or_id"
if containers then
for i, v in ipairs(containers) do
if v.State == "running" and v.Ports then
for _, port in ipairs(v.Ports) do
if port.PublicPort and port.IP and not string.find(port.IP,":") then
o:value(port.PublicPort.."/"..port.Type, v.Names[1]:sub(2) .. " | " .. port.PublicPort .. " | " .. port.Type)
end
end
end
end
end
end
return m

View File

@ -0,0 +1,810 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
require "luci.util"
local docker = require "luci.model.docker"
local dk = docker.new()
container_id = arg[1]
local action = arg[2] or "info"
local m, s, o
local images, networks, container_info, res
if not container_id then
return
end
res = dk.containers:inspect({id = container_id})
if res.code < 300 then
container_info = res.body
else
return
end
local get_ports = function(d)
local data
if d.HostConfig and d.HostConfig.PortBindings then
for inter, out in pairs(d.HostConfig.PortBindings) do
data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
end
end
return data
end
local get_env = function(d)
local data
if d.Config and d.Config.Env then
for _,v in ipairs(d.Config.Env) do
data = (data and (data .. "<br>") or "") .. v
end
end
return data
end
local get_command = function(d)
local data
if d.Config and d.Config.Cmd then
for _,v in ipairs(d.Config.Cmd) do
data = (data and (data .. " ") or "") .. v
end
end
return data
end
local get_mounts = function(d)
local data
if d.Mounts then
for _,v in ipairs(d.Mounts) do
local v_sorce_d, v_dest_d
local v_sorce = ""
local v_dest = ""
for v_sorce_d in v["Source"]:gmatch('[^/]+') do
if v_sorce_d and #v_sorce_d > 12 then
v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..."
else
v_sorce = v_sorce .."/".. v_sorce_d
end
end
for v_dest_d in v["Destination"]:gmatch('[^/]+') do
if v_dest_d and #v_dest_d > 12 then
v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..."
else
v_dest = v_dest .."/".. v_dest_d
end
end
data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "")
end
end
return data
end
local get_device = function(d)
local data
if d.HostConfig and d.HostConfig.Devices then
for _,v in ipairs(d.HostConfig.Devices) do
data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
end
end
return data
end
local get_links = function(d)
local data
if d.HostConfig and d.HostConfig.Links then
for _,v in ipairs(d.HostConfig.Links) do
data = (data and (data .. "<br>") or "") .. v
end
end
return data
end
local get_tmpfs = function(d)
local data
if d.HostConfig and d.HostConfig.Tmpfs then
for k, v in pairs(d.HostConfig.Tmpfs) do
data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
end
end
return data
end
local get_dns = function(d)
local data
if d.HostConfig and d.HostConfig.Dns then
for _, v in ipairs(d.HostConfig.Dns) do
data = (data and (data .. "<br>") or "") .. v
end
end
return data
end
local get_sysctl = function(d)
local data
if d.HostConfig and d.HostConfig.Sysctls then
for k, v in pairs(d.HostConfig.Sysctls) do
data = (data and (data .. "<br>") or "") .. k..":"..v
end
end
return data
end
local get_networks = function(d)
local data={}
if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then
for k,v in pairs(d.NetworkSettings.Networks) do
data[k] = v.IPAddress or ""
end
end
return data
end
local start_stop_remove = function(m, cmd)
local res
docker:clear_status()
docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
if cmd ~= "upgrade" then
res = dk.containers[cmd](dk, {id = container_id})
else
res = dk.containers_upgrade(dk, {id = container_id})
end
if res and res.code >= 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
else
docker:clear_status()
if cmd ~= "remove" and cmd ~= "upgrade" then
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
else
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
end
end
end
m=SimpleForm("docker",
translatef("Docker - Container (%s)", container_info.Name:sub(2)),
translate("On this page, the selected container can be managed."))
m.redirect = luci.dispatcher.build_url("admin/docker/containers")
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template = "cbi/nullsection"
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Start")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"start")
end
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"restart")
end
o = s:option(Button, "_stop")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Stop")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"stop")
end
o = s:option(Button, "_kill")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Kill")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"kill")
end
o = s:option(Button, "_export")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Export")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container_export/"..container_id))
end
o = s:option(Button, "_upgrade")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Upgrade")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"upgrade")
end
o = s:option(Button, "_duplicate")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Duplicate/Edit")
o.inputstyle = "add"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
end
o = s:option(Button, "_remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Remove")
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"remove")
end
s = m:section(SimpleSection)
s.template = "dockerman/container"
if action == "info" then
res = dk.networks:list()
if res.code < 300 then
networks = res.body
else
return
end
m.submit = false
m.reset = false
table_info = {
["01name"] = {
_key = translate("Name"),
_value = container_info.Name:sub(2) or "-",
_button=translate("Update")
},
["02id"] = {
_key = translate("ID"),
_value = container_info.Id or "-"
},
["03image"] = {
_key = translate("Image"),
_value = container_info.Config.Image .. "<br>" .. container_info.Image
},
["04status"] = {
_key = translate("Status"),
_value = container_info.State and container_info.State.Status or "-"
},
["05created"] = {
_key = translate("Created"),
_value = container_info.Created or "-"
},
}
if container_info.State.Status == "running" then
table_info["06start"] = {
_key = translate("Start Time"),
_value = container_info.State and container_info.State.StartedAt or "-"
}
else
table_info["06start"] = {
_key = translate("Finish Time"),
_value = container_info.State and container_info.State.FinishedAt or "-"
}
end
table_info["07healthy"] = {
_key = translate("Healthy"),
_value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"
}
table_info["08restart"] = {
_key = translate("Restart Policy"),
_value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-",
_button=translate("Update")
}
table_info["081user"] = {
_key = translate("User"),
_value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"
}
table_info["09mount"] = {
_key = translate("Mount/Volume"),
_value = get_mounts(container_info) or "-"
}
table_info["10cmd"] = {
_key = translate("Command"),
_value = get_command(container_info) or "-"
}
table_info["11env"] = {
_key = translate("Env"),
_value = get_env(container_info) or "-"
}
table_info["12ports"] = {
_key = translate("Ports"),
_value = get_ports(container_info) or "-"
}
table_info["13links"] = {
_key = translate("Links"),
_value = get_links(container_info) or "-"
}
table_info["14device"] = {
_key = translate("Device"),
_value = get_device(container_info) or "-"
}
table_info["15tmpfs"] = {
_key = translate("Tmpfs"),
_value = get_tmpfs(container_info) or "-"
}
table_info["16dns"] = {
_key = translate("DNS"),
_value = get_dns(container_info) or "-"
}
table_info["17sysctl"] = {
_key = translate("Sysctl"),
_value = get_sysctl(container_info) or "-"
}
info_networks = get_networks(container_info)
list_networks = {}
for _, v in ipairs (networks) do
if v and v.Name then
local parent = v.Options and v.Options.parent or nil
local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
list_networks[v.Name] = network_name
end
end
if type(info_networks)== "table" then
for k,v in pairs(info_networks) do
table_info["14network"..k] = {
_key = translate("Network"),
_value = k.. (v~="" and (" | ".. v) or ""),
_button=translate("Disconnect")
}
list_networks[k]=nil
end
end
table_info["15connect"] = {
_key = translate("Connect Network"),
_value = list_networks ,_opts = "",
_button=translate("Connect")
}
s = m:section(Table,table_info)
s.nodescr=true
s.formvalue=function(self, section)
return table_info
end
o = s:option(DummyValue, "_key", translate("Info"))
o.width = "20%"
o = s:option(ListValue, "_value")
o.render = function(self, section, scope)
if table_info[section]._key == translate("Name") then
self:reset_values()
self.template = "cbi/value"
self.size = 30
self.keylist = {}
self.vallist = {}
self.default=table_info[section]._value
Value.render(self, section, scope)
elseif table_info[section]._key == translate("Restart Policy") then
self.template = "cbi/lvalue"
self:reset_values()
self.size = nil
self:value("no", "No")
self:value("unless-stopped", "Unless stopped")
self:value("always", "Always")
self:value("on-failure", "On failure")
self.default=table_info[section]._value
ListValue.render(self, section, scope)
elseif table_info[section]._key == translate("Connect Network") then
self.template = "cbi/lvalue"
self:reset_values()
self.size = nil
for k,v in pairs(list_networks) do
if k ~= "host" then
self:value(k,v)
end
end
self.default=table_info[section]._value
ListValue.render(self, section, scope)
else
self:reset_values()
self.rawhtml=true
self.template = "cbi/dvalue"
self.default=table_info[section]._value
DummyValue.render(self, section, scope)
end
end
o.forcewrite = true
o.write = function(self, section, value)
table_info[section]._value=value
end
o.validate = function(self, value)
return value
end
o = s:option(Value, "_opts")
o.forcewrite = true
o.write = function(self, section, value)
table_info[section]._opts=value
end
o.validate = function(self, value)
return value
end
o.render = function(self, section, scope)
if table_info[section]._key==translate("Connect Network") then
self.template = "cbi/value"
self.keylist = {}
self.vallist = {}
self.placeholder = "10.1.1.254"
self.datatype = "ip4addr"
self.default=table_info[section]._opts
Value.render(self, section, scope)
else
self.rawhtml=true
self.template = "cbi/dvalue"
self.default=table_info[section]._opts
DummyValue.render(self, section, scope)
end
end
o = s:option(Button, "_button")
o.forcewrite = true
o.render = function(self, section, scope)
if table_info[section]._button and table_info[section]._value ~= nil then
self.inputtitle=table_info[section]._button
self.template = "cbi/button"
self.inputstyle = "edit"
Button.render(self, section, scope)
else
self.template = "cbi/dvalue"
self.default=""
DummyValue.render(self, section, scope)
end
end
o.write = function(self, section, value)
local res
docker:clear_status()
if section == "01name" then
docker:append_status("Containers: rename " .. container_id .. "...")
local new_name = table_info[section]._value
res = dk.containers:rename({
id = container_id,
query = {
name=new_name
}
})
elseif section == "08restart" then
docker:append_status("Containers: update " .. container_id .. "...")
local new_restart = table_info[section]._value
res = dk.containers:update({
id = container_id,
body = {
RestartPolicy = {
Name = new_restart
}
}
})
elseif table_info[section]._key == translate("Network") then
local _,_,leave_network
_, _, leave_network = table_info[section]._value:find("(.-) | .+")
leave_network = leave_network or table_info[section]._value
docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...")
res = dk.networks:disconnect({
name = leave_network,
body = {
Container = container_id
}
})
elseif section == "15connect" then
local connect_network = table_info[section]._value
local network_opiton
if connect_network ~= "none"
and connect_network ~= "bridge"
and connect_network ~= "host" then
network_opiton = table_info[section]._opts ~= "" and {
IPAMConfig={
IPv4Address=table_info[section]._opts
}
} or nil
end
docker:append_status("Network: connect " .. connect_network .. container_id .. "...")
res = dk.networks:connect({
name = connect_network,
body = {
Container = container_id,
EndpointConfig= network_opiton
}
})
end
if res and res.code > 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
else
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
end
elseif action == "resources" then
s = m:section(SimpleSection)
o = s:option( Value, "cpus",
translate("CPUs"),
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
o.placeholder = "1.5"
o.rmempty = true
o.datatype="ufloat"
o.default = container_info.HostConfig.NanoCpus / (10^9)
o = s:option(Value, "cpushares",
translate("CPU Shares Weight"),
translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024."))
o.placeholder = "1024"
o.rmempty = true
o.datatype="uinteger"
o.default = container_info.HostConfig.CpuShares
o = s:option(Value, "memory",
translate("Memory"),
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
o.placeholder = "128m"
o.rmempty = true
o.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
o = s:option(Value, "blkioweight",
translate("Block IO Weight"),
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
o.placeholder = "500"
o.rmempty = true
o.datatype="uinteger"
o.default = container_info.HostConfig.BlkioWeight
m.handle = function(self, state, data)
if state == FORM_VALID then
local memory = data.memory
if memory and memory ~= 0 then
_,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
if n then
unit = unit and unit:sub(1,1):upper() or "B"
if unit == "M" then
memory = tonumber(n) * 1024 * 1024
elseif unit == "G" then
memory = tonumber(n) * 1024 * 1024 * 1024
elseif unit == "K" then
memory = tonumber(n) * 1024
else
memory = tonumber(n)
end
end
end
request_body = {
BlkioWeight = tonumber(data.blkioweight),
NanoCPUs = tonumber(data.cpus)*10^9,
Memory = tonumber(memory),
CpuShares = tonumber(data.cpushares)
}
docker:write_status("Containers: update " .. container_id .. "...")
local res = dk.containers:update({id = container_id, body = request_body})
if res and res.code >= 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
else
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
end
end
elseif action == "file" then
m.submit = false
m.reset = false
s= m:section(SimpleSection)
s.template = "dockerman/container_file_manager"
s.container = container_id
m.redirect = nil
elseif action == "inspect" then
s = m:section(SimpleSection)
s.syslog = luci.jsonc.stringify(container_info, true)
s.title = translate("Container Inspect")
s.template = "dockerman/logs"
m.submit = false
m.reset = false
elseif action == "logs" then
local logs = ""
local query ={
stdout = 1,
stderr = 1,
tail = 1000
}
s = m:section(SimpleSection)
logs = dk.containers:logs({id = container_id, query = query})
if logs.code == 200 then
s.syslog=logs.body
else
s.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
end
s.title=translate("Container Logs")
s.template = "dockerman/logs"
m.submit = false
m.reset = false
elseif action == "console" then
m.submit = false
m.reset = false
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
local cmd = "/bin/sh"
local uid
s = m:section(SimpleSection)
o = s:option(Value, "command", translate("Command"))
o:value("/bin/sh", "/bin/sh")
o:value("/bin/ash", "/bin/ash")
o:value("/bin/bash", "/bin/bash")
o.default = "/bin/sh"
o.forcewrite = true
o.write = function(self, section, value)
cmd = value
end
o = s:option(Value, "uid", translate("UID"))
o.forcewrite = true
o.write = function(self, section, value)
uid = value
end
o = s:option(Button, "connect")
o.render = function(self, section, scope)
self.inputstyle = "add"
self.title = " "
self.inputtitle = translate("Connect")
Button.render(self, section, scope)
end
o.write = function(self, section)
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then
return
end
local ttyd_ssl = uci.get("ttyd", "@ttyd[0]", "ssl")
local ttyd_ssl_key = uci.get("ttyd", "@ttyd[0]", "ssl_key")
local ttyd_ssl_cert = uci.get("ttyd", "@ttyd[0]", "ssl_cert")
if ttyd_ssl=="1" and ttyd_ssl_cert and ttyd_ssl_key then
cmd_ttyd=string.format('%s -S -C %s -K %s',cmd_ttyd,ttyd_ssl_cert,ttyd_ssl_key)
end
local pid = luci.util.trim(luci.util.exec("netstat -lnpt | grep :7682 | grep ttyd | tr -s ' ' | cut -d ' ' -f7 | cut -d'/' -f1"))
if pid and pid ~= "" then
luci.util.exec("kill -9 " .. pid)
end
local hosts
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") or false
local host = nil
local port = nil
local socket = nil
if remote then
host = uci:get("dockerd", "dockerman", "remote_host") or nil
port = uci:get("dockerd", "dockerman", "remote_port") or nil
else
socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
end
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket then
hosts = "unix://" .. socket
else
return
end
if uid and uid ~= "" then
uid = "-u " .. uid
else
uid = ""
end
local start_cmd = string.format('%s -d 2 --once -p 7682 %s -H "%s" exec -it %s %s %s&', cmd_ttyd, cmd_docker, hosts, uid, container_id, cmd)
os.execute(start_cmd)
o = s:option(DummyValue, "console")
o.container_id = container_id
o.template = "dockerman/container_console"
end
end
elseif action == "stats" then
local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
local container_top
if response.code == 200 then
container_top=response.body
else
response = dk.containers:top({id = container_id})
if response.code == 200 then
container_top=response.body
end
end
if type(container_top) == "table" then
s = m:section(SimpleSection)
s.container_id = container_id
s.template = "dockerman/container_stats"
table_stats = {
cpu={
key=translate("CPU Useage"),
value='-'
},
memory={
key=translate("Memory Useage"),
value='-'
}
}
container_top = response.body
s = m:section(Table, table_stats, translate("Stats"))
s:option(DummyValue, "key", translate("Stats")).width="33%"
s:option(DummyValue, "value")
top_section = m:section(Table, container_top.Processes, translate("TOP"))
for i, v in ipairs(container_top.Titles) do
top_section:option(DummyValue, i, translate(v))
end
end
m.submit = false
m.reset = false
end
return m

View File

@ -0,0 +1,284 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local http = require "luci.http"
local docker = require "luci.model.docker"
local m, s, o
local images, networks, containers, res, lost_state
local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.images:list()
if res and res.code and res.code < 300 then
images = res.body
end
res = dk.networks:list()
if res and res.code and res.code < 300 then
networks = res.body
end
res = dk.containers:list({
query = {
all = true
}
})
if res and res.code and res.code < 300 then
containers = res.body
end
end
function get_containers()
local data = {}
if type(containers) ~= "table" then
return nil
end
for i, v in ipairs(containers) do
local index = (10^12 - v.Created) .. "_id_" .. v.Id
data[index]={}
data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(1,12)
-- data[index]["name"] = v.Names[1]:sub(2)
data[index]["_status"] = v.Status
if v.Status:find("^Up") then
data[index]["_name"] = "<font color='green'>"..v.Names[1]:sub(2).."</font>"
data[index]["_status"] = "<a href='"..luci.dispatcher.build_url("admin/docker/container/"..v.Id).."/stats'><font color='green'>".. data[index]["_status"] .. "</font>" .. "<br><font color='#9f9f9f' class='container_cpu_status'></font><br><font color='#9f9f9f' class='container_mem_status'></font><br><font color='#9f9f9f' class='container_network_status'></font></a>"
else
data[index]["_name"] = "<font color='red'>"..v.Names[1]:sub(2).."</font>"
data[index]["_status"] = '<font class="container_not_running" color="red">'.. data[index]["_status"] .. "</font>"
end
if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
for networkname, netconfig in pairs(v.NetworkSettings.Networks) do
data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "")
end
end
-- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge"
-- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil
-- local _, _, image = v.Image:find("^sha256:(.+)")
-- if image ~= nil then
-- image=image:sub(1,12)
-- end
if v.Ports and next(v.Ports) ~= nil then
data[index]["_ports"] = nil
local ip = require "luci.ip"
for _,v2 in ipairs(v.Ports) do
-- display ipv4 only
if ip.new(v2.IP or "0.0.0.0"):is4() then
data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "")
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('<a href="javascript:void(0);" onclick="window.open((window.location.origin.match(/^(.+):\\d+$/) && window.location.origin.match(/^(.+):\\d+$/)[1] || window.location.origin) + \':\' + '.. v2.PublicPort ..', \'_blank\');">') or "")
.. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "")
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
end
end
end
for ii,iv in ipairs(images) do
if iv.Id == v.ImageID then
data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":&lt;none&gt;")
end
end
data[index]["_id_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. data[index]["_name"] .. "<br><font color='#9f9f9f'>ID: " .. data[index]["_id"]
.. "</font></a><br>Image: " .. (data[index]["_image"] or "&lt;none&gt;")
.. "<br><font color='#9f9f9f' class='container_size_".. v.Id .."'></font>"
if type(v.Mounts) == "table" and next(v.Mounts) then
for _, v2 in pairs(v.Mounts) do
if v2.Type ~= "volume" then
local v_sorce_d, v_dest_d
local v_sorce = ""
local v_dest = ""
for v_sorce_d in v2["Source"]:gmatch('[^/]+') do
if v_sorce_d and #v_sorce_d > 12 then
v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,8) .. ".."
else
v_sorce = v_sorce .."/".. v_sorce_d
end
end
for v_dest_d in v2["Destination"]:gmatch('[^/]+') do
if v_dest_d and #v_dest_d > 12 then
v_dest = v_dest .. "/" .. v_dest_d:sub(1,8) .. ".."
else
v_dest = v_dest .."/".. v_dest_d
end
end
data[index]["_mounts"] = (data[index]["_mounts"] and (data[index]["_mounts"] .. "<br>") or "") .. '<span title="'.. v2.Source.. "" .. v2.Destination .. '" ><a href="'..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..'/file?path='..v2["Destination"]..'">' .. v_sorce .. "" .. v_dest..'</a></span>'
end
end
end
data[index]["_image_id"] = v.ImageID:sub(8,20)
data[index]["_command"] = v.Command
end
return data
end
local container_list = not lost_state and get_containers() or {}
m = SimpleForm("docker",
translate("Docker - Containers"),
translate("This page displays all containers that have been created on the connected docker host."))
m.submit=false
m.reset=false
m:append(Template("dockerman/containers_running_stats"))
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table, container_list, translate("Containers"))
s.nodescr=true
s.config="containers"
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.width = "1%"
o.write=function(self, section, value)
container_list[section]._selected = value
end
-- o = s:option(DummyValue, "_id", translate("ID"))
-- o.width="10%"
-- o = s:option(DummyValue, "_name", translate("Container Name"))
-- o.rawhtml = true
o = s:option(DummyValue, "_id_name", translate("Container Info"))
o.rawhtml = true
o.width="15%"
o = s:option(DummyValue, "_status", translate("Status"))
o.width="15%"
o.rawhtml=true
o = s:option(DummyValue, "_network", translate("Network"))
o.width="10%"
o = s:option(DummyValue, "_ports", translate("Ports"))
o.width="5%"
o.rawhtml = true
o = s:option(DummyValue, "_mounts", translate("Mounts"))
o.width="25%"
o.rawhtml = true
-- o = s:option(DummyValue, "_image", translate("Image"))
-- o.width="8%"
o = s:option(DummyValue, "_command", translate("Command"))
o.width="15%"
local start_stop_remove = function(m, cmd)
local container_selected = {}
-- 遍历table中sectionid
for k in pairs(container_list) do
-- 得到选中项的名字
if container_list[k]._selected == 1 then
container_selected[#container_selected + 1] = container_list[k]["_id"]
end
end
if #container_selected > 0 then
local success = true
docker:clear_status()
for _, cont in ipairs(container_selected) do
docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
local res = dk.containers[cmd](dk, {id = cont})
if res and res.code and res.code >= 300 then
success = false
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
else
docker:append_status("done\n")
end
end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
end
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "_new")
o.inputtitle = translate("Add")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "add"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
o.disable = lost_state
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Start")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"start")
end
o.disable = lost_state
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"restart")
end
o.disable = lost_state
o = s:option(Button, "_stop")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Stop")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"stop")
end
o.disable = lost_state
o = s:option(Button, "_kill")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Kill")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"kill")
end
o.disable = lost_state
o = s:option(Button, "_remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Remove")
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m, "remove")
end
o.disable = lost_state
return m

View File

@ -0,0 +1,284 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local dk = docker.new()
local containers, images, res, lost_state
local m, s, o
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.images:list()
if res and res.code and res.code < 300 then
images = res.body
end
res = dk.containers:list({ query = { all = true } })
if res and res.code and res.code < 300 then
containers = res.body
end
end
function get_images()
local data = {}
for i, v in ipairs(images) do
local index = v.Created .. v.Id
data[index]={}
data[index]["_selected"] = 0
data[index]["id"] = v.Id:sub(8)
data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>'
if v.RepoTags and next(v.RepoTags)~=nil then
for i, v1 in ipairs(v.RepoTags) do
data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>'))
if not data[index]["tag"] then
data[index]["tag"] = v1
end
end
else
data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
end
data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;")
for ci,cv in ipairs(containers) do
if v.Id == cv.ImageID then
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2).."</a>"
end
end
data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB"
data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created)
end
return data
end
local image_list = not lost_state and get_images() or {}
m = SimpleForm("docker",
translate("Docker - Images"),
translate("On this page all images are displayed that are available on the system and with which a container can be created."))
m.submit=false
m.reset=false
local pull_value={
_image_tag_name="",
_registry="index.docker.io"
}
s = m:section(SimpleSection,
translate("Pull Image"),
translate("By entering a valid image name with the corresponding version, the docker image can be downloaded from the configured registry."))
s.template="cbi/nullsection"
o = s:option(Value, "_image_tag_name")
o.template = "dockerman/cbi/inlinevalue"
o.placeholder="lisaac/luci:latest"
o.write = function(self, section, value)
local hastag = value:find(":")
if not hastag then
value = value .. ":latest"
end
pull_value["_image_tag_name"] = value
end
o = s:option(Button, "_pull")
o.inputtitle= translate("Pull")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "add"
o.disable = lost_state
o.write = function(self, section)
local tag = pull_value["_image_tag_name"]
local json_stringify = luci.jsonc and luci.jsonc.stringify
if tag and tag ~= "" then
docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n")
local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
if res and res.code and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. tag)) then
docker:clear_status()
else
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
end
else
docker:append_status("code: 400 please input the name of image name!")
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end
s = m:section(SimpleSection,
translate("Import Image"),
translate("When pressing the Import button, both a local image can be loaded onto the system and a valid image tar can be downloaded from remote."))
o = s:option(DummyValue, "_image_import")
o.template = "dockerman/images_import"
o.disable = lost_state
s = m:section(Table, image_list, translate("Images overview"))
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.write = function(self, section, value)
image_list[section]._selected = value
end
o = s:option(DummyValue, "_id", translate("ID"))
o.rawhtml = true
o = s:option(DummyValue, "_tags", translate("RepoTags"))
o.rawhtml = true
o = s:option(DummyValue, "_containers", translate("Containers"))
o.rawhtml = true
o = s:option(DummyValue, "_size", translate("Size"))
o = s:option(DummyValue, "_created", translate("Created"))
local remove_action = function(force)
local image_selected = {}
for k in pairs(image_list) do
if image_list[k]._selected == 1 then
image_selected[#image_selected+1] = (image_list[k]["_tags"]:match("<br>") or image_list[k]["_tags"]:match("&lt;none&gt;")) and image_list[k].id or image_list[k].tag
end
end
if next(image_selected) ~= nil then
local success = true
docker:clear_status()
for _, img in ipairs(image_selected) do
local query
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
if force then
query = {force = true}
end
local msg = dk.images:remove({
id = img,
query = query
})
if msg and msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false
else
docker:append_status("done\n")
end
end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end
end
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err = docker:read_status()
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
remove_action()
end
o.disable = lost_state
o = s:option(Button, "forceremove")
o.inputtitle= translate("Force Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
remove_action(true)
end
o.disable = lost_state
o = s:option(Button, "save")
o.inputtitle= translate("Save")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "edit"
o.disable = lost_state
o.forcewrite = true
o.write = function (self, section)
local image_selected = {}
for k in pairs(image_list) do
if image_list[k]._selected == 1 then
image_selected[#image_selected + 1] = image_list[k].id
end
end
if next(image_selected) ~= nil then
local names, first, show_name
for _, img in ipairs(image_selected) do
names = names and (names .. "&names=".. img) or img
end
if #image_selected > 1 then
show_name = "images"
else
show_name = image_selected[1]
end
local cb = function(res, chunk)
if res and res.code and res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="'.. show_name .. '.tar"')
luci.http.header('Content-Type', 'application\/x-tar')
end
luci.ltn12.pump.all(chunk, luci.http.write)
else
if not first then
first = true
luci.http.prepare_content("text/plain")
end
luci.ltn12.pump.all(chunk, luci.http.write)
end
end
docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
local msg = dk.images:get({query = {names = names}}, cb)
if msg and msg.code and msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
else
docker:clear_status()
end
end
end
o = s:option(Button, "load")
o.inputtitle= translate("Load")
o.template = "dockerman/images_load"
o.inputstyle = "add"
o.disable = lost_state
return m

View File

@ -0,0 +1,159 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local m, s, o
local networks, dk, res, lost_state
dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.networks:list()
if res and res.code and res.code < 300 then
networks = res.body
end
end
local get_networks = function ()
local data = {}
if type(networks) ~= "table" then
return nil
end
for i, v in ipairs(networks) do
local index = v.Created .. v.Id
data[index]={}
data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(1,12)
data[index]["_name"] = v.Name
data[index]["_driver"] = v.Driver
if v.Driver == "bridge" then
data[index]["_interface"] = v.Options["com.docker.network.bridge.name"]
elseif v.Driver == "macvlan" then
data[index]["_interface"] = v.Options.parent
end
data[index]["_subnet"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
data[index]["_gateway"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil
end
return data
end
local network_list = not lost_state and get_networks() or {}
m = SimpleForm("docker",
translate("Docker - Networks"),
translate("This page displays all docker networks that have been created on the connected docker host."))
m.submit=false
m.reset=false
s = m:section(Table, network_list, translate("Networks overview"))
s.nodescr=true
o = s:option(Flag, "_selected","")
o.template = "dockerman/cbi/xfvalue"
o.disabled = 0
o.enabled = 1
o.default = 0
o.render = function(self, section, scope)
self.disable = 0
if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then
self.disable = 1
end
Flag.render(self, section, scope)
end
o.write = function(self, section, value)
network_list[section]._selected = value
end
o = s:option(DummyValue, "_id", translate("ID"))
o = s:option(DummyValue, "_name", translate("Network Name"))
o = s:option(DummyValue, "_driver", translate("Driver"))
o = s:option(DummyValue, "_interface", translate("Parent Interface"))
o = s:option(DummyValue, "_subnet", translate("Subnet"))
o = s:option(DummyValue, "_gateway", translate("Gateway"))
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err = docker:read_status()
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "_new")
o.inputtitle= translate("New")
o.template = "dockerman/cbi/inlinebutton"
o.notitle=true
o.inputstyle = "add"
o.forcewrite = true
o.disable = lost_state
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
end
o = s:option(Button, "_remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.disable = lost_state
o.write = function(self, section)
local network_selected = {}
local network_name_selected = {}
local network_driver_selected = {}
for k in pairs(network_list) do
if network_list[k]._selected == 1 then
network_selected[#network_selected + 1] = network_list[k]._id
network_name_selected[#network_name_selected + 1] = network_list[k]._name
network_driver_selected[#network_driver_selected + 1] = network_list[k]._driver
end
end
if next(network_selected) ~= nil then
local success = true
docker:clear_status()
for ii, net in ipairs(network_selected) do
docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
local res = dk.networks["remove"](dk, {id = net})
if res and res.code and res.code >= 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
success = false
else
docker:append_status("done\n")
if network_driver_selected[ii] == "macvlan" then
docker.remove_macvlan_interface(network_name_selected[ii])
end
end
end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
end
end
return m

View File

@ -0,0 +1,923 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local m, s, o
local dk = docker.new()
local cmd_line = table.concat(arg, '/')
local images, networks
local create_body = {}
if dk:_ping().code ~= 200 then
lost_state = true
images = {}
networks = {}
else
images = dk.images:list().body
networks = dk.networks:list().body
end
local is_quot_complete = function(str)
local num = 0, w
require "math"
if not str then
return true
end
local num = 0, w
for w in str:gmatch("\"") do
num = num + 1
end
if math.fmod(num, 2) ~= 0 then
return false
end
num = 0
for w in str:gmatch("\'") do
num = num + 1
end
if math.fmod(num, 2) ~= 0 then
return false
end
return true
end
function contains(list, x)
for _, v in pairs(list) do
if v == x then
return true
end
end
return false
end
local resolve_cli = function(cmd_line)
local config = {
advance = 1
}
local key_no_val = {
't',
'd',
'i',
'tty',
'rm',
'read_only',
'interactive',
'init',
'help',
'detach',
'privileged',
'P',
'publish_all',
}
local key_with_val = {
'sysctl',
'add_host',
'a',
'attach',
'blkio_weight_device',
'cap_add',
'cap_drop',
'device',
'device_cgroup_rule',
'device_read_bps',
'device_read_iops',
'device_write_bps',
'device_write_iops',
'dns',
'dns_option',
'dns_search',
'e',
'env',
'env_file',
'expose',
'group_add',
'l',
'label',
'label_file',
'link',
'link_local_ip',
'log_driver',
'log_opt',
'network_alias',
'p',
'publish',
'security_opt',
'storage_opt',
'tmpfs',
'v',
'volume',
'volumes_from',
'blkio_weight',
'cgroup_parent',
'cidfile',
'cpu_period',
'cpu_quota',
'cpu_rt_period',
'cpu_rt_runtime',
'c',
'cpu_shares',
'cpus',
'cpuset_cpus',
'cpuset_mems',
'detach_keys',
'disable_content_trust',
'domainname',
'entrypoint',
'gpus',
'health_cmd',
'health_interval',
'health_retries',
'health_start_period',
'health_timeout',
'h',
'hostname',
'ip',
'ip6',
'ipc',
'isolation',
'kernel_memory',
'mac_address',
'm',
'memory',
'memory_reservation',
'memory_swap',
'memory_swappiness',
'mount',
'name',
'network',
'no_healthcheck',
'oom_kill_disable',
'oom_score_adj',
'pid',
'pids_limit',
'restart',
'runtime',
'shm_size',
'sig_proxy',
'stop_signal',
'stop_timeout',
'ulimit',
'u',
'user',
'userns',
'uts',
'volume_driver',
'w',
'workdir'
}
local key_abb = {
net='network',
a='attach',
c='cpu-shares',
d='detach',
e='env',
h='hostname',
i='interactive',
l='label',
m='memory',
p='publish',
P='publish_all',
t='tty',
u='user',
v='volume',
w='workdir'
}
local key_with_list = {
'sysctl',
'add_host',
'a',
'attach',
'blkio_weight_device',
'cap_add',
'cap_drop',
'device',
'device_cgroup_rule',
'device_read_bps',
'device_read_iops',
'device_write_bps',
'device_write_iops',
'dns',
'dns_optiondns_search',
'e',
'env',
'env_file',
'expose',
'group_add',
'l',
'label',
'label_file',
'link',
'link_local_ip',
'log_opt',
'network_alias',
'p',
'publish',
'security_opt',
'storage_opt',
'tmpfs',
'v',
'volume',
'volumes_from',
}
local key = nil
local _key = nil
local val = nil
local is_cmd = false
cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)")
for w in cmd_line:gmatch("[^%s]+") do
if w =='\\' then
elseif not key and not _key and not is_cmd then
--key=val
key, val = w:match("^%-%-([%lP%-]-)=(.+)")
if not key then
--key val
key = w:match("^%-%-([%lP%-]+)")
if not key then
-- -v val
key = w:match("^%-([%lP%-]+)")
if key then
-- for -dit
if key:match("i") or key:match("t") or key:match("d") then
if key:match("i") then
config[key_abb["i"]] = true
key:gsub("i", "")
end
if key:match("t") then
config[key_abb["t"]] = true
key:gsub("t", "")
end
if key:match("d") then
config[key_abb["d"]] = true
key:gsub("d", "")
end
if key:match("P") then
config[key_abb["P"]] = true
key:gsub("P", "")
end
if key == "" then
key = nil
end
end
end
end
end
if key then
key = key:gsub("-","_")
key = key_abb[key] or key
if contains(key_no_val, key) then
config[key] = true
val = nil
key = nil
elseif contains(key_with_val, key) then
-- if key == "cap_add" then config.privileged = true end
else
key = nil
val = nil
end
else
config.image = w
key = nil
val = nil
is_cmd = true
end
elseif (key or _key) and not is_cmd then
if key == "mount" then
-- we need resolve mount options here
-- type=bind,source=/source,target=/app
local _type = w:match("^type=([^,]+),") or "bind"
local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or ""
local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
local ro = w:match(",readonly") and "ro" or nil
if source and target then
if _type ~= "tmpfs" then
local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
else
local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
key = "tmpfs"
val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
if not config[key] then
config[key] = {}
end
table.insert( config[key], val )
key = nil
val = nil
end
end
else
val = w
end
elseif is_cmd then
config["command"] = (config["command"] and (config["command"] .. " " )or "") .. w
end
if (key or _key) and val then
key = _key or key
if contains(key_with_list, key) then
if not config[key] then
config[key] = {}
end
if _key then
config[key][#config[key]] = config[key][#config[key]] .. " " .. w
else
table.insert( config[key], val )
end
if is_quot_complete(config[key][#config[key]]) then
config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
_key = nil
else
_key = key
end
else
config[key] = (config[key] and (config[key] .. " ") or "") .. val
if is_quot_complete(config[key]) then
config[key] = config[key]:gsub("[\"\']", "")
_key = nil
else
_key = key
end
end
key = nil
val = nil
end
end
return config
end
local default_config = {}
if cmd_line and cmd_line:match("^DOCKERCLI.+") then
default_config = resolve_cli(cmd_line)
elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
local container_id = cmd_line:match("^duplicate/(.+)")
create_body = dk:containers_duplicate_config({id = container_id}) or {}
if not create_body.HostConfig then
create_body.HostConfig = {}
end
if next(create_body) ~= nil then
default_config.name = nil
default_config.image = create_body.Image
default_config.hostname = create_body.Hostname
default_config.tty = create_body.Tty and true or false
default_config.interactive = create_body.OpenStdin and true or false
default_config.privileged = create_body.HostConfig.Privileged and true or false
default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
-- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
-- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig
default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
default_config.link = create_body.HostConfig.Links
default_config.env = create_body.Env
default_config.dns = create_body.HostConfig.Dns
default_config.volume = create_body.HostConfig.Binds
default_config.cap_add = create_body.HostConfig.CapAdd
default_config.publish_all = create_body.HostConfig.PublishAllPorts
if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then
default_config.sysctl = {}
for k, v in pairs(create_body.HostConfig.Sysctls) do
table.insert( default_config.sysctl, k.."="..v )
end
end
if create_body.HostConfig.LogConfig then
if create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then
default_config.log_opt = {}
for k, v in pairs(create_body.HostConfig.LogConfig.Config) do
table.insert( default_config.log_opt, k.."="..v )
end
end
default_config.log_driver = create_body.HostConfig.LogConfig.Type or nil
end
if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
default_config.publish = {}
for k, v in pairs(create_body.HostConfig.PortBindings) do
for x, y in ipairs(v) do
table.insert( default_config.publish, y.HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
end
end
end
default_config.user = create_body.User or nil
default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil
default_config.advance = 1
default_config.cpus = create_body.HostConfig.NanoCPUs
default_config.cpu_shares = create_body.HostConfig.CpuShares
default_config.memory = create_body.HostConfig.Memory
default_config.blkio_weight = create_body.HostConfig.BlkioWeight
if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then
default_config.device = {}
for _, v in ipairs(create_body.HostConfig.Devices) do
table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
end
end
if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
default_config.tmpfs = {}
for k, v in pairs(create_body.HostConfig.Tmpfs) do
table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v )
end
end
end
end
m = SimpleForm("docker", translate("Docker - Containers"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
if lost_state then
m.submit=false
m.reset=false
end
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(SimpleSection, translate("Create new docker container"))
s.addremove = true
s.anonymous = true
o = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
o.rawhtml = true
o.template = "dockerman/newcontainer_resolve"
o = s:option(Value, "name", translate("Container Name"))
o.rmempty = true
o.default = default_config.name or nil
o = s:option(Flag, "interactive", translate("Interactive (-i)"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.interactive and 1 or 0
o = s:option(Flag, "tty", translate("TTY (-t)"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.tty and 1 or 0
o = s:option(Value, "image", translate("Docker Image"))
o.rmempty = true
o.default = default_config.image or nil
for _, v in ipairs (images) do
if v.RepoTags then
o:value(v.RepoTags[1], v.RepoTags[1])
end
end
o = s:option(Flag, "_force_pull", translate("Always pull image first"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = 0
o = s:option(Flag, "privileged", translate("Privileged"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.privileged and 1 or 0
o = s:option(ListValue, "restart", translate("Restart Policy"))
o.rmempty = true
o:value("no", "No")
o:value("unless-stopped", "Unless stopped")
o:value("always", "Always")
o:value("on-failure", "On failure")
o.default = default_config.restart or "unless-stopped"
local d_network = s:option(ListValue, "network", translate("Networks"))
d_network.rmempty = true
d_network.default = default_config.network or "bridge"
local d_ip = s:option(Value, "ip", translate("IPv4 Address"))
d_ip.datatype="ip4addr"
d_ip:depends("network", "nil")
d_ip.default = default_config.ip or nil
o = s:option(DynamicList, "link", translate("Links with other containers"))
o.placeholder = "container_name:alias"
o.rmempty = true
o:depends("network", "bridge")
o.default = default_config.link or nil
o = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
o.placeholder = "8.8.8.8"
o.rmempty = true
o.default = default_config.dns or nil
o = s:option(Value, "user",
translate("User(-u)"),
translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
o.placeholder = "1000:1000"
o.rmempty = true
o.default = default_config.user or nil
o = s:option(DynamicList, "env",
translate("Environmental Variable(-e)"),
translate("Set environment variables to inside the container"))
o.placeholder = "TZ=Asia/Shanghai"
o.rmempty = true
o.default = default_config.env or nil
o = s:option(DynamicList, "volume",
translate("Bind Mount(-v)"),
translate("Bind mount a volume"))
o.placeholder = "/media:/media:slave"
o.rmempty = true
o.default = default_config.volume or nil
local d_publish = s:option(DynamicList, "publish",
translate("Exposed Ports(-p)"),
translate("Publish container's port(s) to the host"))
d_publish.placeholder = "2200:22/tcp"
d_publish.rmempty = true
d_publish.default = default_config.publish or nil
o = s:option(Value, "command", translate("Run command"))
o.placeholder = "/bin/sh init.sh"
o.rmempty = true
o.default = default_config.command or nil
o = s:option(Flag, "advance", translate("Advance"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.advance or 0
o = s:option(Value, "hostname",
translate("Host Name"),
translate("The hostname to use for the container"))
o.rmempty = true
o.default = default_config.hostname or nil
o:depends("advance", 1)
o = s:option(Flag, "publish_all",
translate("Exposed All Ports(-P)"),
translate("Allocates an ephemeral host port for all of a container's exposed ports"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.publish_all and 1 or 0
o:depends("advance", 1)
o = s:option(DynamicList, "device",
translate("Device(--device)"),
translate("Add host device to the container"))
o.placeholder = "/dev/sda:/dev/xvdc:rwm"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.device or nil
o = s:option(DynamicList, "tmpfs",
translate("Tmpfs(--tmpfs)"),
translate("Mount tmpfs directory"))
o.placeholder = "/run:rw,noexec,nosuid,size=65536k"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.tmpfs or nil
o = s:option(DynamicList, "sysctl",
translate("Sysctl(--sysctl)"),
translate("Sysctls (kernel parameters) options"))
o.placeholder = "net.ipv4.ip_forward=1"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.sysctl or nil
o = s:option(DynamicList, "cap_add",
translate("CAP-ADD(--cap-add)"),
translate("A list of kernel capabilities to add to the container"))
o.placeholder = "NET_ADMIN"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.cap_add or nil
o = s:option(Value, "cpus",
translate("CPUs"),
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
o.placeholder = "1.5"
o.rmempty = true
o:depends("advance", 1)
o.datatype="ufloat"
o.default = default_config.cpus or nil
o = s:option(Value, "cpu_shares",
translate("CPU Shares Weight"),
translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
o.placeholder = "1024"
o.rmempty = true
o:depends("advance", 1)
o.datatype="uinteger"
o.default = default_config.cpu_shares or nil
o = s:option(Value, "memory",
translate("Memory"),
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
o.placeholder = "128m"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.memory or nil
o = s:option(Value, "blkio_weight",
translate("Block IO Weight"),
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
o.placeholder = "500"
o.rmempty = true
o:depends("advance", 1)
o.datatype="uinteger"
o.default = default_config.blkio_weight or nil
o = s:option(Value, "log_driver",
translate("Logging driver"),
translate("The logging driver for the container"))
o.placeholder = "json-file"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.log_driver or nil
o = s:option(DynamicList, "log_opt",
translate("Log driver options"),
translate("The logging configuration for this container"))
o.placeholder = "max-size=1m"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.log_opt or nil
for _, v in ipairs (networks) do
if v.Name then
local parent = v.Options and v.Options.parent or nil
local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
d_network:value(v.Name, network_name)
if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
d_ip:depends("network", v.Name)
end
if v.Driver == "bridge" then
d_publish:depends("network", v.Name)
end
end
end
m.handle = function(self, state, data)
if state ~= FORM_VALID then
return
end
local tmp
local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S"))
local hostname = data.hostname
local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false
local publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all or false
local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false
local image = data.image
local user = data.user
if image and not image:match(".-:.+") then
image = image .. ":latest"
end
local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false
local restart = data.restart
local env = data.env
local dns = data.dns
local cap_add = data.cap_add
local sysctl = {}
local log_driver = data.log_driver
tmp = data.sysctl
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local k,v1 = v:match("(.-)=(.+)")
if k and v1 then
sysctl[k]=v1
end
end
end
local log_opt = {}
tmp = data.log_opt
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local k,v1 = v:match("(.-)=(.+)")
if k and v1 then
log_opt[k]=v1
end
end
end
local network = data.network
local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
local volume = data.volume
local memory = data.memory or nil
local cpu_shares = data.cpu_shares or nil
local cpus = data.cpus or nil
local blkio_weight = data.blkio_weight or nil
local portbindings = {}
local exposedports = {}
local tmpfs = {}
tmp = data.tmpfs
if type(tmp) == "table" then
for i, v in ipairs(tmp)do
local k= v:match("([^:]+)")
local v1 = v:match(".-:([^:]+)") or ""
if k then
tmpfs[k]=v1
end
end
end
local device = {}
tmp = data.device
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local t = {}
local _,_, h, c, p = v:find("(.-):(.-):(.+)")
if h and c then
t['PathOnHost'] = h
t['PathInContainer'] = c
t['CgroupPermissions'] = p or "rwm"
else
local _,_, h, c = v:find("(.-):(.+)")
if h and c then
t['PathOnHost'] = h
t['PathInContainer'] = c
t['CgroupPermissions'] = "rwm"
else
t['PathOnHost'] = v
t['PathInContainer'] = v
t['CgroupPermissions'] = "rwm"
end
end
if next(t) ~= nil then
table.insert( device, t )
end
end
end
tmp = data.publish or {}
for i, v in ipairs(tmp) do
for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do
local _,_,p= v2:find("^%d+/(%w+)")
if p == nil then
v2=v2..'/tcp'
end
portbindings[v2] = {{HostPort=v1}}
exposedports[v2] = {HostPort=v1}
end
end
local link = data.link
tmp = data.command
local command = {}
if tmp ~= nil then
for v in string.gmatch(tmp, "[^%s]+") do
command[#command+1] = v
end
end
if memory and memory ~= 0 then
_,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
if n then
unit = unit and unit:sub(1,1):upper() or "B"
if unit == "M" then
memory = tonumber(n) * 1024 * 1024
elseif unit == "G" then
memory = tonumber(n) * 1024 * 1024 * 1024
elseif unit == "K" then
memory = tonumber(n) * 1024
else
memory = tonumber(n)
end
end
end
create_body.Hostname = network ~= "host" and (hostname or name) or nil
create_body.Tty = tty and true or false
create_body.OpenStdin = interactive and true or false
create_body.User = user
create_body.Cmd = command
create_body.Env = env
create_body.Image = image
create_body.ExposedPorts = exposedports
create_body.HostConfig = create_body.HostConfig or {}
create_body.HostConfig.Dns = dns
create_body.HostConfig.Binds = volume
create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
create_body.HostConfig.Privileged = privileged and true or false
create_body.HostConfig.PortBindings = portbindings
create_body.HostConfig.Memory = memory and tonumber(memory)
create_body.HostConfig.CpuShares = cpu_shares and tonumber(cpu_shares)
create_body.HostConfig.NanoCPUs = cpus and tonumber(cpus) * 10 ^ 9
create_body.HostConfig.BlkioWeight = blkio_weight and tonumber(blkio_weight)
create_body.HostConfig.PublishAllPorts = publish_all
if create_body.HostConfig.NetworkMode ~= network then
create_body.NetworkingConfig = nil
end
create_body.HostConfig.NetworkMode = network
if ip then
if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
v.IPAMConfig.IPv4Address = ip
else
create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
end
break
end
else
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
end
elseif not create_body.NetworkingConfig then
create_body.NetworkingConfig = nil
end
create_body["HostConfig"]["Tmpfs"] = tmpfs
create_body["HostConfig"]["Devices"] = device
create_body["HostConfig"]["Sysctls"] = sysctl
create_body["HostConfig"]["CapAdd"] = cap_add
create_body["HostConfig"]["LogConfig"] = {
Config = log_opt,
Type = log_driver
}
if network == "bridge" then
create_body["HostConfig"]["Links"] = link
end
local pull_image = function(image)
local json_stringify = luci.jsonc and luci.jsonc.stringify
docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb)
if res and res.code and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then
docker:append_status("done\n")
else
res.code = (res.code == 200) and 500 or res.code
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
end
docker:clear_status()
local exist_image = false
if image then
for _, v in ipairs (images) do
if v.RepoTags and v.RepoTags[1] == image then
exist_image = true
break
end
end
if not exist_image then
pull_image(image)
elseif data._force_pull == 1 then
pull_image(image)
end
end
create_body = docker.clear_empty_tables(create_body)
docker:append_status("Container: " .. "create" .. " " .. name .. "...")
local res = dk.containers:create({name = name, body = create_body})
if res and res.code and res.code == 201 then
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
else
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
end
return m

View File

@ -0,0 +1,258 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local m, s, o
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
end
m = SimpleForm("docker", translate("Docker - Network"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
if lost_state then
m.submit=false
m.reset=false
end
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(SimpleSection, translate("Create new docker network"))
s.addremove = true
s.anonymous = true
o = s:option(Value, "name",
translate("Network Name"),
translate("Name of the network that can be selected during container creation"))
o.rmempty = true
o = s:option(ListValue, "driver", translate("Driver"))
o.rmempty = true
o:value("bridge", translate("Bridge device"))
o:value("macvlan", translate("MAC VLAN"))
o:value("ipvlan", translate("IP VLAN"))
o:value("overlay", translate("Overlay network"))
o = s:option(Value, "parent", translate("Base device"))
o.rmempty = true
o:depends("driver", "macvlan")
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
for _, v in ipairs(interfaces) do
o:value(v, v)
end
o.default="br-lan"
o.placeholder="br-lan"
o = s:option(ListValue, "macvlan_mode", translate("Mode"))
o.rmempty = true
o:depends("driver", "macvlan")
o.default="bridge"
o:value("bridge", translate("Bridge (Support direct communication between MAC VLANs)"))
o:value("private", translate("Private (Prevent communication between MAC VLANs)"))
o:value("vepa", translate("VEPA (Virtual Ethernet Port Aggregator)"))
o:value("passthru", translate("Pass-through (Mirror physical device to single MAC VLAN)"))
o = s:option(ListValue, "ipvlan_mode", translate("Ipvlan Mode"))
o.rmempty = true
o:depends("driver", "ipvlan")
o.default="l3"
o:value("l2", translate("L2 bridge"))
o:value("l3", translate("L3 bridge"))
o = s:option(Flag, "ingress",
translate("Ingress"),
translate("Ingress network is the network which provides the routing-mesh in swarm mode"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = 0
o:depends("driver", "overlay")
o = s:option(DynamicList, "options", translate("Options"))
o.rmempty = true
o.placeholder="com.docker.network.driver.mtu=1500"
o = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network"))
o.rmempty = true
o:depends("driver", "overlay")
o.disabled = 0
o.enabled = 1
o.default = 0
if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then
o = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt"))
o:depends("driver", "macvlan")
o.disabled = 0
o.enabled = 1
o.default = 1
end
o = s:option(Value, "subnet", translate("Subnet"))
o.rmempty = true
o.placeholder="10.1.0.0/16"
o.datatype="ip4addr"
o = s:option(Value, "gateway", translate("Gateway"))
o.rmempty = true
o.placeholder="10.1.1.1"
o.datatype="ip4addr"
o = s:option(Value, "ip_range", translate("IP range"))
o.rmempty = true
o.placeholder="10.1.1.0/24"
o.datatype="ip4addr"
o = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
o.rmempty = true
o.placeholder="my-route=10.1.1.1"
o = s:option(Flag, "ipv6", translate("Enable IPv6"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = 0
o = s:option(Value, "subnet6", translate("IPv6 Subnet"))
o.rmempty = true
o.placeholder="fe80::/10"
o.datatype="ip6addr"
o:depends("ipv6", 1)
o = s:option(Value, "gateway6", translate("IPv6 Gateway"))
o.rmempty = true
o.placeholder="fe80::1"
o.datatype="ip6addr"
o:depends("ipv6", 1)
m.handle = function(self, state, data)
if state == FORM_VALID then
local name = data.name
local driver = data.driver
local internal = data.internal == 1 and true or false
local subnet = data.subnet
local gateway = data.gateway
local ip_range = data.ip_range
local aux_address = {}
local tmp = data.aux_address or {}
for i,v in ipairs(tmp) do
_,_,k1,v1 = v:find("(.-)=(.+)")
aux_address[k1] = v1
end
local options = {}
tmp = data.options or {}
for i,v in ipairs(tmp) do
_,_,k1,v1 = v:find("(.-)=(.+)")
options[k1] = v1
end
local ipv6 = data.ipv6 == 1 and true or false
local create_body = {
Name = name,
Driver = driver,
EnableIPv6 = ipv6,
IPAM = {
Driver= "default"
},
Internal = internal
}
if subnet or gateway or ip_range then
create_body["IPAM"]["Config"] = {
{
Subnet = subnet,
Gateway = gateway,
IPRange = ip_range,
AuxAddress = aux_address,
AuxiliaryAddresses = aux_address
}
}
end
if driver == "macvlan" then
create_body["Options"] = {
macvlan_mode = data.macvlan_mode,
parent = data.parent
}
elseif driver == "ipvlan" then
create_body["Options"] = {
ipvlan_mode = data.ipvlan_mode
}
elseif driver == "overlay" then
create_body["Ingress"] = data.ingerss == 1 and true or false
end
if ipv6 and data.subnet6 and data.subnet6 then
if type(create_body["IPAM"]["Config"]) ~= "table" then
create_body["IPAM"]["Config"] = {}
end
local index = #create_body["IPAM"]["Config"]
create_body["IPAM"]["Config"][index+1] = {
Subnet = data.subnet6,
Gateway = data.gateway6
}
end
if next(options) ~= nil then
create_body["Options"] = create_body["Options"] or {}
for k, v in pairs(options) do
create_body["Options"][k] = v
end
end
create_body = docker.clear_empty_tables(create_body)
docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
local res = dk.networks:create({
body = create_body
})
if res and res.code == 201 then
docker:write_status("Network: " .. "create macvlan interface...")
res = dk.networks:inspect({
name = create_body.Name
})
if driver == "macvlan" and
data.op_macvlan ~= 0 and
res and
res.code and
res.code == 200 and
res.body and
res.body.IPAM and
res.body.IPAM.Config and
res.body.IPAM.Config[1] and
res.body.IPAM.Config[1].Gateway and
res.body.IPAM.Config[1].Subnet then
docker.create_macvlan_interface(data.name,
data.parent,
res.body.IPAM.Config[1].Gateway,
res.body.IPAM.Config[1].Subnet)
end
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
else
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
end
end
end
return m

View File

@ -0,0 +1,151 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local uci = (require "luci.model.uci").cursor()
local m, s, o, lost_state
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
end
m = SimpleForm("dockerd",
translate("Docker - Overview"),
translate("An overview with the relevant data is displayed here with which the LuCI docker client is connected.")
..
" " ..
[[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..
translate("Github") ..
[[</a>]])
m.submit=false
m.reset=false
local docker_info_table = {}
-- docker_info_table['0OperatingSystem'] = {_key=translate("Operating System"),_value='-'}
-- docker_info_table['1Architecture'] = {_key=translate("Architecture"),_value='-'}
-- docker_info_table['2KernelVersion'] = {_key=translate("Kernel Version"),_value='-'}
docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'}
docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'}
docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'}
docker_info_table['6MemTotal'] = {_key=translate("Total Memory"),_value='-'}
docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value='-'}
docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'}
docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'}
if nixio.fs.access("/usr/bin/dockerd") and not uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template = "cbi/nullsection"
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = lost_state and translate("Start") or translate("Stop")
o.inputstyle = lost_state and "add" or "remove"
o.forcewrite = true
o.write = function(self, section)
docker:clear_status()
if lost_state then
docker:append_status("Docker daemon: starting")
luci.util.exec("/etc/init.d/dockerd start")
luci.util.exec("sleep 5")
luci.util.exec("/etc/init.d/dockerman start")
else
docker:append_status("Docker daemon: stopping")
luci.util.exec("/etc/init.d/dockerd stop")
end
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
end
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
docker:clear_status()
docker:append_status("Docker daemon: restarting")
luci.util.exec("/etc/init.d/dockerd restart")
luci.util.exec("sleep 5")
luci.util.exec("/etc/init.d/dockerman start")
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
end
end
s = m:section(Table, docker_info_table)
s:option(DummyValue, "_key", translate("Info"))
s:option(DummyValue, "_value")
s = m:section(SimpleSection)
s.template = "dockerman/overview"
s.containers_running = '-'
s.images_used = '-'
s.containers_total = '-'
s.images_total = '-'
s.networks_total = '-'
s.volumes_total = '-'
-- local socket = luci.model.uci.cursor():get("dockerd", "dockerman", "socket_path")
if not lost_state then
local containers_list = dk.containers:list({query = {all=true}}).body
local images_list = dk.images:list().body
local vol = dk.volumes:list()
local volumes_list = vol and vol.body and vol.body.Volumes or {}
local networks_list = dk.networks:list().body or {}
local docker_info = dk:info()
-- docker_info_table['0OperatingSystem']._value = docker_info.body.OperatingSystem
-- docker_info_table['1Architecture']._value = docker_info.body.Architecture
-- docker_info_table['2KernelVersion']._value = docker_info.body.KernelVersion
docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion
docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"]
docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
docker_info_table['6MemTotal']._value = docker.byte_format(docker_info.body.MemTotal)
if docker_info.body.DockerRootDir then
local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir)
local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0
docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(docker.byte_format(size)) .. " " .. translate("Available") .. ")"
end
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do
docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v)
end
s.images_used = 0
for i, v in ipairs(images_list) do
for ci,cv in ipairs(containers_list) do
if v.Id == cv.ImageID then
s.images_used = s.images_used + 1
break
end
end
end
s.containers_running = tostring(docker_info.body.ContainersRunning)
s.images_used = tostring(s.images_used)
s.containers_total = tostring(docker_info.body.Containers)
s.images_total = tostring(#images_list)
s.networks_total = tostring(#networks_list)
s.volumes_total = tostring(#volumes_list)
else
docker_info_table['3ServerVersion']._value = translate("Can NOT connect to docker daemon, please check!!")
end
return m

View File

@ -0,0 +1,142 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local dk = docker.new()
local m, s, o
local res, containers, volumes, lost_state
function get_volumes()
local data = {}
for i, v in ipairs(volumes) do
local index = v.Name
data[index]={}
data[index]["_selected"] = 0
data[index]["_nameraw"] = v.Name
data[index]["_name"] = v.Name:sub(1,12)
for ci,cv in ipairs(containers) do
if cv.Mounts and type(cv.Mounts) ~= "table" then
break
end
for vi, vv in ipairs(cv.Mounts) do
if v.Name == vv.Name then
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2)..'</a>'
end
end
end
data[index]["_driver"] = v.Driver
data[index]["_mountpoint"] = nil
for v1 in v.Mountpoint:gmatch('[^/]+') do
if v1 == index then
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
else
data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
end
end
data[index]["_created"] = v.CreatedAt
data[index]["_size"] = "<font class='volume_size_" .. v.Name .. "'>-</font>"
end
return data
end
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.volumes:list()
if res and res.code and res.code <300 then
volumes = res.body.Volumes
end
res = dk.containers:list({
query = {
all=true
}
})
if res and res.code and res.code <300 then
containers = res.body
end
end
local volume_list = not lost_state and get_volumes() or {}
m = SimpleForm("docker", translate("Docker - Volumes"))
m.submit=false
m.reset=false
m:append(Template("dockerman/volume_size"))
s = m:section(Table, volume_list, translate("Volumes overview"))
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.write = function(self, section, value)
volume_list[section]._selected = value
end
o = s:option(DummyValue, "_name", translate("Name"))
o = s:option(DummyValue, "_driver", translate("Driver"))
o = s:option(DummyValue, "_containers", translate("Containers"))
o.rawhtml = true
o = s:option(DummyValue, "_mountpoint", translate("Mount Point"))
o = s:option(DummyValue, "_size", translate("Size"))
o.rawhtml = true
o = s:option(DummyValue, "_created", translate("Created"))
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.disable = lost_state
o.write = function(self, section)
local volume_selected = {}
for k in pairs(volume_list) do
if volume_list[k]._selected == 1 then
volume_selected[#volume_selected+1] = k
end
end
if next(volume_selected) ~= nil then
local success = true
docker:clear_status()
for _,vol in ipairs(volume_selected) do
docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
local msg = dk.volumes["remove"](dk, {id = vol})
if msg and msg.code and msg.code ~= 204 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false
else
docker:append_status("done\n")
end
end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
end
end
return m

View File

@ -0,0 +1,507 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.docker"
local fs = require "nixio.fs"
local uci = (require "luci.model.uci").cursor()
local _docker = {}
_docker.options = {}
--pull image and return iamge id
local update_image = function(self, image_name)
local json_stringify = luci.jsonc and luci.jsonc.stringify
_docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n")
local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb)
if res and res.code and res.code == 200 and (#res.body > 0 and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image_name)) then
_docker:append_status("done\n")
else
res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
end
new_image_id = self.images:inspect({name = image_name}).body.Id
return new_image_id, res
end
local table_equal = function(t1, t2)
if not t1 then
return true
end
if not t2 then
return false
end
if #t1 ~= #t2 then
return false
end
for i, v in ipairs(t1) do
if t1[i] ~= t2[i] then
return false
end
end
return true
end
local table_subtract = function(t1, t2)
if not t1 or next(t1) == nil then
return nil
end
if not t2 or next(t2) == nil then
return t1
end
local res = {}
for _, v1 in ipairs(t1) do
local found = false
for _, v2 in ipairs(t2) do
if v1 == v2 then
found= true
break
end
end
if not found then
table.insert(res, v1)
end
end
return next(res) == nil and nil or res
end
local map_subtract = function(t1, t2)
if not t1 or next(t1) == nil then
return nil
end
if not t2 or next(t2) == nil then
return t1
end
local res = {}
for k1, v1 in pairs(t1) do
local found = false
for k2, v2 in ipairs(t2) do
if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then
found= true
break
end
end
if not found then
res[k1] = v1
end
end
return next(res) ~= nil and res or nil
end
_docker.clear_empty_tables = function ( t )
local k, v
if next(t) == nil then
t = nil
else
for k, v in pairs(t) do
if type(v) == 'table' then
t[k] = _docker.clear_empty_tables(v)
if t[k] and next(t[k]) == nil then
t[k] = nil
end
end
end
end
return t
end
local get_config = function(container_config, image_config)
local config = container_config.Config
local old_host_config = container_config.HostConfig
local old_network_setting = container_config.NetworkSettings.Networks or {}
if config.WorkingDir == image_config.WorkingDir then
config.WorkingDir = ""
end
if config.User == image_config.User then
config.User = ""
end
if table_equal(config.Cmd, image_config.Cmd) then
config.Cmd = nil
end
if table_equal(config.Entrypoint, image_config.Entrypoint) then
config.Entrypoint = nil
end
if table_equal(config.ExposedPorts, image_config.ExposedPorts) then
config.ExposedPorts = nil
end
config.Env = table_subtract(config.Env, image_config.Env)
config.Labels = table_subtract(config.Labels, image_config.Labels)
config.Volumes = map_subtract(config.Volumes, image_config.Volumes)
if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
config.ExposedPorts = {}
for p, v in pairs(old_host_config.PortBindings) do
config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
end
end
local network_setting = {}
local multi_network = false
local extra_network = {}
for k, v in pairs(old_network_setting) do
if multi_network then
extra_network[k] = v
else
network_setting[k] = v
end
multi_network = true
end
local host_config = old_host_config
host_config.Mounts = {}
for i, v in ipairs(container_config.Mounts) do
if v.Type == "volume" then
table.insert(host_config.Mounts, {
Type = v.Type,
Target = v.Destination,
Source = v.Source:match("([^/]+)\/_data"),
BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
ReadOnly = not v.RW
})
end
end
local create_body = config
create_body["HostConfig"] = host_config
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
create_body = _docker.clear_empty_tables(create_body) or {}
extra_network = _docker.clear_empty_tables(extra_network) or {}
return create_body, extra_network
end
local upgrade = function(self, request)
_docker:clear_status()
local container_info = self.containers:inspect({id = request.id})
if container_info.code > 300 and type(container_info.body) == "table" then
return container_info
end
local image_name = container_info.body.Config.Image
if not image_name:match(".-:.+") then
image_name = image_name .. ":latest"
end
local old_image_id = container_info.body.Image
local container_name = container_info.body.Name:sub(2)
local image_id, res = update_image(self, image_name)
if res and res.code and res.code ~= 200 then
return res
end
if image_id == old_image_id then
return {code = 305, body = {message = "Already up to date"}}
end
local t = os.date("%Y%m%d%H%M%S")
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old_".. t .. "...")
res = self.containers:rename({name = container_name, query = { name = container_name .. "_old_" ..t }})
if res and res.code and res.code < 300 then
_docker:append_status("done\n")
else
return res
end
local image_config = self.images:inspect({id = old_image_id}).body.Config
local create_body, extra_network = get_config(container_info.body, image_config)
-- create new container
_docker:append_status("Container: Create" .. " " .. container_name .. "...")
create_body = _docker.clear_empty_tables(create_body)
res = self.containers:create({name = container_name, body = create_body})
if res and res.code and res.code > 300 then
return res
end
_docker:append_status("done\n")
-- extra networks need to network connect action
for k, v in pairs(extra_network) do
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
if res and res.code and res.code > 300 then
return res
end
_docker:append_status("done\n")
end
_docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "_old_".. t .. "...")
res = self.containers:stop({name = container_name .. "_old_" ..t })
if res and res.code and res.code < 305 then
_docker:append_status("done\n")
else
return res
end
_docker:append_status("Container: " .. "Start" .. " " .. container_name .. "...")
res = self.containers:start({name = container_name})
if res and res.code and res.code < 305 then
_docker:append_status("done\n")
else
return res
end
_docker:clear_status()
return res
end
local duplicate_config = function (self, request)
local container_info = self.containers:inspect({id = request.id})
if container_info.code > 300 and type(container_info.body) == "table" then
return nil
end
local old_image_id = container_info.body.Image
local image_config = self.images:inspect({id = old_image_id}).body.Config
return get_config(container_info.body, image_config)
end
_docker.new = function()
local host = nil
local port = nil
local socket_path = nil
local debug_path = nil
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
host = uci:get("dockerd", "dockerman", "remote_host") or nil
port = uci:get("dockerd", "dockerman", "remote_port") or nil
else
socket_path = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
end
local debug = uci:get_bool("dockerd", "dockerman", "debug")
if debug then
debug_path = uci:get("dockerd", "dockerman", "debug_path") or "/tmp/.docker_debug"
end
local status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
_docker.options = {
host = host,
port = port,
socket_path = socket_path,
debug = debug,
debug_path = debug_path,
status_path = status_path
}
local _new = docker.new(_docker.options)
_new.containers_upgrade = upgrade
_new.containers_duplicate_config = duplicate_config
return _new
end
_docker.options.status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
_docker.append_status=function(self,val)
if not val then
return
end
local file_docker_action_status=io.open(self.options.status_path, "a+")
file_docker_action_status:write(val)
file_docker_action_status:close()
end
_docker.write_status=function(self,val)
if not val then
return
end
local file_docker_action_status=io.open(self.options.status_path, "w+")
file_docker_action_status:write(val)
file_docker_action_status:close()
end
_docker.read_status=function(self)
return fs.readfile(self.options.status_path)
end
_docker.clear_status=function(self)
fs.remove(self.options.status_path)
end
local status_cb = function(res, source, handler)
res.body = res.body or {}
while true do
local chunk = source()
if chunk then
--standard output to res.body
table.insert(res.body, chunk)
handler(chunk)
else
return
end
end
end
--{"status":"Pulling from library\/debian","id":"latest"}
--{"status":"Pulling fs layer","progressDetail":[],"id":"50e431f79093"}
--{"status":"Downloading","progressDetail":{"total":50381971,"current":2029978},"id":"50e431f79093","progress":"[==> ] 2.03MB\/50.38MB"}
--{"status":"Download complete","progressDetail":[],"id":"50e431f79093"}
--{"status":"Extracting","progressDetail":{"total":50381971,"current":17301504},"id":"50e431f79093","progress":"[=================> ] 17.3MB\/50.38MB"}
--{"status":"Pull complete","progressDetail":[],"id":"50e431f79093"}
--{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"}
--{"status":"Status: Downloaded newer image for debian:latest"}
_docker.pull_image_show_status_cb = function(res, source)
return status_cb(res, source, function(chunk)
local json_parse = luci.jsonc.parse
local step = json_parse(chunk)
if type(step) == "table" then
local buf = _docker:read_status()
local num = 0
local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
if step.id then
buf, num = buf:gsub("\t"..step.id .. ": .-\n", str)
end
if num == 0 then
buf = buf .. str
end
_docker:write_status(buf)
end
end)
end
--{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"}
--{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"}
--{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
_docker.import_image_show_status_cb = function(res, source)
return status_cb(res, source, function(chunk)
local json_parse = luci.jsonc.parse
local step = json_parse(chunk)
if type(step) == "table" then
local buf = _docker:read_status()
local num = 0
local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
if step.status then
buf, num = buf:gsub("\t"..step.status .. " .-\n", str)
end
if num == 0 then
buf = buf .. str
end
_docker:write_status(buf)
end
end)
end
_docker.create_macvlan_interface = function(name, device, gateway, subnet)
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
return
end
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
return
end
local ip = require "luci.ip"
local if_name = "docker_"..name
local dev_name = "macvlan_"..name
local net_mask = tostring(ip.new(subnet):mask())
local lan_interfaces
-- add macvlan device
uci:delete("network", dev_name)
uci:set("network", dev_name, "device")
uci:set("network", dev_name, "name", dev_name)
uci:set("network", dev_name, "ifname", device)
uci:set("network", dev_name, "type", "macvlan")
uci:set("network", dev_name, "mode", "bridge")
-- add macvlan interface
uci:delete("network", if_name)
uci:set("network", if_name, "interface")
uci:set("network", if_name, "proto", "static")
uci:set("network", if_name, "ifname", dev_name)
uci:set("network", if_name, "ipaddr", gateway)
uci:set("network", if_name, "netmask", net_mask)
uci:foreach("firewall", "zone", function(s)
if s.name == "lan" then
local interfaces
if type(s.network) == "table" then
interfaces = table.concat(s.network, " ")
uci:delete("firewall", s[".name"], "network")
else
interfaces = s.network and s.network or ""
end
interfaces = interfaces .. " " .. if_name
interfaces = interfaces:gsub("%s+", " ")
uci:set("firewall", s[".name"], "network", interfaces)
end
end)
uci:commit("firewall")
uci:commit("network")
os.execute("ifup " .. if_name)
end
_docker.remove_macvlan_interface = function(name)
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
return
end
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
return
end
local if_name = "docker_"..name
local dev_name = "macvlan_"..name
uci:foreach("firewall", "zone", function(s)
if s.name == "lan" then
local interfaces
if type(s.network) == "table" then
interfaces = table.concat(s.network, " ")
else
interfaces = s.network and s.network or ""
end
interfaces = interfaces and interfaces:gsub(if_name, "")
interfaces = interfaces and interfaces:gsub("%s+", " ")
uci:set("firewall", s[".name"], "network", interfaces)
end
end)
uci:delete("network", dev_name)
uci:delete("network", if_name)
uci:commit("network")
uci:commit("firewall")
os.execute("ip link del " .. if_name)
end
_docker.byte_format = function (byte)
if not byte then return 'NaN' end
local suff = {"B", "KB", "MB", "GB", "TB"}
for i=1, 5 do
if byte > 1024 and i < 5 then
byte = byte / 1024
else
return string.format("%.2f %s", byte, suff[i])
end
end
end
return _docker

View File

@ -0,0 +1,147 @@
<style type="text/css">
#docker_apply_overlay {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
display: none;
z-index: 20000;
}
#docker_apply_overlay .alert-message {
position: relative;
top: 10%;
width: 60%;
margin: auto;
display: flex;
flex-wrap: wrap;
min-height: 32px;
align-items: center;
}
#docker_apply_overlay .alert-message > h4,
#docker_apply_overlay .alert-message > p,
#docker_apply_overlay .alert-message > div {
flex-basis: 100%;
}
#docker_apply_overlay .alert-message > img {
margin-right: 1em;
flex-basis: 32px;
}
body.apply-overlay-active {
overflow: hidden;
height: 100vh;
}
body.apply-overlay-active #docker_apply_overlay {
display: block;
}
</style>
<script type="text/javascript">//<![CDATA[
var xhr = new XHR(),
uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 90, 90)%>,
uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>,
uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>,
uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>,
was_xhr_poll_running = false;
function docker_status_message(type, content) {
document.getElementById('docker_apply_overlay') || document.body.insertAdjacentHTML("beforeend",'<div id="docker_apply_overlay"><div class="alert-message"></div></div>')
var overlay = document.getElementById('docker_apply_overlay')
message = overlay.querySelector('.alert-message');
if (message && type) {
if (!message.classList.contains(type)) {
message.classList.remove('notice');
message.classList.remove('warning');
message.classList.add(type);
}
if (content)
message.innerHTML = content;
document.body.classList.add('apply-overlay-active');
document.body.scrollTop = document.documentElement.scrollTop = 0;
if (!was_xhr_poll_running) {
was_xhr_poll_running = XHR.running();
XHR.halt();
}
}
else {
document.body.classList.remove('apply-overlay-active');
if (was_xhr_poll_running)
XHR.run();
}
}
var loading_msg="Loading.."
function uci_confirm_docker() {
var tt;
docker_status_message('notice');
var call = function(r, resjson, duration) {
if (r && r.status === 200 ) {
var indicator = document.querySelector('.uci_change_indicator');
if (indicator) indicator.style.display = 'none';
docker_status_message('notice', '<%:Docker actions done.%>');
document.body.classList.remove('apply-overlay-active');
window.clearTimeout(tt);
return;
}
loading_msg = resjson?resjson.info:loading_msg
// var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
var delay =1000
window.setTimeout(function() {
xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000);
}, delay);
};
var tick = function() {
var now = Date.now();
docker_status_message('notice',
'<img src="<%=resource%>/icons/loading.gif" alt="" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
loading_msg + '</span>');
tt = window.setTimeout(tick, 200);
ts = now;
};
tick();
/* wait a few seconds for the settings to become effective */
window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1));
}
// document.getElementsByTagName("form")[0].addEventListener("submit", (e)=>{
// uci_confirm_docker()
// })
function fnSubmitForm(el){
if (el.id != "cbid.table.1._new") {
uci_confirm_docker()
}
}
<% if self.err then -%>
docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>');
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{
docker_status_message()
})
<%- end %>
window.onload= function (){
var buttons = document.querySelectorAll('input[type="submit"]');
[].slice.call(buttons).forEach(function (el) {
el.onclick = fnSubmitForm.bind(this, el);
});
if(typeof(fnWindowLoad) == "function"){
fnWindowLoad()
}
}
//]]></script>

View File

@ -0,0 +1,7 @@
<div style="display: inline-block;">
<% if self:cfgvalue(section) ~= false then %>
<input class="btn cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
<% else %>
-
<% end %>
</div>

View File

@ -0,0 +1,33 @@
<div style="display: inline-block;">
<!-- <%- if self.title then -%>
<label class="cbi-value-title"<%= attr("for", cbid) %>>
<%- if self.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=self.titleref%>"><%- end -%>
<%-=self.title-%>
<%- if self.titleref then -%></a><%- end -%>
</label>
<%- end -%> -->
<%- if self.password then -%>
<input type="password" style="position:absolute; left:-100000px" aria-hidden="true"<%=
attr("name", "password." .. cbid)
%> />
<%- end -%>
<input data-update="change"<%=
attr("id", cbid) ..
attr("name", cbid) ..
attr("type", self.password and "password" or "text") ..
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
attr("value", self:cfgvalue(section) or self.default) ..
ifattr(self.password, "autocomplete", "new-password") ..
ifattr(self.size, "size") ..
ifattr(self.placeholder, "placeholder") ..
ifattr(self.readonly, "readonly") ..
ifattr(self.maxlength, "maxlength") ..
ifattr(self.datatype, "data-type", self.datatype) ..
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
%> />
<%- if self.password then -%>
<div class="btn cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'"></div>
<% end %>
</div>

View File

@ -0,0 +1,9 @@
<% if self:cfgvalue(self.section) then section = self.section %>
<div class="cbi-section" id="cbi-<%=self.config%>-<%=section%>">
<%+cbi/tabmenu%>
<div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>">
<%+cbi/ucisection%>
</div>
</div>
<% end %>
<!-- /nsection -->

View File

@ -0,0 +1,10 @@
<%+cbi/valueheader%>
<input type="hidden" value="1"<%=
attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option)
%> />
<input class="cbi-input-checkbox" data-update="click change" type="checkbox" <% if self.disable == 1 then %>disabled <% end %><%=
attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) ..
ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked")
%> />
<label<%= attr("for", cbid)%>></label>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,28 @@
<br>
<ul class="cbi-tabmenu">
<li id="cbi-tab-container_info"><a id="a-cbi-tab-container_info" href=""><%:Info%></a></li>
<li id="cbi-tab-container_resources"><a id="a-cbi-tab-container_resources" href=""><%:Resources%></a></li>
<li id="cbi-tab-container_stats"><a id="a-cbi-tab-container_stats" href=""><%:Stats%></a></li>
<li id="cbi-tab-container_file"><a id="a-cbi-tab-container_file" href=""><%:File%></a></li>
<li id="cbi-tab-container_console"><a id="a-cbi-tab-container_console" href=""><%:Console%></a></li>
<li id="cbi-tab-container_inspect"><a id="a-cbi-tab-container_inspect" href=""><%:Inspect%></a></li>
<li id="cbi-tab-container_logs"><a id="a-cbi-tab-container_logs" href=""><%:Logs%></a></li>
</ul>
<script type="text/javascript">
let re = /\/admin\/docker\/container\//
let p = window.location.href
let path = p.split(re)
let container_id = path[1].split('/')[0] || path[1]
let action = path[1].split('/')[1] || "info"
let actions=["info","resources","stats","file","console","logs","inspect"]
actions.forEach(function(item) {
document.getElementById("a-cbi-tab-container_" + item).href= path[0]+"/admin/docker/container/"+container_id+'/'+item
if (action === item) {
document.getElementById("cbi-tab-container_" + item).className="cbi-tab"
}
else {
document.getElementById("cbi-tab-container_" + item).className="cbi-tab-disabled"
}
})
</script>

View File

@ -0,0 +1,6 @@
<div class="cbi-map">
<iframe id="terminal" style="width: 100%; min-height: 500px; border: none; border-radius: 3px;"></iframe>
</div>
<script type="text/javascript">
document.getElementById("terminal").src = window.location.protocol + "//" + window.location.hostname + ":7682";
</script>

View File

@ -0,0 +1,332 @@
<link rel="stylesheet" href="/luci-static/resources/dockerman/file-manager.css?v=@ver">
<fieldset class="cbi-section fb-container">
<input id="current-path" type="text" class="current-path cbi-input-text" value="/" />
<div class="panel-container">
<input type="file" name="upload_archive" accept="*/*"
style="visibility:hidden; position: absolute;top: 0px; left: 0px;" multiple="multiple" id="upload_archive" />
<button id="upload-file" class="upload-toggle cbi-button cbi-button-edit"><%:Upload%></button>
</div>
<div id="list-content"></div>
</fieldset>
<script type="text/javascript" src="<%=resource%>/dockerman/tar.min.js"></script>
<script>
String.prototype.replaceAll = function (search, replacement) {
var target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
(function () {
var iwxhr = new XHR();
var listElem = document.getElementById("list-content");
listElem.onclick = handleClick;
var currentPath;
var pathElem = document.getElementById("current-path");
pathElem.onblur = function () {
update_list(this.value.trim());
};
pathElem.onkeydown = function (evt) {
if (evt.keyCode == 13) {
this.blur()
evt.preventDefault()
}
};
function removePath(filename, isdir) {
var c = confirm('!!!<%:DELETING%> ' + filename + ' ... <%:PLEASE CONFIRM%>!!!');
if (c) {
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_remove_file")%>/<%=self.container%>',
{
path: concatPath(currentPath, filename),
isdir: isdir
},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, currentPath);
}
});
}
}
function renamePath(filename) {
var newname = prompt('%:Please input new filename%>: ', filename);
if (newname) {
newname = newname.trim();
if (newname != filename) {
var newpath = concatPath(currentPath, newname);
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_rename_file")%>/<%=self.container%>',
{
filepath: concatPath(currentPath, filename),
newpath: newpath
},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, currentPath);
}
}
);
}
}
}
function openpath(filename, dirname) {
dirname = dirname || currentPath;
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path='
+ encodeURIComponent(dirname + '/' + filename) + '&filename='
+ encodeURIComponent(filename))
}
function getFileElem(elem) {
if (elem.className.indexOf('-icon') > -1) {
return elem;
}
else if (elem.parentNode.className.indexOf('-icon') > -1) {
return elem.parentNode;
}
}
function concatPath(path, filename) {
if (path === '/') {
return path + filename;
}
else {
return path.replace(/\/$/, '') + '/' + filename;
}
}
function handleClick(evt) {
// evt.preventDefault();
var targetElem = evt.target;
var infoElem;
if (targetElem.className.indexOf('cbi-button-remove') > -1) {
infoElem = targetElem.parentNode.parentNode;
removePath(infoElem.dataset['filename'], infoElem.dataset['isdir'])
evt.preventDefault();
location.reload()
}
else if (targetElem.className.indexOf('cbi-button-download') > -1) {
infoElem = targetElem.parentNode.parentNode;
openpath(targetElem.parentNode.parentNode.dataset['filename']);
evt.preventDefault();
}
else if (targetElem.className.indexOf('cbi-button-rename') > -1) {
renamePath(targetElem.parentNode.parentNode.dataset['filename']);
evt.preventDefault();
location.reload()
}
else if (targetElem = getFileElem(targetElem)) {
if (targetElem.className.indexOf('parent-icon') > -1) {
update_list(currentPath.replace(/\/[^/]+($|\/$)/, ''));
}
else if (targetElem.className.indexOf('file-icon') > -1) {
openpath(targetElem.parentNode.dataset['filename']);
}
else if (targetElem.className.indexOf('link-icon') > -1) {
infoElem = targetElem.parentNode;
var filepath = infoElem.dataset['linktarget'];
if (filepath) {
if (infoElem.dataset['isdir'] === "1") {
update_list(filepath);
}
else {
var lastSlash = filepath.lastIndexOf('/')
openpath(filepath.substring(lastSlash + 1), filepath.substring(0, lastSlash));
}
}
}
else if (targetElem.className.indexOf('folder-icon') > -1) {
update_list(concatPath(currentPath, targetElem.parentNode.dataset['filename']))
}
}
}
function refresh_list(filenames, path) {
var listHtml = '<table class="cbi-section-table"><tbody>';
if (path !== '/') {
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-2"><td class="parent-icon" colspan="6"><strong>..</strong></td></tr>';
}
if (filenames) {
for (var i = 0; i < filenames.length; i++) {
var line = filenames[i]
if (line) {
var f = line.match(/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\S\s]+)/);
var isLink = f[1][0] === 'z' || f[1][0] === 'l' || f[1][0] === 'x';
var o = {
displayname: f[9],
filename: isLink ? f[9].split(' -> ')[0] : f[9],
perms: f[1],
date: f[7] + ' ' + f[6] + ' ' + f[8],
size: f[5],
owner: f[3],
icon: (f[1][0] === 'd') ? "folder-icon" : (isLink ? "link-icon" : "file-icon")
};
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-' + (1 + i % 2)
+ '" data-filename="' + o.filename + '" data-isdir="' + Number(f[1][0] === 'd' || f[1][0] === 'z') + '"'
+ ((f[1][0] === 'z' || f[1][0] === 'l') ? (' data-linktarget="' + f[9].split(' -> ')[1]) : '')
+ '">'
+ '<td class="cbi-value-field ' + o.icon + '">'
+ '<strong>' + o.displayname + '</strong>'
+ '</td>'
+ '<td class="cbi-value-field cbi-value-owner">' + o.owner + '</td>'
+ '<td class="cbi-value-field cbi-value-date">' + o.date + '</td>'
+ '<td class="cbi-value-field cbi-value-size">' + o.size + '</td>'
+ '<td class="cbi-value-field cbi-value-perm">' + o.perms + '</td>'
+ '<td class="cbi-section-table-cell">\
<button class="btn cbi-button cbi-button-rename cbi-button-edit">'+ "<%:Rename%>" + '</button>\
<button class="btn cbi-button cbi-button-download cbi-button-add">'+ "<%:Download%>" + '</button>\
<button class="btn cbi-button cbi-button-remove">'+ "<%:Remove%>" + '</button>\
</td>'
+ '</tr>';
}
}
}
listHtml += "</table>";
listElem.innerHTML = listHtml;
}
function update_list(path, opt) {
opt = opt || {};
path = concatPath(path, '');
if (currentPath != path) {
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_list_file")%>/<%=self.container%>',
{ path: path },
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, path);
}
else {
refresh_list([], path);
}
}
);
if (!opt.popState) {
history.pushState({ path: path }, null, '?path=' + path);
}
currentPath = path;
pathElem.value = currentPath;
}
};
async function file2Tar(tarFile, fileToLoad) {
if (! fileToLoad) return
function file2Byte(file) {
return new Promise((resolve, reject) => {
var fileReader = new FileReader();
fileReader.onerror = () => {
fileReader.abort();
reject(new DOMException("Problem parsing input file."));
};
fileReader.onload = (fileLoadedEvent) => {
resolve(ByteHelper.stringUTF8ToBytes(fileLoadedEvent.target.result));
}
fileReader.readAsBinaryString(file);
})
}
const x = await file2Byte(fileToLoad)
return fileByte2Tar(tarFile, fileToLoad.name, x).downloadAs(fileToLoad.name + ".tar")
}
function fileByte2Tar(tarFile, fileName, fileBytes) {
if (!tarFile) tarFile = TarFile.create(fileName)
var tarHeader = TarFileEntryHeader.default();
var tarFileEntryHeader = new TarFileEntryHeader
(
// ByteHelper.bytesToStringUTF8(fileName),
fileName,
tarHeader.fileMode,
tarHeader.userIDOfOwner,
tarHeader.userIDOfGroup,
fileBytes.length, // fileSizeInBytes,
tarHeader.timeModifiedInUnixFormat, // todo
0, // checksum,
TarFileTypeFlag.Instances().Normal,
tarHeader.nameOfLinkedFile,
tarHeader.uStarIndicator,
tarHeader.uStarVersion,
tarHeader.userNameOfOwner,
tarHeader.groupNameOfOwner,
tarHeader.deviceNumberMajor,
tarHeader.deviceNumberMinor,
tarHeader.filenamePrefix
);
tarFileEntryHeader.checksumCalculate();
var entryForFileToAdd = new TarFileEntry
(
tarFileEntryHeader,
fileBytes
);
tarFile.entries.push(entryForFileToAdd);
return tarFile
}
var btnUpload = document.getElementById('upload-file');
btnUpload.onclick = function (e) {
document.getElementById("upload_archive").click()
e.preventDefault()
}
let fileLoad = document.getElementById('upload_archive')
fileLoad.onchange = async function (e) {
let uploadArchive = document.getElementById('upload_archive')
// let uploadPath = document.getElementById('path').value
if (!uploadArchive.value) {
docker_status_message('warning', "<%:Please input the PATH and select the file !%>")
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
docker_status_message()
})
return
}
docker_status_message('notice',
'<img src="<%=resource%>/icons/loading.gif" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
'Uploading...' + '</span>');
Globals.Instance.tarFile = TarFile.create("Archive.tar")
let bytesToWriteAsBlob
for (let i = 0; i < uploadArchive.files.length; i++) {
let fileName = uploadArchive.files[i].name
bytesToWriteAsBlob = await file2Tar(Globals.Instance.tarFile, uploadArchive.files[i])
}
let formData = new FormData()
formData.append('upload-filename', "Archive.tar")
formData.append('upload-path', concatPath(currentPath, ''))
formData.append('upload-archive', bytesToWriteAsBlob)
let xhr = new XMLHttpRequest()
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/container_put_archive")%>/<%=self.container%>', true)
xhr.onload = function () {
if (xhr.status == 200) {
uploadArchive.value = ''
docker_status_message('notice', "<%:Upload Success%>")
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
sleep(800).then(() => {
location.reload()
})
}
else {
// docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText)
docker_status_message('warning', "<%:Upload Error%>:" + '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
JSON.parse(xhr.response).message + '</span>')
}
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
docker_status_message()
})
}
xhr.send(formData)
}
document.addEventListener('DOMContentLoaded', function (evt) {
var initPath = '/';
if (/path=([/\w\.\-\_]+)/.test(location.search)) {
initPath = RegExp.$1;
}
update_list(initPath, { popState: true });
});
window.addEventListener('popstate', function (evt) {
var path = '/';
if (evt.state && evt.state.path) {
path = evt.state.path;
}
update_list(path, { popState: true });
});
})();
</script>

View File

@ -0,0 +1,81 @@
<script type="text/javascript">//<![CDATA[
let last_bw_tx
let last_bw_rx
let interval = 3
function progressbar(v, m, pc, np, f) {
m = m || 100
return String.format(
'<div style="width:100%%; max-width:500px; position:relative; border:1px solid #999999">' +
'<div style="background-color:#CCCCCC; width:%d%%; height:15px">' +
'<div style="position:absolute; left:0; top:0; text-align:center; width:100%%; color:#000000">' +
'<small>%s '+(f?f:'/')+' %s ' + (np ? "" : '(%d%%)') + '</small>' +
'</div>' +
'</div>' +
'</div>', pc, v, m, pc, f
);
}
function niceBytes(bytes, decimals) {
if (bytes == 0) return '0 Bytes';
var k = 1000,
dm = decimals + 1 || 3,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/<%=self.container_id%>', { status: 1 },
function (x, info) {
var e;
if (e = document.getElementById('cbi-table-cpu-value'))
e.innerHTML = progressbar(
(info.cpu_percent), 100, (info.cpu_percent ? info.cpu_percent : 0));
if (e = document.getElementById('cbi-table-memory-value'))
e.innerHTML = progressbar(
niceBytes(info.memory.mem_useage),
niceBytes(info.memory.mem_limit),
((100 / (info.memory.mem_limit ? info.memory.mem_limit : 100)) * (info.memory.mem_useage ? info.memory.mem_useage : 0))
);
for (var eth in info.bw_rxtx) {
if (!document.getElementById("cbi-table-speed_" + eth + "-value")) {
let tab = document.getElementById("cbi-table-cpu").parentNode
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
div.id = "cbi-table-speed_" + eth;
div.children[0].innerHTML = "<%:Upload/Download%>: " + eth
div.children[1].id = "cbi-table-speed_" + eth + "-value"
tab.appendChild(div)
}
if (!document.getElementById("cbi-table-network_" + eth + "-value")) {
let tab = document.getElementById("cbi-table-cpu").parentNode
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
div.id = "cbi-table-network_" + eth;
div.children[0].innerHTML = "<%:TX/RX%>: " + eth
div.children[1].id = "cbi-table-network_" + eth + "-value"
tab.appendChild(div)
}
e = document.getElementById("cbi-table-network_" + eth + "-value")
e.innerHTML = progressbar(
'↑'+niceBytes(info.bw_rxtx[eth].bw_tx),
'↓'+niceBytes(info.bw_rxtx[eth].bw_rx),
null,
true, " "
);
e = document.getElementById("cbi-table-speed_" + eth + "-value")
if (! last_bw_tx) last_bw_tx = info.bw_rxtx[eth].bw_tx
if (! last_bw_rx) last_bw_rx = info.bw_rxtx[eth].bw_rx
e.innerHTML = progressbar(
'↑'+niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx)/interval)+'/s',
'↓'+niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx)/interval)+'/s',
null,
true, " "
);
last_bw_tx = info.bw_rxtx[eth].bw_tx
last_bw_rx = info.bw_rxtx[eth].bw_rx
}
});
//]]></script>

View File

@ -0,0 +1,91 @@
<script type="text/javascript">
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function niceBytes(x) {
let l = 0, n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
}
fnWindowLoad = function () {
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
if(!info || !info.Containers || !info.Containers.forEach) return
info.Containers.forEach(item=>{
const size_c = document.getElementsByClassName("container_size_" + item.Id)
size_c[0].title = "RW Size: " + niceBytes(item.SizeRw) + " / RootFS Size(Include Image): " + niceBytes(item.SizeRootFs)
size_c[0].innerText = "Size: " + niceBytes(item.SizeRw) + "/" + niceBytes(item.SizeRootFs)
})
})
let lines = document.querySelectorAll('[id^=cbi-containers-]')
let last_bw_tx = {}
let last_bw_rx = {}
let interval = 30
let containers = []
lines.forEach((item) => {
let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
if (!containerId) { return }
containerId = containerId[1]
if (item.getElementsByClassName("container_not_running").length > 0) { return }
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/' + containerId, null, (x, info) => {
// handle stats info
if (!info) { return }
item.childNodes.forEach((cell) => {
if (cell && cell.attributes) {
if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
let runningStats = cell.getElementsByClassName("container_cpu_status")
runningStats[0].innerText = "CPU: " + info.cpu_percent + "%"
runningStats = cell.getElementsByClassName("container_mem_status")
runningStats[0].innerText = "MEM: " + niceBytes(info.memory.mem_useage)
runningStats = cell.getElementsByClassName("container_network_status")
for (var eth in info.bw_rxtx) {
if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
runningStats[0].innerText = '↑' + niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
}
last_bw_rx[containerId] = info.bw_rxtx[eth].bw_rx
last_bw_tx[containerId] = info.bw_rxtx[eth].bw_tx
}
}
}
})
})
// containers.push(containerId)
})
// XHR.post('<%=luci.dispatcher.build_url("admin/docker/containers_stats")%>', {
// containers: JSON.stringify(containers)
// }, (x, info) => {
// lines.forEach((item) => {
// if (!info) { return }
// let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
// if (!containerId) { return }
// containerId = containerId[1]
// if (!info[containerId]) { return }
// infoC = info[containerId]
// if (item.getElementsByClassName("container_not_running").length > 0) { return }
// item.childNodes.forEach((cell) => {
// if (cell && cell.attributes) {
// if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
// let runningStats = cell.getElementsByClassName("container_cpu_status")
// runningStats[0].innerText = "CPU: " + infoC.cpu_percent + "%"
// runningStats = cell.getElementsByClassName("container_mem_status")
// runningStats[0].innerText = "MEM: " + niceBytes(infoC.memory.mem_useage)
// runningStats = cell.getElementsByClassName("container_network_status")
// for (var eth in infoC.bw_rxtx) {
// if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
// runningStats[0].innerText = '↑' + niceBytes((infoC.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((infoC.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
// }
// last_bw_rx[containerId] = infoC.bw_rxtx[eth].bw_rx
// last_bw_tx[containerId] = infoC.bw_rxtx[eth].bw_tx
// }
// }
// }
// })
// })
// })
XHR.run()
XHR.halt()
}
</script>

View File

@ -0,0 +1,104 @@
<input type="text" class="cbi-input-text" name="isrc" placeholder="http://host/image.tar" id="isrc" />
<input type="text" class="cbi-input-text" name="itag" placeholder="repository:tag" id="itag" />
<div style="display: inline-block;">
<input type="button"" class="btn cbi-button cbi-button-add" id="btnimport" name="import" value="<%:Import%>" <% if self.disable then %>disabled <% end %>/>
<input type="file" id="file_import" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" />
</div>
<script type="text/javascript">
let btnImport = document.getElementById('btnimport')
let valISrc = document.getElementById('isrc')
let valITag = document.getElementById('itag')
btnImport.onclick = function (e) {
if (valISrc.value == "") {
document.getElementById("file_import").click()
return
}
else {
let formData = new FormData()
formData.append('src', valISrc.value)
formData.append('tag', valITag.value)
let xhr = new XMLHttpRequest()
uci_confirm_docker()
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
xhr.onload = function () {
location.reload()
}
xhr.send(formData)
}
}
let fileimport = document.getElementById('file_import')
fileimport.onchange = function (e) {
let fileimport = document.getElementById('file_import')
if (!fileimport.value) {
return
}
let valITag = document.getElementById('itag')
let fileName = fileimport.files[0].name
let formData = new FormData()
formData.append('upload-filename', fileName)
formData.append('tag', valITag.value)
formData.append('upload-archive', fileimport.files[0])
let xhr = new XMLHttpRequest()
uci_confirm_docker()
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
xhr.onload = function () {
fileimport.value = ''
location.reload()
}
xhr.send(formData)
}
let new_tag = function (image_id) {
let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "")
if (new_tag) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>",
{
id: image_id,
tag: new_tag
},
function (r) {
if (r.status == 201) {
location.reload()
}
else {
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message()
}
)
}
}
)
}
}
let un_tag = function (tag) {
if (tag.match("<none>"))
return
if (confirm("<%:Remove tag%>: " + tag + " ?")) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>",
{
tag: tag
},
function (r) {
if (r.status == 200) {
location.reload()
}
else {
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message()
}
)
}
}
)
}
}
</script>

View File

@ -0,0 +1,40 @@
<div style="display: inline-block;">
<input type="button"" class="btn cbi-button cbi-button-add" id="btnload" name="load" value="<%:Load%>" <% if self.disable then %>disabled <% end %>/>
<input type="file" id="file_load" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" accept="application/x-tar" />
</div>
<script type="text/javascript">
let btnLoad = document.getElementById('btnload')
btnLoad.onclick = function (e) {
document.getElementById("file_load").click()
e.preventDefault()
}
let fileLoad = document.getElementById('file_load')
fileLoad.onchange = function(e){
let fileLoad = document.getElementById('file_load')
if (!fileLoad.value) {
return
}
let fileName = fileLoad.files[0].name
let formData = new FormData()
formData.append('upload-filename', fileName)
formData.append('upload-archive', fileLoad.files[0])
let xhr = new XMLHttpRequest()
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true)
xhr.onload = function() {
if (xhr.status == 200) {
docker_status_message('notice', xhr.statusText)
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
sleep(1500).then(() => {
location.reload()
})
} else {
location.reload()
}
}
uci_confirm_docker()
xhr.send(formData)
}
</script>

View File

@ -0,0 +1,13 @@
<% if self.title == "Events" then %>
<%+header%>
<h2 name="content"><%:Docker - Events%></h2>
<div class="cbi-section">
<h3><%:Events%></h3>
<% end %>
<div id="content_syslog">
<textarea readonly="readonly" wrap="off" rows="<%=self.syslog:cmatch('\n')+2%>" id="syslog"><%=self.syslog:pcdata()%></textarea>
</div>
<% if self.title == "Events" then %>
</div>
<%+footer%>
<% end %>

View File

@ -0,0 +1,102 @@
<style type="text/css">
#dialog_reslov {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
display: none;
z-index: 20000;
}
#dialog_reslov .dialog_box {
position: relative;
background: rgba(255, 255, 255);
top: 10%;
width: 50%;
margin: auto;
display: flex;
flex-wrap: wrap;
height:auto;
align-items: center;
}
#dialog_reslov .dialog_line {
margin-top: .5em;
margin-bottom: .5em;
margin-left: 2em;
margin-right: 2em;
}
#dialog_reslov .dialog_box>h4,
#dialog_reslov .dialog_box>p,
#dialog_reslov .dialog_box>div {
flex-basis: 100%;
}
#dialog_reslov .dialog_box>img {
margin-right: 1em;
flex-basis: 32px;
}
body.dialog-reslov-active {
overflow: hidden;
height: 100vh;
}
body.dialog-reslov-active #dialog_reslov {
display: block;
}
</style>
<script type="text/javascript">
function close_reslov_dialog() {
document.body.classList.remove('dialog-reslov-active')
document.documentElement.style.overflowY = 'scroll'
}
function reslov_container() {
let s = document.getElementById('cmd-line-status')
if (!s)
return
let cmd_line = document.getElementById("dialog_reslov_text").value;
if (cmd_line == null || cmd_line == "") {
return
}
cmd_line = cmd_line.replace(/(^\s*)/g,"")
if (!cmd_line.match(/^docker\s+(run|create)/)) {
s.innerHTML = "<font color='red'><%:Command line Error%></font>"
return
}
let reg_space = /\s+/g
let reg_muti_line= /\\\s*\n/g
// reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# `
let reg_rem =/`#.+`/g// the command has `# `
cmd_line = cmd_line.replace(/^docker\s+(run|create)/,"DOCKERCLI").replace(reg_rem, " ").replace(reg_muti_line, " ").replace(reg_space, " ")
console.log(cmd_line)
window.location.href = '<%=luci.dispatcher.build_url("admin/docker/newcontainer")%>/' + encodeURI(cmd_line)
}
function clear_text(){
let s = document.getElementById('cmd-line-status')
s.innerHTML = ""
}
function show_reslov_dialog() {
document.getElementById('dialog_reslov') || document.body.insertAdjacentHTML("beforeend", '<div id="dialog_reslov"><div class="dialog_box"><div class="dialog_line"></div><div class="dialog_line"><span><%:Plese input <docker create/run> command line:%></span><br><span id="cmd-line-status"></span></div><div class="dialog_line"><textarea class="cbi-input-textarea" id="dialog_reslov_text" style="width: 100%; height:100%;" rows="15" onkeyup="clear_text()" placeholder="docker run -d alpine sh"></textarea></div><div class="dialog_line" style="text-align: right;"><input type="button" class="btn cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()"/> <input type="button" class="btn cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_reslov_dialog()" /></div><div class="dialog_line"></div></div></div>')
document.body.classList.add('dialog-reslov-active')
let s = document.getElementById('cmd-line-status')
s.innerHTML = ""
document.documentElement.style.overflowY = 'hidden'
}
</script>
<%+cbi/valueheader%>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" />
<%+cbi/valuefooter%>

View File

@ -0,0 +1,197 @@
<style>
/*!
Pure v1.0.1
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE.md
*/
.pure-g {
letter-spacing: -.31em;
text-rendering: optimizespeed;
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-flow: row wrap;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-webkit-align-content: flex-start;
-ms-flex-line-pack: start;
align-content: flex-start
}
.pure-u {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-g [class*=pure-u] {
font-family: sans-serif
}
.pure-u-1-4,
.pure-u-2-5,
.pure-u-3-5 {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-u-1-4 {
width: 25%
}
.pure-u-2-5 {
width: 40%
}
.pure-u-3-5 {
width: 60%
}
.status {
margin: 1rem -0.5rem 1rem -0.5rem;
}
.block {
margin: 0.5rem 0.5rem;
padding: 0;
font-weight: normal;
font-style: normal;
line-height: 1;
font-family: inherit;
min-width: inherit;
overflow-x: auto;
overflow-y: hidden;
border: 1px solid rgba(0, 0, 0, .05);
border-radius: .375rem;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
}
.img-con {
margin: 1rem;
min-width: 4rem;
max-width: 4rem;
min-height: 4rem;
max-height: 4rem;
}
.block h4 {
font-size: .8125rem;
font-weight: 600;
margin: 1rem;
color: #8898aa !important;
line-height: 1.8em;
}
.cbi-section-table-cell {
position: relative;
}
@media screen and (max-width: 700px) {
.pure-u-1-4 {
width: 50%;
}
.cbi-button-add {
position: fixed;
padding: 0.3rem 0.5rem;
z-index: 1000;
width: 50px !important;
height: 50px;
bottom: 90px;
right: 5px;
font-size: 16px;
border-radius: 50%;
display: block;
background-color: #fb6340 !important;
border-color: #fb6340 !important;
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75);
}
}
</style>
<div class="pure-g status">
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<img src="<%=resource%>/dockerman/containers.svg" />
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4>
<h4 style="text-align: right;">
<%- if self.containers_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/containers")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span>
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span>
<%- if self.containers_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<img src="<%=resource%>/dockerman/images.svg" />
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Images%></h4>
<h4 style="text-align: right;">
<%- if self.images_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/images")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.images_used%></span>
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span>
<%- if self.images_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<img src="<%=resource%>/dockerman/networks.svg" />
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4>
<h4 style="text-align: right;">
<%- if self.networks_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/networks")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<%- if self.networks_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<img src="<%=resource%>/dockerman/volumes.svg" />
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Volumes%></h4>
<h4 style="text-align: right;">
<%- if self.volumes_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/volumes")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.volumes_total%></span>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<%- if self.volumes_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
<script type="text/javascript">
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function niceBytes(x) {
let l = 0, n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
}
fnWindowLoad = function () {
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
if(!info || !info.Volumes || !info.Volumes.forEach) return
info.Volumes.forEach(item=>{
console.log(info)
const size_c = document.getElementsByClassName("volume_size_" + item.Name)
size_c[0].innerText = item.UsageData ? niceBytes(item.UsageData.Size) : '-'
})
})
}
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
zh-cn

View File

@ -0,0 +1,14 @@
#!/bin/sh
/init.sh env
touch /etc/config/dockerd
uci set dockerd.dockerman=dockerman
uci set dockerd.dockerman.socket_path=`uci get dockerd.dockerman.socket_path 2&> /dev/null || echo '/var/run/docker.sock'`
uci set dockerd.dockerman.status_path=`uci get dockerd.dockerman.status_path 2&> /dev/null || echo '/tmp/.docker_action_status'`
uci set dockerd.dockerman.debug=`uci get dockerd.dockerman.debug 2&> /dev/null || echo 'false'`
uci set dockerd.dockerman.debug_path=`uci get dockerd.dockerman.debug_path 2&> /dev/null || echo '/tmp/.docker_debug'`
uci set dockerd.dockerman.remote_port=`uci get dockerd.dockerman.remote_port 2&> /dev/null || echo '2375'`
uci set dockerd.dockerman.remote_endpoint=`uci get dockerd.dockerman.remote_endpoint 2&> /dev/null || echo '0'`
uci del_list dockerd.dockerman.ac_allowed_interface='br-lan'
uci add_list dockerd.dockerman.ac_allowed_interface='br-lan'
uci commit dockerd

View File

@ -0,0 +1,131 @@
#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
# PROCD_DEBUG=1
config_load 'dockerd'
# config_get daemon_ea "dockerman" daemon_ea
_DOCKERD=/etc/init.d/dockerd
docker_running(){
docker version > /dev/null 2>&1
return $?
}
add_ports() {
[ $# -eq 0 ] && return
$($_DOCKERD running) && docker_running || return 1
ids=$@
for id in $ids; do
id=$(docker ps --filter "ID=$id" --quiet)
[ -z "$id" ] && {
echo "Docker containner not running";
return 1;
}
ports=$(docker ps --filter "ID=$id" --format "{{.Ports}}")
# echo "$ports"
for port in $ports; do
echo "$port" | grep -qE "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:.*$" || continue;
[ "${port: -1}" == "," ] && port="${port:0:-1}"
local protocol=""
[ "${port%tcp}" != "$port" ] && protocol="/tcp"
[ "${port%udp}" != "$port" ] && protocol="/udp"
[ "$protocol" == "" ] && continue
port="${port%%->*}"
port="${port##*:}"
uci_add_list dockerd dockerman ac_allowed_ports "${port}${protocol}"
done
done
uci_commit dockerd
}
convert() {
_convert() {
_id=$1
_id=$(docker ps --all --filter "ID=$_id" --quiet)
if [ -z "$_id" ]; then
uci_remove_list dockerd dockerman ac_allowed_container "$1"
return
fi
if /etc/init.d/dockerman add_ports "$_id"; then
uci_remove_list dockerd dockerman ac_allowed_container "$_id"
fi
}
config_list_foreach dockerman ac_allowed_container _convert
uci_commit dockerd
}
iptables_append(){
# Wait for a maximum of 10 second per command, retrying every millisecond
local iptables_wait_args="--wait 10 --wait-interval 1000"
if ! iptables ${iptables_wait_args} --check $@ 2>/dev/null; then
iptables ${iptables_wait_args} -A $@ 2>/dev/null
fi
}
init_dockerman_chain(){
iptables -N DOCKER-MAN >/dev/null 2>&1
iptables -F DOCKER-MAN >/dev/null 2>&1
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
iptables -I DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
}
delete_dockerman_chain(){
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
iptables -F DOCKER-MAN >/dev/null 2>&1
iptables -X DOCKER-MAN >/dev/null 2>&1
}
add_allowed_interface(){
iptables_append DOCKER-MAN -i $1 -o docker0 -j RETURN
}
add_allowed_ports(){
port=$1
if [ "${port%/tcp}" != "$port" ]; then
iptables_append DOCKER-MAN -p tcp -m conntrack --ctorigdstport ${port%/tcp} --ctdir ORIGINAL -j RETURN
elif [ "${port%/udp}" != "$port" ]; then
iptables_append DOCKER-MAN -p udp -m conntrack --ctorigdstport ${port%/udp} --ctdir ORIGINAL -j RETURN
fi
}
handle_allowed_ports(){
config_list_foreach "dockerman" "ac_allowed_ports" add_allowed_ports
}
handle_allowed_interface(){
config_list_foreach "dockerman" "ac_allowed_interface" add_allowed_interface
iptables_append DOCKER-MAN -m conntrack --ctstate ESTABLISHED,RELATED -o docker0 -j RETURN >/dev/null 2>&1
iptables_append DOCKER-MAN -m conntrack --ctstate NEW,INVALID -o docker0 -j DROP >/dev/null 2>&1
iptables_append DOCKER-MAN -j RETURN >/dev/null 2>&1
}
start_service(){
[ -x "$_DOCKERD" ] && $($_DOCKERD enabled) || return 0
delete_dockerman_chain
$($_DOCKERD running) && docker_running || return 0
init_dockerman_chain
handle_allowed_ports
handle_allowed_interface
}
stop_service(){
delete_dockerman_chain
}
service_triggers() {
procd_add_reload_trigger 'dockerd'
}
reload_service() {
start
}
boot() {
sleep 5s
start
}
extra_command "add_ports" "Add allowed ports based on the container ID(s)"
extra_command "convert" "Convert Ac allowed container to AC allowed ports"

View File

@ -0,0 +1,36 @@
#!/bin/sh
. $IPKG_INSTROOT/lib/functions.sh
[ -x "$(command -v dockerd)" ] && chmod +x /etc/init.d/dockerman && /etc/init.d/dockerman enable >/dev/null 2>&1
sed -i 's/self:cfgvalue(section) or {}/self:cfgvalue(section) or self.default or {}/' /usr/lib/lua/luci/view/cbi/dynlist.htm
/etc/init.d/uhttpd restart >/dev/null 2>&1
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache >/dev/null 2>&1
touch /etc/config/dockerd
ls /etc/rc.d/*dockerd &> /dev/null && uci -q set dockerd.globals.auto_start="1" || uci -q set dockerd.globals.auto_start="0"
uci -q batch <<-EOF >/dev/null
set uhttpd.main.script_timeout="3600"
commit uhttpd
set dockerd.dockerman=dockerman
set dockerd.dockerman.socket_path='/var/run/docker.sock'
set dockerd.dockerman.status_path='/tmp/.docker_action_status'
set dockerd.dockerman.debug='false'
set dockerd.dockerman.debug_path='/tmp/.docker_debug'
set dockerd.dockerman.remote_endpoint='0'
del_list dockerd.dockerman.ac_allowed_interface='br-lan'
add_list dockerd.dockerman.ac_allowed_interface='br-lan'
commit dockerd
EOF
# remove dockerd firewall
config_load dockerd
remove_firewall(){
cfg=${1}
uci_remove dockerd ${1}
}
config_foreach remove_firewall firewall
# Convert ac_allowed_container to ac_allowed_ports
(sleep 30s && /etc/init.d/dockerman convert;/etc/init.d/dockerman restart) &
exit 0

View File

@ -0,0 +1,11 @@
{
"luci-app-dockerman": {
"description": "Grant UCI access for luci-app-dockerman",
"read": {
"uci": [ "dockerd" ]
},
"write": {
"uci": [ "dockerd" ]
}
}
}

106
luci-app-openclash/Makefile Normal file
View File

@ -0,0 +1,106 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclash
PKG_VERSION:=0.44.16
PKG_RELEASE:=
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI support for clash
PKGARCH:=all
DEPENDS:=+iptables +dnsmasq-full +coreutils +coreutils-nohup +bash +curl +ca-certificates +ipset +ip-full +iptables-mod-tproxy +iptables-mod-extra +libcap +libcap-bin +ruby +ruby-yaml +kmod-tun
MAINTAINER:=vernesong
endef
define Package/$(PKG_NAME)/description
A LuCI support for clash
endef
define Build/Prepare
$(CP) $(CURDIR)/root $(PKG_BUILD_DIR)
$(CP) $(CURDIR)/luasrc $(PKG_BUILD_DIR)
$(foreach po,$(wildcard ${CURDIR}/po/zh-cn/*.po), \
po2lmo $(po) $(PKG_BUILD_DIR)/$(patsubst %.po,%.lmo,$(notdir $(po)));)
chmod 0755 $(PKG_BUILD_DIR)/root/etc/init.d/openclash
chmod -R 0755 $(PKG_BUILD_DIR)/root/usr/share/openclash/
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/config
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/rule_provider
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/backup
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/core
mkdir -p $(PKG_BUILD_DIR)/root/usr/share/openclash/backup
cp -f "$(PKG_BUILD_DIR)/root/etc/config/openclash" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_rules.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_rules.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_rules_2.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_rules_2.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_hosts.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_hosts.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_fake_filter.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_fake_filter.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_domain_dns.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_domain_dns.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_domain_dns_policy.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_domain_dns_policy.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_fallback_filter.yaml" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_fallback_filter.yaml" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_netflix_domains.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_netflix_domains.list" >/dev/null 2>&1
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/conffiles
endef
define Package/$(PKG_NAME)/preinst
#!/bin/sh
if [ -f "/etc/config/openclash" ]; then
cp -f "/etc/config/openclash" "/tmp/openclash.bak" >/dev/null 2>&1
cp -rf "/etc/openclash" "/tmp/openclash" >/dev/null 2>&1
fi
endef
define Package/$(PKG_NAME)/postinst
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
uci -q set openclash.config.enable=0
uci -q commit openclash
cp -f "/etc/config/openclash" "/tmp/openclash.bak" >/dev/null 2>&1
cp -rf "/etc/openclash" "/tmp/openclash" >/dev/null 2>&1
endef
define Package/$(PKG_NAME)/postrm
#!/bin/sh
rm -rf /etc/openclash
rm -rf /tmp/openclash.log
rm -rf /tmp/openclash_start.log
rm -rf /tmp/openclash_last_version
rm -rf /tmp/openclash_config.tmp
rm -rf /tmp/openclash.change
rm -rf /tmp/Proxy_Group
rm -rf /tmp/rules_name
rm -rf /tmp/rule_providers_name
rm -rf /tmp/clash_last_version
rm -rf /usr/share/openclash/backup
rm -rf /tmp/openclash_fake_filter.list
rm -rf /tmp/openclash_servers_fake_filter.conf
rm -rf /tmp/dler*
uci -q delete firewall.openclash
uci -q commit firewall
uci -q delete ucitrack.@openclash[-1]
uci -q commit ucitrack
rm -rf /tmp/luci-*
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
$(INSTALL_DATA) $(PKG_BUILD_DIR)/*.*.lmo $(1)/usr/lib/lua/luci/i18n/
$(CP) $(PKG_BUILD_DIR)/root/* $(1)/
$(CP) $(PKG_BUILD_DIR)/luasrc/* $(1)/usr/lib/lua/luci/
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require("luci.model.uci").cursor()
m = SimpleForm("openclash",translate("OpenClash"))
m.description = translate("A Clash Client For OpenWrt")
m.reset = false
m.submit = false
m:section(SimpleSection).template = "openclash/status"
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
function config_check(CONFIG_FILE)
local yaml = fs.isfile(CONFIG_FILE)
if yaml then
yaml = SYS.exec(string.format('ruby -ryaml -E UTF-8 -e "puts YAML.load_file(\'%s\')" 2>/dev/null',CONFIG_FILE))
if yaml ~= "false\n" and yaml ~= "" then
return "Config Normal"
else
return "Config Abnormal"
end
elseif (yaml ~= 0) then
return "File Not Exist"
end
end
local e,a={}
for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(o)
if a then
e[t]={}
e[t].num=string.format(t)
e[t].name=fs.basename(o)
BACKUP_FILE="/etc/openclash/backup/".. e[t].name
if fs.mtime(BACKUP_FILE) then
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(BACKUP_FILE))
else
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",a.mtime)
end
if uci:get("openclash", "config", "config_path") and string.sub(uci:get("openclash", "config", "config_path"), 23, -1) == e[t].name then
e[t].state=translate("Enable")
else
e[t].state=translate("Disable")
end
e[t].check=translate(config_check(o))
end
end
form = SimpleForm("openclash")
form.reset = false
form.submit = false
tb=form:section(Table,e)
st=tb:option(DummyValue,"state",translate("State"))
st.template="openclash/cfg_check"
nm=tb:option(DummyValue,"name",translate("Config Alias"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
ck=tb:option(DummyValue,"check",translate("Grammar Check"))
ck.template="openclash/cfg_check"
nm.template="openclash/sub_info_show"
btnis=tb:option(Button,"switch",translate("Switch Config"))
btnis.template="openclash/other_button"
btnis.render=function(o,t,a)
if not e[t] then return false end
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
a.display=""
else
a.display="none"
end
o.inputstyle="apply"
Button.render(o,t,a)
end
btnis.write=function(a,t)
fs.unlink("/tmp/Proxy_Group")
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name)
uci:set("openclash", "config", "enable", 1)
uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "client"))
end
s = SimpleForm("openclash")
s.reset = false
s.submit = false
s:section(SimpleSection).template = "openclash/myip"
local t = {
{enable, disable}
}
ap = SimpleForm("openclash")
ap.reset = false
ap.submit = false
ss = ap:section(Table, t)
o = ss:option(Button, "enable", " ")
o.inputtitle = translate("Enable OpenClash")
o.inputstyle = "apply"
o.write = function()
uci:set("openclash", "config", "enable", 1)
uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
end
o = ss:option(Button, "disable", " ")
o.inputtitle = translate("Disable OpenClash")
o.inputstyle = "reset"
o.write = function()
uci:set("openclash", "config", "enable", 0)
uci:commit("openclash")
SYS.call("/etc/init.d/openclash stop >/dev/null 2>&1 &")
end
d = SimpleForm("openclash")
d.title = translate("Credits")
d.reset = false
d.submit = false
d:section(SimpleSection).template = "openclash/developer"
dler = SimpleForm("openclash")
dler.reset = false
dler.submit = false
dler:section(SimpleSection).template = "openclash/dlercloud"
if uci:get("openclash", "config", "dler_token") then
return m, dler, form, s, ap, d
else
return m, form, s, ap, d
end

View File

@ -0,0 +1,193 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local json = require "luci.jsonc"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Config Subscribe Edit"))
m.pageaction = false
m.description=translate("Convert Subscribe function of Online is Supported By subconverter Written By tindy X") ..
"<br/>"..
"<br/>"..translate("API By tindy X & lhie1")..
"<br/>"..
"<br/>"..translate("Subconverter external configuration (subscription conversion template) Description: https://github.com/tindy2013/subconverter#external-configuration-file")..
"<br/>"..
"<br/>"..translate("If you need to customize the external configuration file (subscription conversion template), please write it according to the instructions, upload it to the accessible location of the external network, and fill in the address correctly when using it")..
"<br/>"..
"<br/>"..translate("If you have a recommended external configuration file (subscription conversion template), you can modify by following The file format of /usr/share/opencrash/res/sub_ini.list and pr")
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-subscribe")
if m.uci:get(openclash, sid) ~= "config_subscribe" then
luci.http.redirect(m.redirect)
return
end
-- [[ Config Subscribe Setting ]]--
s = m:section(NamedSection, sid, "config_subscribe")
s.anonymous = true
s.addremove = false
---- name
o = s:option(Value, "name", translate("Config Alias"))
o.description = font_red..bold_on..translate("Name For Distinguishing")..bold_off..font_off
o.placeholder = translate("config")
o.rmempty = true
---- address
o = s:option(Value, "address", translate("Subscribe Address"))
o.description = font_red..bold_on..translate("Not Null")..bold_off..font_off
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o.rmempty = false
local sub_path = "/tmp/dler_sub"
local info, token, get_sub, sub_info
local token = uci:get("openclash", "config", "dler_token")
if token then
get_sub = string.format("curl -sL -H 'Content-Type: application/json' --connect-timeout 2 -d '{\"access_token\":\"%s\"}' -X POST https://dler.cloud/api/v1/managed/clash -o %s", token, sub_path)
if not nixio.fs.access(sub_path) then
luci.sys.exec(get_sub)
else
if fs.readfile(sub_path) == "" or not fs.readfile(sub_path) then
luci.sys.exec(get_sub)
end
end
sub_info = fs.readfile(sub_path)
if sub_info then
sub_info = json.parse(sub_info)
end
if sub_info and sub_info.ret == 200 then
o:value(sub_info.smart)
o:value(sub_info.ss)
o:value(sub_info.vmess)
o:value(sub_info.trojan)
else
fs.unlink(sub_path)
end
end
---- subconverter
o = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
o.description = translate("Convert Subscribe Online With Template, Mix Proxies and Keep Settings options Will Not Effect")
o.default=0
---- Convert Address
o = s:option(Value, "convert_address", translate("Convert Address"))
o.rmempty = true
o.description = font_red..bold_on..translate("Note: There is A Risk of Privacy Leakage in Online Convert")..bold_off..font_off
o:depends("sub_convert", "1")
o:value("https://api.dler.io/sub", translate("api.dler.io")..translate("(Default)"))
o:value("https://subconverter.herokuapp.com/sub", translate("subconverter.herokuapp.com")..translate("(Default)"))
o:value("https://sub.id9.cc/sub", translate("sub.id9.cc"))
o:value("https://api.wcc.best/sub", translate("api.wcc.best"))
o.default = "https://api.dler.io/sub"
---- Template
o = s:option(ListValue, "template", translate("Template Name"))
o.rmempty = true
o:depends("sub_convert", "1")
file = io.open("/usr/share/openclash/res/sub_ini.list", "r");
for l in file:lines() do
if l ~= "" and l ~= nil then
o:value(string.sub(luci.sys.exec(string.format("echo '%s' |awk -F ',' '{print $1}' 2>/dev/null",l)),1,-2))
end
end
file:close()
o:value("0", translate("Custom Template"))
---- Custom Template
o = s:option(Value, "custom_template_url", translate("Custom Template URL"))
o.rmempty = true
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o:depends("template", "0")
---- emoji
o = s:option(ListValue, "emoji", translate("Emoji"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default=0
o:depends("sub_convert", "1")
---- udp
o = s:option(ListValue, "udp", translate("UDP Enable"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default=0
o:depends("sub_convert", "1")
---- skip-cert-verify
o = s:option(ListValue, "skip_cert_verify", translate("skip-cert-verify"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default=0
o:depends("sub_convert", "1")
---- sort
o = s:option(ListValue, "sort", translate("Sort"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default=0
o:depends("sub_convert", "1")
---- node type
o = s:option(ListValue, "node_type", translate("Append Node Type"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default=0
o:depends("sub_convert", "1")
---- key
o = s:option(DynamicList, "keyword", font_red..bold_on..translate("Keyword Match")..bold_off..font_off)
o.description = font_red..bold_on..translate("eg: hk or tw&bgp")..bold_off..font_off
o.rmempty = true
---- exkey
o = s:option(DynamicList, "ex_keyword", font_red..bold_on..translate("Exclude Keyword Match")..bold_off..font_off)
o.description = font_red..bold_on..translate("eg: hk or tw&bgp")..bold_off..font_off
o.rmempty = true
---- de_exkey
o = s:option(MultiValue, "de_ex_keyword", font_red..bold_on..translate("Exclude Keyword Match Default")..bold_off..font_off)
o.rmempty = true
o:depends("sub_convert", 0)
o:value("过期时间")
o:value("剩余流量")
o:value("TG群")
o:value("官网")
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
return m

View File

@ -0,0 +1,143 @@
local m, s, o
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map("openclash", translate("Config Update"))
m.pageaction = false
s = m:section(TypedSection, "openclash")
s.anonymous = true
---- update Settings
o = s:option(Flag, "auto_update", translate("Auto Update"))
o.description = translate("Auto Update Server subscription")
o.default=0
o = s:option(ListValue, "config_auto_update_mode", translate("Update Mode"))
o:depends("auto_update", "1")
o:value("0", translate("Appointment Mode"))
o:value("1", translate("Loop Mode"))
o.default=0
o.rmempty = true
o = s:option(ListValue, "config_update_week_time", translate("Update Time (Every Week)"))
o:depends("config_auto_update_mode", "0")
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default=1
o.rmempty = true
o = s:option(ListValue, "auto_update_time", translate("Update time (every day)"))
o:depends("config_auto_update_mode", "0")
for t = 0,23 do
o:value(t, t..":00")
end
o.default=0
o.rmempty = true
o = s:option(Value, "config_update_interval", translate("Update Interval(min)"))
o.default="60"
o.datatype = "integer"
o:depends("config_auto_update_mode", "1")
o.rmempty = true
-- [[ Edit Server ]] --
s = m:section(TypedSection, "config_subscribe", translate("Config Subscribe Edit"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/config-subscribe-edit/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- name
o = s:option(DummyValue, "name", translate("Config Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("config")
end
---- address
o = s:option(Value, "address", translate("Subscribe Address"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
---- template
o = s:option(DummyValue, "template", translate("Template Name"))
function o.cfgvalue(...)
if Value.cfgvalue(...) ~= "0" then
return Value.cfgvalue(...) or translate("None")
else
return translate("Custom Template")
end
end
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Update Config")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
uci:foreach("openclash", "config_subscribe",
function(s)
if s.name ~= "" and s.name ~= nil and s.enabled == "1" then
local back_cfg_path_yaml="/etc/openclash/backup/" .. s.name .. ".yaml"
local back_cfg_path_yml="/etc/openclash/backup/" .. s.name .. ".yml"
fs.unlink(back_cfg_path_yaml)
fs.unlink(back_cfg_path_yml)
end
end)
SYS.call("/usr/share/openclash/openclash.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,427 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require("luci.model.uci").cursor()
local CHIF = "0"
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
align_mid = [[<p align="center">]]
align_mid_off = [[</p>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
function default_config_set(f)
local cf = uci:get("openclash", "config", "config_path")
if cf == "/etc/openclash/config/"..f or not cf or cf == "" or not fs.isfile(cf) then
if CHIF == "1" and cf == "/etc/openclash/config/"..f then
return
end
local fis = fs.glob("/etc/openclash/config/*")[1]
if fis ~= nil then
fcf = fs.basename(fis)
if fcf then
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..fcf)
uci:commit("openclash")
end
else
uci:set("openclash", "config", "config_path", "/etc/openclash/config/config.yaml")
uci:commit("openclash")
end
end
end
function config_check(CONFIG_FILE)
local yaml = fs.isfile(CONFIG_FILE)
if yaml then
yaml = SYS.exec(string.format('ruby -ryaml -E UTF-8 -e "puts YAML.load_file(\'%s\')" 2>/dev/null',CONFIG_FILE))
if yaml ~= "false\n" and yaml ~= "" then
return "Config Normal"
else
return "Config Abnormal"
end
elseif (yaml ~= 0) then
return "File Not Exist"
end
end
ful = SimpleForm("upload", translate("Config Manage"), nil)
ful.reset = false
ful.submit = false
sul =ful:section(SimpleSection, "")
o = sul:option(FileUpload, "")
o.template = "openclash/upload"
um = sul:option(DummyValue, "", nil)
um.template = "openclash/dvalue"
local dir, fd, clash
clash = "/etc/openclash/clash"
dir = "/etc/openclash/config/"
bakck_dir="/etc/openclash/backup"
proxy_pro_dir="/etc/openclash/proxy_provider/"
rule_pro_dir="/etc/openclash/rule_provider/"
backup_dir="/tmp/"
create_bakck_dir=fs.mkdir(bakck_dir)
create_proxy_pro_dir=fs.mkdir(proxy_pro_dir)
create_rule_pro_dir=fs.mkdir(rule_pro_dir)
HTTP.setfilehandler(
function(meta, chunk, eof)
local fp = HTTP.formvalue("file_type")
if not fd then
if not meta then return end
if fp == "config" then
if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end
elseif fp == "proxy-provider" then
if meta and chunk then fd = nixio.open(proxy_pro_dir .. meta.file, "w") end
elseif fp == "rule-provider" then
if meta and chunk then fd = nixio.open(rule_pro_dir .. meta.file, "w") end
elseif fp == "backup-file" then
if meta and chunk then fd = nixio.open(backup_dir .. meta.file, "w") end
end
if not fd then
um.value = translate("upload file error.")
return
end
end
if chunk and fd then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
if fp == "config" then
CHIF = "1"
if IsYamlFile(meta.file) then
local yamlbackup="/etc/openclash/backup/" .. meta.file
local c=fs.copy(dir .. meta.file,yamlbackup)
default_config_set(meta.file)
end
if IsYmlFile(meta.file) then
local ymlname=string.lower(string.sub(meta.file,0,-5))
local ymlbackup="/etc/openclash/backup/".. ymlname .. ".yaml"
local c=fs.rename(dir .. meta.file,"/etc/openclash/config/".. ymlname .. ".yaml")
local c=fs.copy("/etc/openclash/config/".. ymlname .. ".yaml",ymlbackup)
local yamlname=ymlname .. ".yaml"
default_config_set(yamlname)
end
um.value = translate("File saved to") .. ' "/etc/openclash/config/"'
elseif fp == "proxy-provider" then
um.value = translate("File saved to") .. ' "/etc/openclash/proxy_provider/"'
elseif fp == "rule-provider" then
um.value = translate("File saved to") .. ' "/etc/openclash/rule_provider/"'
elseif fp == "backup-file" then
os.execute("tar -C '/etc/openclash/' -xzf %s >/dev/null 2>&1" % (backup_dir .. meta.file))
os.execute("mv /etc/openclash/openclash /etc/config/openclash >/dev/null 2>&1")
fs.unlink(backup_dir .. meta.file)
um.value = translate("Backup File Restore Successful!")
end
fs.unlink("/tmp/Proxy_Group")
end
end
)
if HTTP.formvalue("upload") then
local f = HTTP.formvalue("ulfile")
if #f <= 0 then
um.value = translate("No specify upload file.")
end
end
local e,a={}
for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(o)
if a then
e[t]={}
e[t].name=fs.basename(o)
BACKUP_FILE="/etc/openclash/backup/".. e[t].name
if fs.mtime(BACKUP_FILE) then
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(BACKUP_FILE))
else
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",a.mtime)
end
if uci:get("openclash", "config", "config_path") and string.sub(uci:get("openclash", "config", "config_path"), 23, -1) == e[t].name then
e[t].state=translate("Enable")
else
e[t].state=translate("Disable")
end
e[t].size=fs.filesize(a.size)
e[t].check=translate(config_check(o))
e[t].remove=0
end
end
form=SimpleForm("config_file_list",translate("Config File List"))
form.reset=false
form.submit=false
tb=form:section(Table,e)
st=tb:option(DummyValue,"state",translate("State"))
st.template="openclash/cfg_check"
nm=tb:option(DummyValue,"name",translate("Config Alias"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
sz=tb:option(DummyValue,"size",translate("Size"))
ck=tb:option(DummyValue,"check",translate("Grammar Check"))
ck.template="openclash/cfg_check"
nm.template="openclash/sub_info_show"
btnis=tb:option(Button,"switch",translate("Switch Config"))
btnis.template="openclash/other_button"
btnis.render=function(o,t,a)
if not e[t] then return false end
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
a.display=""
else
a.display="none"
end
o.inputstyle="apply"
Button.render(o,t,a)
end
btnis.write=function(a,t)
fs.unlink("/tmp/Proxy_Group")
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name)
uci:commit("openclash")
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
end
btncp=tb:option(Button,"copy",translate("Copy Config"))
btncp.template="openclash/other_button"
btncp.render=function(o,t,a)
if not e[t] then return false end
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
a.display=""
else
a.display="none"
end
o.inputstyle="apply"
Button.render(o,t,a)
end
btncp.write=function(a,t)
local num = 1
while true do
num = num + 1
if not fs.isfile("/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml") then
fs.copy("/etc/openclash/config/"..e[t].name, "/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml")
break
end
end
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
end
btndl = tb:option(Button,"download",translate("Download Config"))
btndl.template="openclash/other_button"
btndl.render=function(e,t,a)
e.inputstyle="remove"
Button.render(e,t,a)
end
btndl.write = function (a,t)
local sPath, sFile, fd, block
sPath = "/etc/openclash/config/"..e[t].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btndlr = tb:option(Button,"download_run",translate("Download Running Config"))
btndlr.template="openclash/other_button"
btndlr.render=function(c,t,a)
if nixio.fs.access("/etc/openclash/"..e[t].name) then
a.display=""
else
a.display="none"
end
c.inputstyle="remove"
Button.render(c,t,a)
end
btndlr.write = function (a,t)
local sPath, sFile, fd, block
sPath = "/etc/openclash/"..e[t].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm=tb:option(Button,"remove",translate("Remove"))
btnrm.render=function(e,t,a)
e.inputstyle="reset"
Button.render(e,t,a)
end
btnrm.write=function(a,t)
fs.unlink("/tmp/Proxy_Group")
fs.unlink("/etc/openclash/backup/"..fs.basename(e[t].name))
fs.unlink("/etc/openclash/history/"..fs.filename(e[t].name))
fs.unlink("/etc/openclash/history/"..fs.filename(e[t].name)..".db")
fs.unlink("/etc/openclash/"..fs.basename(e[t].name))
local a=fs.unlink("/etc/openclash/config/"..fs.basename(e[t].name))
default_config_set(fs.basename(e[t].name))
if a then table.remove(e,t)end
HTTP.redirect(DISP.build_url("admin", "services", "openclash","config"))
end
p = SimpleForm("provider_file_manage",translate("Provider File Manage"))
p.reset = false
p.submit = false
local provider_manage = {
{proxy_mg, rule_mg, game_mg}
}
promg = p:section(Table, provider_manage)
o = promg:option(Button, "proxy_mg", " ")
o.inputtitle = translate("Proxy Provider File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = promg:option(Button, "rule_mg", " ")
o.inputtitle = translate("Rule Providers File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = promg:option(Button, "game_mg", " ")
o.inputtitle = translate("Game Rules File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage"))
end
m = SimpleForm("openclash",translate("Config File Edit"))
m.reset = false
m.submit = false
local tab = {
{user, default}
}
s = m:section(Table, tab)
s.description = align_mid..translate("Support syntax check, press").." "..font_green..bold_on.."F11"..bold_off..font_off.." "..translate("to enter full screen editing mode")..align_mid_off
s.anonymous = true
s.addremove = false
local conf = uci:get("openclash", "config", "config_path")
local dconf = "/usr/share/openclash/res/default.yaml"
if not conf then conf = "/etc/openclash/config/config.yaml" end
local conf_name = fs.basename(conf)
if not conf_name then conf_name = "config.yaml" end
local sconf = "/etc/openclash/"..conf_name
sev = s:option(TextValue, "user")
sev.description = align_mid..translate("Modify Your Config file:").." "..font_green..bold_on..conf_name..bold_off..font_off.." "..translate("Here, Except The Settings That Were Taken Over")..align_mid_off
sev.rows = 40
sev.wrap = "off"
sev.cfgvalue = function(self, section)
return NXFS.readfile(conf) or NXFS.readfile(dconf) or ""
end
sev.write = function(self, section, value)
if (CHIF == "0") then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile(conf)
if value ~= old_value then
NXFS.writefile(conf, value)
end
end
end
def = s:option(TextValue, "default")
if fs.isfile(sconf) then
def.description = align_mid..translate("Config File Edited By OpenClash For Running")..align_mid_off
else
def.description = align_mid..translate("Default Config File With Correct Template")..align_mid_off
end
def.rows = 40
def.wrap = "off"
def.readonly = true
def.cfgvalue = function(self, section)
return NXFS.readfile(sconf) or NXFS.readfile(dconf) or ""
end
def.write = function(self, section, value)
end
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
uci:set("openclash", "config", "enable", 1)
uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/config_editor"))
return ful , form , p , m

View File

@ -0,0 +1,105 @@
local rule_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local g,h={}
for n,m in ipairs(fs.glob("/etc/openclash/game_rules/*"))do
h=fs.stat(m)
if h then
g[n]={}
g[n].name=fs.basename(m)
g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime)
g[n].size=fs.filesize(h.size)
g[n].remove=0
g[n].enable=false
end
end
rule_form=SimpleForm("game_rules_file_list",translate("Game Rules File List"))
rule_form.reset=false
rule_form.submit=false
tb2=rule_form:section(Table,g)
nm2=tb2:option(DummyValue,"name",translate("File Name"))
mt2=tb2:option(DummyValue,"mtime",translate("Update Time"))
sz2=tb2:option(DummyValue,"size",translate("Size"))
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
btndl2.template="openclash/other_button"
btndl2.render=function(m,n,h)
m.inputstyle="remove"
Button.render(m,n,h)
end
btndl2.write = function (h,n)
local sPath, sFile, fd, block
sPath = "/etc/openclash/game_rules/"..g[n].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm2=tb2:option(Button,"remove2",translate("Remove"))
btnrm2.render=function(g,n,h)
g.inputstyle="reset"
Button.render(g,n,h)
end
btnrm2.write=function(h,n)
local h=fs.unlink("/etc/openclash/game_rules/"..luci.openclash.basename(g[n].name))
if h then table.remove(g,n)end
return h
end
local t = {
{Refresh, Delete_all, Apply}
}
a = rule_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage"))
end
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/game_rules/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
return rule_form

View File

@ -0,0 +1,99 @@
local form, m
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
m = SimpleForm("openclash", translate("Game Rules List"))
m.description=translate("Rule Project:").." SSTap-Rule ( https://github.com/FQrabbit/SSTap-Rule )"
m.reset = false
m.submit = false
local t = {
{Refresh, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-settings"))
end
if not NXFS.access("/tmp/rules_name") then
SYS.call("awk -F ',' '{print $1}' /usr/share/openclash/res/game_rules.list > /tmp/rules_name 2>/dev/null")
end
file = io.open("/tmp/rules_name", "r");
---- Rules List
local e={},o,t
if NXFS.access("/tmp/rules_name") then
for o in file:lines() do
table.insert(e,o)
end
for t,o in ipairs(e) do
e[t]={}
e[t].num=string.format(t)
e[t].name=o
e[t].filename=string.sub(luci.sys.exec(string.format("grep -F '%s,' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $3}' 2>/dev/null",e[t].name)),1,-2)
if e[t].filename == "" then
e[t].filename=string.sub(luci.sys.exec(string.format("grep -F '%s,' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $2}' 2>/dev/null",e[t].name)),1,-2)
end
RULE_FILE="/etc/openclash/game_rules/".. e[t].filename
if fs.mtime(RULE_FILE) then
e[t].size=fs.filesize(fs.stat(RULE_FILE).size)
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(RULE_FILE))
else
e[t].size="/"
e[t].mtime="/"
end
if fs.isfile(RULE_FILE) then
e[t].exist=translate("Exist")
else
e[t].exist=translate("Not Exist")
end
e[t].remove=0
end
end
file:close()
form=SimpleForm("filelist")
form.reset=false
form.submit=false
tb=form:section(Table,e)
nu=tb:option(DummyValue,"num",translate("Order Number"))
st=tb:option(DummyValue,"exist",translate("State"))
st.template="openclash/cfg_check"
nm=tb:option(DummyValue,"name",translate("Rule Name"))
fm=tb:option(DummyValue,"filename",translate("File Name"))
sz=tb:option(DummyValue,"size",translate("Size"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
btnis=tb:option(DummyValue,"filename",translate("Download Rule"))
btnis.template="openclash/download_rule"
btnrm=tb:option(Button,"remove",translate("Remove"))
btnrm.render=function(e,t,a)
e.inputstyle="reset"
Button.render(e,t,a)
end
btnrm.write=function(a,t)
fs.unlink("/etc/openclash/game_rules/"..e[t].filename)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage"))
end
return m, form

View File

@ -0,0 +1,142 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Group"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers")
if m.uci:get(openclash, sid) ~= "groups" then
luci.http.redirect(m.redirect)
return
end
-- [[ Groups Setting ]]--
s = m:section(NamedSection, sid, "groups")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(ListValue, "type", translate("Group Type"))
o.rmempty = true
o.description = translate("Choose The Operation Mode")
o:value("select", translate("Manual-Select"))
o:value("url-test", translate("URL-Test"))
o:value("fallback", translate("Fallback"))
o:value("load-balance", translate("Load-Balance"))
o:value("relay", translate("Relay-Traffic"))
o = s:option(ListValue, "strategy", translate("Strategy Type"))
o.rmempty = true
o.description = translate("Choose The Load-Balance's Strategy Type")
o:value("consistent-hashing", translate("Consistent-hashing"))
o:value("round-robin", translate("Round-robin"))
o:depends("type", "load-balance")
o = s:option(Value, "name", translate("Group Name"))
o.rmempty = false
o.default = "Group - "..sid
o = s:option(ListValue, "disable_udp", translate("Disable UDP"))
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = false
o = s:option(Value, "test_url", translate("Test URL"))
o:value("http://www.gstatic.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o.rmempty = false
o:depends("type", "url-test")
o:depends("type", "fallback")
o:depends("type", "load-balance")
o = s:option(Value, "test_interval", translate("Test Interval(s)"))
o.default = "300"
o.rmempty = false
o:depends("type", "url-test")
o:depends("type", "fallback")
o:depends("type", "load-balance")
o = s:option(Value, "tolerance", translate("Tolerance(ms)"))
o.default = "150"
o.rmempty = true
o:depends("type", "url-test")
-- [[ interface-name ]]--
o = s:option(Value, "interface_name", translate("interface-name"))
o.rmempty = true
o.placeholder = translate("eth0")
-- [[ routing-mark ]]--
o = s:option(Value, "routing_mark", translate("routing-mark"))
o.rmempty = true
o.placeholder = translate("2333")
o = s:option(DynamicList, "other_group", translate("Other Group"))
o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT'")..bold_off..font_off
uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil and s.name ~= m.uci:get(openclash, sid, "name") then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("REJECT")
o.rmempty = true
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
sys.call("/usr/share/openclash/yml_groups_name_ch.sh")
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
return m

View File

@ -0,0 +1,22 @@
--
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
m = Map("openclash", translate("Server Logs"))
s = m:section(TypedSection, "openclash")
m.pageaction = false
s.anonymous = true
s.addremove=false
log = s:option(TextValue, "clog")
log.readonly=true
log.pollcheck=true
log.template="openclash/log"
log.description = translate("")
log.rows = 29
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,362 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Other Rules Edit"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/settings")
if m.uci:get(openclash, sid) ~= "other_rules" then
luci.http.redirect(m.redirect)
return
end
-- [[ Other Rules Setting ]]--
s = m:section(NamedSection, sid, "other_rules")
s.anonymous = true
s.addremove = false
o = s:option(Value, "Note", translate("Note"))
o.default = "default"
o.rmempty = false
o = s:option(ListValue, "config", translate("Config File"))
local e,a={}
local groupnames,filename
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
if e[t].name == m.uci:get(openclash, sid, "config") then
filename = e[t].name
groupnames = sys.exec(string.format('ruby -ryaml -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',f))
end
end
end
o = s:option(Button, translate("Get Group Names"))
o.title = translate("Get Group Names")
o.inputtitle = translate("Get Group Names")
o.description = translate("Get Group Names After Select Config File")
o.inputstyle = "reload"
o.write = function()
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin/services/openclash/other-rules-edit/%s") % sid)
end
if groupnames ~= nil and filename ~= nil then
o = s:option(ListValue, "rule_name", translate("Other Rules Name"))
o.rmempty = true
o:value("lhie1", translate("lhie1 Rules"))
o:value("ConnersHua", translate("ConnersHua(Provider-type) Rules"))
o:value("ConnersHua_return", translate("ConnersHua Return Rules"))
o = s:option(ListValue, "GlobalTV", translate("GlobalTV"))
o:depends("rule_name", "lhie1")
o:depends("rule_name", "ConnersHua")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "AsianTV", translate("AsianTV"))
o:depends("rule_name", "lhie1")
o:depends("rule_name", "ConnersHua")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Proxy", translate("Proxy"))
o:depends("rule_name", "lhie1")
o:depends("rule_name", "ConnersHua")
o:depends("rule_name", "ConnersHua_return")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Youtube", translate("Youtube"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Bilibili", translate("Bilibili"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Bahamut", translate("Bahamut"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "HBOMax", translate("HBO Max"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "HBOGo", translate("HBO Go"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Pornhub", translate("Pornhub"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Apple", translate("Apple"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "GoogleFCM", translate("Google FCM"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Scholar", translate("Scholar"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Microsoft", translate("Microsoft"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Netflix", translate("Netflix"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Disney", translate("Disney Plus"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Spotify", translate("Spotify"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Steam", translate("Steam"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Speedtest", translate("Speedtest"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Telegram", translate("Telegram"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "PayPal", translate("PayPal"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "AdBlock", translate("AdBlock"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Domestic", translate("Domestic"))
o:depends("rule_name", "lhie1")
o:depends("rule_name", "ConnersHua")
o.rmempty = true
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
o = s:option(ListValue, "Others", translate("Others"))
o:depends("rule_name", "lhie1")
o:depends("rule_name", "ConnersHua")
o:depends("rule_name", "ConnersHua_return")
o.rmempty = true
o.description = translate("Choose Proxy Groups, Base On Your Config File").." ( "..font_green..bold_on..filename..bold_off..font_off.." )"
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
o:value("DIRECT")
o:value("REJECT")
end
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
--luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
return m

View File

@ -0,0 +1,140 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local sys = require "luci.sys"
local sid = arg[1]
local fs = require "luci.openclash"
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Proxy-Provider"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers")
if m.uci:get(openclash, sid) ~= "proxy-provider" then
luci.http.redirect(m.redirect)
return
end
-- [[ Provider Setting ]]--
s = m:section(NamedSection, sid, "proxy-provider")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(ListValue, "type", translate("Provider Type"))
o.rmempty = true
o.description = translate("Choose The Provider Type")
o:value("http")
o:value("file")
o = s:option(Value, "name", translate("Provider Name"))
o.rmempty = false
o.default = "Proxy-provider - "..sid
if not m.uci:get("openclash", sid, "name") then
m.uci:set("openclash", sid, "manual", 1)
end
o = s:option(ListValue, "path", translate("Provider Path"))
o.description = translate("Update Your Proxy Provider File From Config Luci Page")
local p,h={}
for t,f in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do
h=fs.stat(f)
if h then
p[t]={}
p[t].name=fs.basename(f)
if IsYamlFile(p[t].name) or IsYmlFile(p[t].name) then
o:value("./proxy_provider/"..p[t].name)
end
end
end
o.rmempty = false
o:depends("type", "file")
o = s:option(Value, "provider_url", translate("Provider URL"))
o.rmempty = false
o:depends("type", "http")
o = s:option(Value, "provider_filter", translate("Provider Filter"))
o.rmempty = true
o.placeholder = "bgp|sg"
o = s:option(Value, "provider_interval", translate("Provider Interval(s)"))
o.default = "3600"
o.rmempty = false
o:depends("type", "http")
o = s:option(ListValue, "health_check", translate("Provider Health Check"))
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default=true
o = s:option(Value, "health_check_url", translate("Health Check URL"))
o:value("http://www.gstatic.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o.rmempty = false
o = s:option(Value, "health_check_interval", translate("Health Check Interval(s)"))
o.default = "300"
o.rmempty = false
o = s:option(DynamicList, "groups", translate("Proxy Group"))
o.description = font_red..bold_on..translate("No Need Set when Config Create, The added Proxy Groups Must Exist")..bold_off..font_off
o.rmempty = true
o:value("all", translate("All Groups"))
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
return m

View File

@ -0,0 +1,105 @@
local proxy_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local p,r={}
for x,y in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do
r=fs.stat(y)
if r then
p[x]={}
p[x].name=fs.basename(y)
p[x].mtime=os.date("%Y-%m-%d %H:%M:%S",r.mtime)
p[x].size=fs.filesize(r.size)
p[x].remove=0
p[x].enable=false
end
end
proxy_form=SimpleForm("proxy_provider_file_list",translate("Proxy Provider File List"))
proxy_form.reset=false
proxy_form.submit=false
tb1=proxy_form:section(Table,p)
nm1=tb1:option(DummyValue,"name",translate("File Name"))
mt1=tb1:option(DummyValue,"mtime",translate("Update Time"))
sz1=tb1:option(DummyValue,"size",translate("Size"))
btndl1 = tb1:option(Button,"download1",translate("Download Config"))
btndl1.template="openclash/other_button"
btndl1.render=function(y,x,r)
y.inputstyle="remove"
Button.render(y,x,r)
end
btndl1.write = function (r,x)
local sPath, sFile, fd, block
sPath = "/etc/openclash/proxy_provider/"..p[x].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm1=tb1:option(Button,"remove1",translate("Remove"))
btnrm1.render=function(p,x,r)
p.inputstyle="reset"
Button.render(p,x,r)
end
btnrm1.write=function(r,x)
local r=fs.unlink("/etc/openclash/proxy_provider/"..luci.openclash.basename(p[x].name))
if r then table.remove(p,x)end
return r
end
local t = {
{Refresh, Delete_all, Apply}
}
a = proxy_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/proxy_provider/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
return proxy_form

View File

@ -0,0 +1,132 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Rule Providers"))
m.pageaction = false
m.description=translate("规则集使用介绍https://lancellc.gitbook.io/clash/clash-config-file/rule-provider")
m.redirect = luci.dispatcher.build_url("admin/services/openclash/rule-providers-settings")
if m.uci:get(openclash, sid) ~= "rule_providers" then
luci.http.redirect(m.redirect)
return
end
-- [[ Rule Providers Setting ]]--
s = m:section(NamedSection, sid, "rule_providers")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(Value, "name", translate("Rule Providers Name"))
o.rmempty = false
o.default = "Rule-provider - "..sid
o = s:option(ListValue, "type", translate("Rule Providers Type"))
o.rmempty = true
o.description = translate("Choose The Rule Providers Type")
o:value("http", translate("http"))
o:value("file", translate("file"))
o = s:option(ListValue, "behavior", translate("Rule Behavior"))
o.rmempty = true
o.description = translate("Choose The Rule Behavior")
o:value("domain")
o:value("ipcidr")
o:value("classical")
o = s:option(ListValue, "path", translate("Rule Providers Path"))
o.description = translate("Update Your Rule Providers File From Config Luci Page")
local p,h={}
for t,f in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
h=fs.stat(f)
if h then
p[t]={}
p[t].name=fs.basename(f)
o:value("./rule_provider/"..p[t].name)
end
end
o.rmempty = false
o:depends("type", "file")
o = s:option(Value, "url", translate("Rule Providers URL"))
o.rmempty = false
o:depends("type", "http")
o = s:option(Value, "interval", translate("Rule Providers Interval(s)"))
o.default = "86400"
o.rmempty = false
o:depends("type", "http")
o = s:option(ListValue, "position", translate("Append Position"))
o.rmempty = false
o:value("0", translate("Priority Match"))
o:value("1", translate("Extended Match"))
o = s:option(ListValue, "group", translate("Set Proxy Group"))
o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT'")..bold_off..font_off
o.rmempty = true
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("REJECT")
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
sys.call("/usr/share/openclash/yml_groups_name_ch.sh")
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
return m

View File

@ -0,0 +1,105 @@
local rule_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local g,h={}
for n,m in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
h=fs.stat(m)
if h then
g[n]={}
g[n].name=fs.basename(m)
g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime)
g[n].size=fs.filesize(h.size)
g[n].remove=0
g[n].enable=false
end
end
rule_form=SimpleForm("rule_provider_file_list",translate("Rule Providers File List"))
rule_form.reset=false
rule_form.submit=false
tb2=rule_form:section(Table,g)
nm2=tb2:option(DummyValue,"name",translate("File Name"))
mt2=tb2:option(DummyValue,"mtime",translate("Update Time"))
sz2=tb2:option(DummyValue,"size",translate("Size"))
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
btndl2.template="openclash/other_button"
btndl2.render=function(m,n,h)
m.inputstyle="remove"
Button.render(m,n,h)
end
btndl2.write = function (h,n)
local sPath, sFile, fd, block
sPath = "/etc/openclash/rule_provider/"..g[n].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm2=tb2:option(Button,"remove2",translate("Remove"))
btnrm2.render=function(g,n,h)
g.inputstyle="reset"
Button.render(g,n,h)
end
btnrm2.write=function(h,n)
local h=fs.unlink("/etc/openclash/rule_provider/"..luci.openclash.basename(g[n].name))
if h then table.remove(g,n)end
return h
end
local t = {
{Refresh, Delete_all, Apply}
}
a = rule_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/rule_provider/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
return rule_form

View File

@ -0,0 +1,106 @@
local form, m
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
m = SimpleForm("openclash", translate("Other Rule Providers List"))
m.description=translate("Rule Project:").." ConnersHua ( https://github.com/DivineEngine/Profiles )<br/>"..
translate("Rule Project:").." lhie1 ( https://github.com/dler-io/Rules )<br/>"..
translate("Rule Project:").." ACL4SSR ( https://github.com/ACL4SSR/ACL4SSR/tree/master )"
m.reset = false
m.submit = false
local t = {
{Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-settings"))
end
if not NXFS.access("/tmp/rule_providers_name") then
SYS.call("awk -v d=',' -F ',' '{print $4d$5}' /usr/share/openclash/res/rule_providers.list > /tmp/rule_providers_name 2>/dev/null")
end
file = io.open("/tmp/rule_providers_name", "r");
---- Rules List
local e={},o,t
if NXFS.access("/tmp/rule_providers_name") then
for o in file:lines() do
table.insert(e,o)
end
for t,o in ipairs(e) do
e[t]={}
e[t].num=string.format(t)
e[t].name=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $1}' 2>/dev/null",o)),1,-2)
e[t].lfilename=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $6}' 2>/dev/null",o)),1,-2)
if e[t].lfilename == "" then
e[t].lfilename=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $5}' 2>/dev/null",o)),1,-2)
end
e[t].filename=o
e[t].author=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $2}' 2>/dev/null",o)),1,-2)
e[t].rule_type=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $3}' 2>/dev/null",o)),1,-2)
RULE_FILE="/etc/openclash/rule_provider/".. e[t].lfilename
if fs.mtime(RULE_FILE) then
e[t].size=fs.filesize(fs.stat(RULE_FILE).size)
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(RULE_FILE))
else
e[t].size="/"
e[t].mtime="/"
end
if fs.isfile(RULE_FILE) then
e[t].exist=translate("Exist")
else
e[t].exist=translate("Not Exist")
end
e[t].remove=0
end
end
file:close()
form=SimpleForm("filelist")
form.reset=false
form.submit=false
tb=form:section(Table,e)
nu=tb:option(DummyValue,"num",translate("Order Number"))
st=tb:option(DummyValue,"exist",translate("State"))
st.template="openclash/cfg_check"
tp=tb:option(DummyValue,"rule_type",translate("Rule Type"))
nm=tb:option(DummyValue,"name",translate("Rule Name"))
au=tb:option(DummyValue,"author",translate("Rule Author"))
fm=tb:option(DummyValue,"lfilename",translate("File Name"))
sz=tb:option(DummyValue,"size",translate("Size"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
btnis=tb:option(DummyValue,"filename",translate("Download Rule"))
btnis.template="openclash/download_rule"
btnrm=tb:option(Button,"remove",translate("Remove"))
btnrm.render=function(e,t,a)
e.inputstyle="reset"
Button.render(e,t,a)
end
btnrm.write=function(a,t)
fs.unlink("/etc/openclash/rule_provider/"..e[t].lfilename)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-manage"))
end
return m, form

View File

@ -0,0 +1,261 @@
local m, s, o
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
m = Map(openclash, translate("Rule Providers and Groups"))
m.pageaction = false
m.description=translate("Attention:")..
"<br/>"..translate("The game proxy is a test function and does not guarantee the availability of rules")..
"<br/>"..translate("Preparation steps:")..
"<br/>"..translate("1. In the <server and policy group management> page, create the policy group and node you are going to use, and apply the configuration (when adding nodes, you must select the policy group you want to join). Policy group type suggestion: fallback, game nodes must support UDP")..
"<br/>"..translate("2. Click the <manage third party game rules> or <manage third party rule set> button to enter the rule list and download the rules you want to use")..
"<br/>"..translate("3. On this page, set the corresponding configuration file and policy group of the rule you have downloaded, and save the settings")..
"<br/>"..translate("4. Install the TUN core")..
"<br/>"..
"<br/>"..translate("When setting this page, if the groups is empty, please go to the <server and group management> page to add")..
"<br/>"..
"<br/>"..translate("Introduction to rule set usage: https://lancellc.gitbook.io/clash/clash-config-file/rule-provider")
function IsRuleFile(e)
e=e or""
local e=string.lower(string.sub(e,-6,-1))
return e==".rules"
end
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
-- [[ Edit Game Rule ]] --
s = m:section(TypedSection, "game_config", translate("Game Rules and Groups (Only TUN Core Support)"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- config
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
---- rule name
o = s:option(DynamicList, "rule_name", translate("Game Rule's Name"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/game_rules/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].filename=fs.basename(f)
if IsRuleFile(e[t].filename) then
e[t].name=string.gsub(luci.sys.exec(string.format("grep ',%s$' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $1}' 2>/dev/null",e[t].filename)), "[\r\n]", "")
if e[t].name ~= "" and e[t].name ~= nil then
o:value(e[t].name)
end
end
end
end
o.rmempty = true
---- Proxy Group
o = s:option(ListValue, "group", translate("Select Proxy Group"))
uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("REJECT")
o.rmempty = true
-- [[ Edit Other Rule Provider ]] --
s = m:section(TypedSection, "rule_provider_config", translate("Other Rule Providers and Groups (Only TUN Core Support)"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- config
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
---- rule name
o = s:option(DynamicList, "rule_name", translate("Rule Provider's Name"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].filename=fs.basename(f)
if IsYamlFile(e[t].filename) or IsYmlFile(e[t].filename) then
e[t].name=string.gsub(luci.sys.exec(string.format("grep ',%s$' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $1}' 2>/dev/null",e[t].filename)), "[\r\n]", "")
if e[t].name ~= "" and e[t].name ~= nil then
o:value(e[t].name)
end
end
end
end
o.rmempty = true
---- Proxy Group
o = s:option(ListValue, "group", translate("Select Proxy Group"))
uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("REJECT")
o.rmempty = true
o = s:option(Value, "interval", translate("Rule Providers Interval(s)"))
o.default = "86400"
o.rmempty = false
---- position
o = s:option(ListValue, "position", translate("Append Position"))
o.rmempty = false
o:value("0", translate("Priority Match"))
o:value("1", translate("Extended Match"))
-- [[ Edit Custom Rule Provider ]] --
s = m:section(TypedSection, "rule_providers", translate("Custom Rule Providers and Groups (Only TUN Core Support)"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/rule-providers-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "name", translate("Rule Providers Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(ListValue, "position", translate("Append Position"))
o.rmempty = false
o:value("0", translate("Priority Match"))
o:value("1", translate("Extended Match"))
local rm = {
{rule_mg, pro_mg}
}
rmg = m:section(Table, rm)
o = rmg:option(Button, "rule_mg", " ")
o.inputtitle = translate("Game Rules Manage")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage"))
end
o = rmg:option(Button, "pro_mg", " ")
o.inputtitle = translate("Other Rule Provider Manage")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-manage"))
end
local t = {
{Commit, Apply}
}
ss = m:section(Table, t)
o = ss:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
end
o = ss:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,445 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
local uuid = luci.sys.exec("cat /proc/sys/kernel/random/uuid")
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
local encrypt_methods_ss = {
-- stream
"rc4-md5",
"aes-128-cfb",
"aes-192-cfb",
"aes-256-cfb",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"aes-128-gcm",
"aes-192-gcm",
"aes-256-gcm",
"chacha20-ietf",
"xchacha20",
"chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305",
}
local encrypt_methods_ssr = {
"rc4-md5",
"aes-128-cfb",
"aes-192-cfb",
"aes-256-cfb",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"chacha20-ietf",
"xchacha20",
}
local securitys = {
"auto",
"none",
"aes-128-gcm",
"chacha20-poly1305"
}
local protocols = {
"origin",
"auth_sha1_v4",
"auth_aes128_md5",
"auth_aes128_sha1",
"auth_chain_a",
"auth_chain_b",
}
local obfs = {
"plain",
"http_simple",
"http_post",
"random_head",
"tls1.2_ticket_auth",
"tls1.2_ticket_fastauth",
}
m = Map(openclash, translate("Edit Server"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers")
if m.uci:get(openclash, sid) ~= "servers" then
luci.http.redirect(m.redirect)
return
end
-- [[ Servers Setting ]] --
s = m:section(NamedSection, sid, "servers")
s.anonymous = true
s.addremove = false
o = s:option(DummyValue, "server_url", "SS/SSR/VMESS/TROJAN URL")
o.rawhtml = true
o.template = "openclash/server_url"
o.value = sid
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(ListValue, "type", translate("Server Node Type"))
o:value("ss", translate("Shadowsocks"))
o:value("ssr", translate("ShadowsocksR"))
o:value("vmess", translate("Vmess"))
o:value("trojan", translate("trojan"))
o:value("snell", translate("Snell"))
o:value("socks5", translate("Socks5"))
o:value("http", translate("HTTP(S)"))
o.description = translate("Using incorrect encryption mothod may causes service fail to start")
o = s:option(Value, "name", translate("Server Alias"))
o.rmempty = false
o.default = "Server - "..sid
if not m.uci:get("openclash", sid, "name") then
m.uci:set("openclash", sid, "manual", 1)
end
o = s:option(Value, "server", translate("Server Address"))
o.datatype = "host"
o.rmempty = true
o = s:option(Value, "port", translate("Server Port"))
o.datatype = "port"
o.rmempty = false
o.default = 443
o = s:option(Value, "password", translate("Password"))
o.password = true
o.rmempty = false
o:depends("type", "ss")
o:depends("type", "ssr")
o:depends("type", "trojan")
o = s:option(Value, "psk", translate("Psk"))
o.rmempty = false
o:depends("type", "snell")
o = s:option(ListValue, "snell_version", translate("Version"))
o:value("2")
o:value("3")
o:depends("type", "snell")
o = s:option(ListValue, "cipher", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods_ss) do o:value(v) end
o.rmempty = true
o:depends("type", "ss")
o = s:option(ListValue, "cipher_ssr", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods_ssr) do o:value(v) end
o.rmempty = true
o:depends("type", "ssr")
o = s:option(ListValue, "protocol", translate("Protocol"))
for _, v in ipairs(protocols) do o:value(v) end
o.rmempty = true
o:depends("type", "ssr")
o = s:option(Value, "protocol_param", translate("Protocol param(optional)"))
o:depends("type", "ssr")
o = s:option(ListValue, "securitys", translate("Encrypt Method"))
for _, v in ipairs(securitys) do o:value(v) end
o.rmempty = true
o:depends("type", "vmess")
o = s:option(ListValue, "obfs_ssr", translate("Obfs"))
for _, v in ipairs(obfs) do o:value(v) end
o.rmempty = true
o:depends("type", "ssr")
o = s:option(Value, "obfs_param", translate("Obfs param(optional)"))
o:depends("type", "ssr")
-- AlterId
o = s:option(Value, "alterId", translate("AlterId"))
o.datatype = "port"
o.default = 32
o.rmempty = true
o:depends("type", "vmess")
-- VmessId
o = s:option(Value, "uuid", translate("VmessId (UUID)"))
o.rmempty = true
o.default = uuid
o:depends("type", "vmess")
o = s:option(ListValue, "udp", translate("UDP Enable"))
o.rmempty = true
o.default = "false"
o:value("true")
o:value("false")
o:depends("type", "ss")
o:depends("type", "ssr")
o:depends("type", "vmess")
o:depends("type", "socks5")
o:depends("type", "trojan")
o:depends({type = "snell", snell_version = "3"})
o = s:option(ListValue, "obfs", translate("obfs-mode"))
o.rmempty = true
o.default = "none"
o:value("none")
o:value("tls")
o:value("http")
o:value("websocket", translate("websocket (ws)"))
o:depends("type", "ss")
o = s:option(ListValue, "obfs_snell", translate("obfs-mode"))
o.rmempty = true
o.default = "none"
o:value("none")
o:value("tls")
o:value("http")
o:depends("type", "snell")
o = s:option(ListValue, "obfs_vmess", translate("obfs-mode"))
o.rmempty = true
o.default = "none"
o:value("none")
o:value("websocket", translate("websocket (ws)"))
o:value("http", translate("http"))
o:value("h2", translate("h2"))
o:value("grpc", translate("grpc"))
o:depends("type", "vmess")
o = s:option(ListValue, "obfs_trojan", translate("obfs-mode"))
o.rmempty = true
o.default = "none"
o:value("none")
o:value("ws", translate("websocket (ws)"))
o:value("grpc", translate("grpc"))
o:depends("type", "trojan")
o = s:option(Value, "host", translate("obfs-hosts"))
o.datatype = "host"
o.placeholder = translate("example.com")
o.rmempty = true
o:depends("obfs", "tls")
o:depends("obfs", "http")
o:depends("obfs", "websocket")
o:depends("obfs_snell", "tls")
o:depends("obfs_snell", "http")
-- vmess路径
o = s:option(Value, "path", translate("path"))
o.rmempty = true
o.placeholder = translate("/")
o:depends("obfs", "websocket")
o = s:option(DynamicList, "h2_host", translate("host"))
o.rmempty = true
o.placeholder = translate("http.example.com")
o.datatype = "host"
o:depends("obfs_vmess", "h2")
o = s:option(Value, "h2_path", translate("path"))
o.rmempty = true
o.default = "/"
o:depends("obfs_vmess", "h2")
o = s:option(DynamicList, "http_path", translate("path"))
o.rmempty = true
o:value("/")
o:value("/video")
o:depends("obfs_vmess", "http")
o = s:option(Value, "custom", translate("headers"))
o.rmempty = true
o.placeholder = translate("v2ray.com")
o:depends("obfs", "websocket")
o = s:option(Value, "ws_opts_path", translate("ws-opts-path"))
o.rmempty = true
o.placeholder = translate("/path")
o:depends("obfs_vmess", "websocket")
o = s:option(DynamicList, "ws_opts_headers", translate("ws-opts-headers"))
o.rmempty = true
o.placeholder = translate("Host: v2ray.com")
o:depends("obfs_vmess", "websocket")
o = s:option(Value, "max_early_data", translate("max-early-data"))
o.rmempty = true
o.placeholder = translate("2048")
o:depends("obfs_vmess", "websocket")
o = s:option(Value, "early_data_header_name", translate("early-data-header-name"))
o.rmempty = true
o.placeholder = translate("Sec-WebSocket-Protocol")
o:depends("obfs_vmess", "websocket")
-- [[ skip-cert-verify ]]--
o = s:option(ListValue, "skip_cert_verify", translate("skip-cert-verify"))
o.rmempty = true
o.default = "false"
o:value("true")
o:value("false")
o:depends("obfs", "websocket")
o:depends("obfs_vmess", "none")
o:depends("obfs_vmess", "websocket")
o:depends("obfs_vmess", "grpc")
o:depends("type", "socks5")
o:depends("type", "http")
o:depends("type", "trojan")
-- [[ TLS ]]--
o = s:option(ListValue, "tls", translate("tls"))
o.rmempty = true
o.default = "false"
o:value("true")
o:value("false")
o:depends("obfs", "websocket")
o:depends("type", "vmess")
o:depends("type", "socks5")
o:depends("type", "http")
o = s:option(Value, "servername", translate("servername"))
o.rmempty = true
o.datatype = "host"
o.placeholder = translate("example.com")
o:depends({obfs_vmess = "websocket", tls = "true"})
o:depends({obfs_vmess = "grpc", tls = "true"})
o:depends({obfs_vmess = "none", tls = "true"})
o = s:option(Value, "keep_alive", translate("keep-alive"))
o.rmempty = true
o.default = "true"
o:value("true")
o:value("false")
o:depends("obfs_vmess", "http")
-- [[ MUX ]]--
o = s:option(ListValue, "mux", translate("mux"))
o.rmempty = true
o.default = "false"
o:value("true")
o:value("false")
o:depends("obfs", "websocket")
-- [[ sni ]]--
o = s:option(Value, "sni", translate("sni"))
o.datatype = "host"
o.placeholder = translate("example.com")
o.rmempty = true
o:depends("type", "trojan")
o:depends("type", "http")
-- 验证用户名
o = s:option(Value, "auth_name", translate("Auth Username"))
o:depends("type", "socks5")
o:depends("type", "http")
o.rmempty = true
-- 验证密码
o = s:option(Value, "auth_pass", translate("Auth Password"))
o:depends("type", "socks5")
o:depends("type", "http")
o.rmempty = true
-- [[ alpn ]]--
o = s:option(DynamicList, "alpn", translate("alpn"))
o.rmempty = true
o:value("h2")
o:value("http/1.1")
o:depends("type", "trojan")
-- [[ grpc ]]--
o = s:option(Value, "grpc_service_name", translate("grpc-service-name"))
o.rmempty = true
o.datatype = "host"
o.placeholder = translate("example")
o:depends("obfs_trojan", "grpc")
o:depends("obfs_vmess", "grpc")
-- [[ trojan-ws-path ]]--
o = s:option(Value, "trojan_ws_path", translate("Path"))
o.rmempty = true
o.placeholder = translate("/path")
o:depends("obfs_trojan", "ws")
-- [[ trojan-ws-headers ]]--
o = s:option(DynamicList, "trojan_ws_headers", translate("Headers"))
o.rmempty = true
o.placeholder = translate("Host: v2ray.com")
o:depends("obfs_trojan", "ws")
-- [[ interface-name ]]--
o = s:option(Value, "interface_name", translate("interface-name"))
o.rmempty = true
o.placeholder = translate("eth0")
-- [[ routing-mark ]]--
o = s:option(Value, "routing_mark", translate("routing-mark"))
o.rmempty = true
o.placeholder = translate("2333")
o = s:option(DynamicList, "groups", translate("Proxy Group"))
o.description = font_red..bold_on..translate("No Need Set when Config Create, The added Proxy Groups Must Exist")..bold_off..font_off
o.rmempty = true
o:value("all", translate("All Groups"))
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
sys.call("/usr/share/openclash/cfg_servers_address_fake_filter.sh &")
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
return m

View File

@ -0,0 +1,263 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Servers manage and Config create"))
m.pageaction = false
s = m:section(TypedSection, "openclash")
s.anonymous = true
o = s:option(Flag, "create_config", translate("Create Config"))
o.description = font_red .. bold_on .. translate("Create Config By One-Click Only Need Proxies") .. bold_off .. font_off
o.default=0
o = s:option(ListValue, "rule_sources", translate("Choose Template For Create Config"))
o.description = translate("Use Other Rules To Create Config")
o:depends("create_config", 1)
o:value("lhie1", translate("lhie1 Rules"))
o:value("ConnersHua", translate("ConnersHua(Provider-type) Rules"))
o:value("ConnersHua_return", translate("ConnersHua Return Rules"))
o = s:option(Flag, "mix_proxies", translate("Mix Proxies"))
o.description = font_red .. bold_on .. translate("Mix This Page's Proxies") .. bold_off .. font_off
o:depends("create_config", 1)
o.default=0
o = s:option(Flag, "servers_update", translate("Keep Settings"))
o.description = font_red .. bold_on .. translate("Only Update Servers Below When Subscription") .. bold_off .. font_off
o.default=0
o = s:option(DynamicList, "new_servers_group", translate("New Servers Group"))
o.description = translate("Set The New Subscribe Server's Default Proxy Groups")
o.rmempty = true
o:depends("servers_update", 1)
o:value("all", translate("All Groups"))
m.uci:foreach("openclash", "groups",
function(s)
o:value(s.name)
end)
-- [[ Groups Manage ]]--
s = m:section(TypedSection, "groups", translate("Proxy Groups(No Need Set when Config Create)"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/groups-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "type", translate("Group Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "name", translate("Group Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Proxy-Provider Manage ]]--
s = m:section(TypedSection, "proxy-provider", translate("Proxy-Provider"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/proxy-provider-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "type", translate("Provider Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "name", translate("Provider Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Servers Manage ]]--
s = m:section(TypedSection, "servers", translate("Proxies"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/servers-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "type", translate("Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "name", translate("Server Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "server", translate("Server Address"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "port", translate("Server Port"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "udp", translate("UDP Support"))
function o.cfgvalue(...)
if Value.cfgvalue(...) == "true" then
return translate("Enable")
elseif Value.cfgvalue(...) == "false" then
return translate("Disable")
else
return translate("None")
end
end
o = s:option(DummyValue,"server",translate("Ping Latency"))
o.template="openclash/ping"
o.width="10%"
local tt = {
{Delete_Unused_Servers, Delete_Servers, Delete_Proxy_Provider, Delete_Groups}
}
b = m:section(Table, tt)
o = b:option(Button,"Delete_Unused_Servers", " ")
o.inputtitle = translate("Delete Unused Servers")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:commit("openclash")
luci.sys.call("sh /usr/share/openclash/cfg_unused_servers_del.sh 2>/dev/null")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
o = b:option(Button,"Delete_Servers", " ")
o.inputtitle = translate("Delete Servers")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:delete_all("openclash", "servers", function(s) return true end)
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
o = b:option(Button,"Delete_Proxy_Provider", " ")
o.inputtitle = translate("Delete Proxy Providers")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:delete_all("openclash", "proxy-provider", function(s) return true end)
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
o = b:option(Button,"Delete_Groups", " ")
o.inputtitle = translate("Delete Groups")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:delete_all("openclash", "groups", function(s) return true end)
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
local t = {
{Load_Config, Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button,"Load_Config", " ")
o.inputtitle = translate("Read Config")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_get.sh 2>/dev/null &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash"))
end
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:set("openclash", "config", "enable", 0)
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:set("openclash", "config", "enable", 0)
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_set.sh >/dev/null 2>&1 &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/server_list"))
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,994 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local json = require "luci.jsonc"
font_green = [[<b style=color:green>]]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
local op_mode = string.sub(luci.sys.exec('uci get openclash.config.operation_mode 2>/dev/null'),0,-2)
if not op_mode then op_mode = "redir-host" end
local lan_ip=SYS.exec("uci -q get network.lan.ipaddr |awk -F '/' '{print $1}' 2>/dev/null |tr -d '\n' || ip addr show 2>/dev/null | grep -w 'inet' | grep 'global' | grep 'brd' | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -n 1 | tr -d '\n'")
m = Map("openclash", translate("Global Settings(Will Modify The Config File Or Subscribe According To The Settings On This Page)"))
m.pageaction = false
m.description=translate("To restore the default configuration, try accessing:").." <a href='javascript:void(0)' onclick='javascript:restore_config(this)'>http://"..lan_ip.."/cgi-bin/luci/admin/services/openclash/restore</a>"
s = m:section(TypedSection, "openclash")
s.anonymous = true
s:tab("op_mode", translate("Operation Mode"))
s:tab("settings", translate("General Settings"))
s:tab("dns", translate("DNS Setting"))
s:tab("stream_enhance", translate("Streaming Enhance"))
s:tab("lan_ac", translate("Access Control"))
if op_mode == "fake-ip" then
s:tab("rules", translate("Rules Setting(Access Control)"))
else
s:tab("rules", translate("Rules Setting"))
end
s:tab("dashboard", translate("Dashboard Settings"))
s:tab("rules_update", translate("Rules Update"))
s:tab("geo_update", translate("GEOIP Update"))
s:tab("chnr_update", translate("Chnroute Update"))
s:tab("auto_restart", translate("Auto Restart"))
s:tab("version_update", translate("Version Update"))
s:tab("debug", translate("Debug Logs"))
s:tab("dlercloud", translate("Dler Cloud"))
o = s:taboption("op_mode", ListValue, "en_mode", font_red..bold_on..translate("Select Mode")..bold_off..font_off)
o.description = translate("Select Mode For OpenClash Work, Try Flush DNS Cache If Network Error")
if op_mode == "redir-host" then
o:value("redir-host", translate("redir-host"))
o:value("redir-host-tun", translate("redir-host(tun mode)"))
o:value("redir-host-mix", translate("redir-host-mix(tun mix mode)"))
o.default = "redir-host"
else
o:value("fake-ip", translate("fake-ip"))
o:value("fake-ip-tun", translate("fake-ip(tun mode)"))
o:value("fake-ip-mix", translate("fake-ip-mix(tun mix mode)"))
o.default = "fake-ip"
end
o = s:taboption("op_mode", Flag, "enable_udp_proxy", font_red..bold_on..translate("Proxy UDP Traffics")..bold_off..font_off)
o.description = translate("The Servers Must Support UDP forwarding")..", "..font_red..bold_on..translate("If Docker is Installed, UDP May Not Forward Normally")..bold_off..font_off
o:depends("en_mode", "redir-host")
o:depends("en_mode", "fake-ip")
o.default=1
o = s:taboption("op_mode", ListValue, "stack_type", translate("Select Stack Type"))
o.description = translate("Select Stack Type For TUN Mode, According To The Running Speed on Your Machine")
o:depends("en_mode", "redir-host-tun")
o:depends("en_mode", "fake-ip-tun")
o:depends("en_mode", "redir-host-mix")
o:depends("en_mode", "fake-ip-mix")
o:value("system", translate("System "))
o:value("gvisor", translate("Gvisor"))
o.default = "system"
o = s:taboption("op_mode", ListValue, "proxy_mode", font_red..bold_on..translate("Proxy Mode")..bold_off..font_off)
o.description = translate("Select Proxy Mode, Use Script Mode Could Prevent Proxy BT traffics If Rules Support, eg.lhie1's")
o:value("rule", translate("Rule Proxy Mode"))
o:value("global", translate("Global Proxy Mode"))
o:value("direct", translate("Direct Proxy Mode"))
o:value("script", translate("Script Proxy Mode (Tun Core Only)"))
o.default = "rule"
o = s:taboption("op_mode", Flag, "ipv6_enable", font_red..bold_on..translate("Proxy IPv6 Traffic")..bold_off..font_off)
o.description = font_red..bold_on..translate("Disable IPv6 DHCP To Avoid Abnormal Connection If You Do Not Use")..bold_off..font_off
o.default=0
o = s:taboption("op_mode", Flag, "china_ip6_route", translate("China IPv6 Route"))
o.description = translate("Bypass The China Network Flows, Improve Performance")
o.default=0
o:depends("ipv6_enable", 1)
o = s:taboption("op_mode", Flag, "disable_udp_quic", font_red..bold_on..translate("Disable QUIC")..bold_off..font_off)
o.description = translate("Prevent YouTube and Others To Use QUIC Transmission")..", "..font_red..bold_on..translate("REJECT UDP Traffic On Port 443")..bold_off..font_off
o.default=1
o = s:taboption("op_mode", Flag, "enable_rule_proxy", font_red..bold_on..translate("Rule Match Proxy Mode")..bold_off..font_off)
o.description = translate("Only Proxy Rules Match, Prevent BT/P2P Passing")
o.default=0
o = s:taboption("op_mode", Flag, "common_ports", font_red..bold_on..translate("Common Ports Proxy Mode")..bold_off..font_off)
o.description = translate("Only Common Ports, Prevent BT/P2P Passing")
o.default=0
o:depends("en_mode", "redir-host")
o:depends("en_mode", "redir-host-tun")
o:depends("en_mode", "redir-host-mix")
o = s:taboption("op_mode", Flag, "china_ip_route", translate("China IP Route"))
o.description = translate("Bypass The China Network Flows, Improve Performance")
o.default=0
o:depends("en_mode", "redir-host")
o:depends("en_mode", "redir-host-tun")
o:depends("en_mode", "redir-host-mix")
o = s:taboption("op_mode", Flag, "small_flash_memory", translate("Small Flash Memory"))
o.description = translate("Move Core And GEOIP Data File To /tmp/etc/openclash For Small Flash Memory Device")
o.default=0
---- Operation Mode
switch_mode = s:taboption("op_mode", DummyValue, "", nil)
switch_mode.template = "openclash/switch_mode"
---- General Settings
o = s:taboption("settings", ListValue, "interface_name", font_red..bold_on..translate("Bind Network Interface")..bold_off..font_off)
local de_int = SYS.exec("ip route |grep 'default' |awk '{print $5}' 2>/dev/null") or SYS.exec("/usr/share/openclash/openclash_get_network.lua 'dhcp'")
o.description = translate("Default Interface Name:").." "..font_green..bold_on..de_int..bold_off..font_off..translate(",Try Enable If Network Loopback")
local interfaces = SYS.exec("ls -l /sys/class/net/ 2>/dev/null |awk '{print $9}' 2>/dev/null")
for interface in string.gmatch(interfaces, "%S+") do
o:value(interface)
end
o:value("0", translate("Disable"))
o.default=0
o = s:taboption("settings", Value, "tolerance", font_red..bold_on..translate("Url-Test Group Tolerance (ms)")..bold_off..font_off)
o.description = translate("Switch To The New Proxy When The Delay Difference Between Old and The Fastest Currently is Greater Than This Value")
o:value("0", translate("Disable"))
o:value("100")
o:value("150")
o.datatype = "uinteger"
o.default = "0"
o = s:taboption("settings", ListValue, "log_level", translate("Log Level"))
o.description = translate("Select Core's Log Level")
o:value("info", translate("Info Mode"))
o:value("warning", translate("Warning Mode"))
o:value("error", translate("Error Mode"))
o:value("debug", translate("Debug Mode"))
o:value("silent", translate("Silent Mode"))
o.default = "silent"
o = s:taboption("settings", Value, "log_size", translate("Log Size (KB)"))
o.description = translate("Set Log File Size (KB)")
o.default=1024
o = s:taboption("settings", Flag, "intranet_allowed", translate("Only intranet allowed"))
o.description = translate("When Enabled, The Control Panel And The Connection Broker Port Will Not Be Accessible From The Public Network, Not Support IPv6 Yet")
o.default=0
o = s:taboption("settings", Value, "dns_port")
o.title = translate("DNS Port")
o.default = 7874
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "proxy_port")
o.title = translate("Redir Port")
o.default = 7892
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "tproxy_port")
o.title = translate("TProxy Port")
o.default = 7895
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "http_port")
o.title = translate("HTTP(S) Port")
o.default = 7890
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "socks_port")
o.title = translate("SOCKS5 Port")
o.default = 7891
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "mixed_port")
o.title = translate("Mixed Port")
o.default = 7893
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
---- DNS Settings
o = s:taboption("dns", Flag, "enable_redirect_dns", font_red..bold_on..translate("Redirect Local DNS Setting")..bold_off..font_off)
o.description = translate("Set Local DNS Redirect")
o.default=1
o = s:taboption("dns", Flag, "enable_custom_dns", font_red..bold_on..translate("Custom DNS Setting")..bold_off..font_off)
o.description = font_red..bold_on..translate("Set OpenClash Upstream DNS Resolve Server")..bold_off..font_off
o.default=0
if op_mode == "redir-host" then
o = s:taboption("dns", Flag, "dns_remote", font_red..bold_on..translate("DNS Remote")..bold_off..font_off)
o.description = font_red..bold_on..translate("Add DNS Remote Support For Redir-Host")..bold_off..font_off
o.default=1
end
o = s:taboption("dns", Flag, "append_wan_dns", font_red..bold_on..translate("Append Upstream DNS")..bold_off..font_off)
o.description = font_red..bold_on..translate("Append The Upstream Assigned DNS And Gateway IP To The Nameserver")..bold_off..font_off
o.default=1
if op_mode == "fake-ip" then
o = s:taboption("dns", Flag, "store_fakeip", font_red..bold_on..translate("Persistence Fake-IP")..bold_off..font_off)
o.description = font_red..bold_on..translate("Cache Fake-IP DNS Resolution Records To File, Improve The Response Speed After Startup")..bold_off..font_off
o.default=1
end
o = s:taboption("dns", Flag, "ipv6_dns", translate("IPv6 DNS Resolve"))
o.description = font_red..bold_on..translate("Enable Clash to Resolve IPv6 DNS Requests")..bold_off..font_off
o.default=0
o = s:taboption("dns", Flag, "disable_masq_cache", translate("Disable Dnsmasq's DNS Cache"))
o.description = translate("Recommended Enabled For Avoiding Some Connection Errors")..font_red..bold_on..translate("(Maybe Incompatible For Your Firmware)")..bold_off..font_off
o.default=0
o = s:taboption("dns", Flag, "custom_fallback_filter", translate("Custom Fallback-Filter"))
o.description = translate("Take Effect If Fallback DNS Setted, Prevent DNS Pollution")
o.default=0
custom_fallback_filter = s:taboption("dns", Value, "custom_fallback_fil")
custom_fallback_filter.template = "cbi/tvalue"
custom_fallback_filter.rows = 20
custom_fallback_filter.wrap = "off"
custom_fallback_filter:depends("custom_fallback_filter", "1")
function custom_fallback_filter.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml") or ""
end
function custom_fallback_filter.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml", value)
end
end
end
o = s:taboption("dns", Flag, "dns_advanced_setting", translate("Advanced Setting"))
o.description = translate("DNS Advanced Settings")..font_red..bold_on..translate("(Please Don't Modify it at Will)")..bold_off..font_off
o.default=0
if op_mode == "fake-ip" then
o = s:taboption("dns", Button, translate("Fake-IP-Filter List Update"))
o.title = translate("Fake-IP-Filter List Update")
o:depends("dns_advanced_setting", "1")
o.inputtitle = translate("Check And Update")
o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("rm -rf /tmp/openclash_fake_filter.list >/dev/null 2>&1 && /etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
custom_fake_black = s:taboption("dns", Value, "custom_fake_filter")
custom_fake_black.template = "cbi/tvalue"
custom_fake_black.description = translate("Domain Names In The List Do Not Return Fake-IP, One rule per line")
custom_fake_black.rows = 20
custom_fake_black.wrap = "off"
custom_fake_black:depends("dns_advanced_setting", "1")
function custom_fake_black.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list") or ""
end
function custom_fake_black.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_fake_filter.list", value)
end
end
end
end
o = s:taboption("dns", Value, "custom_domain_dns_server", translate("Specify DNS Server"))
o.description = translate("Specify DNS Server For List and Server Nodes With Fake-IP Mode, Only One IP Server Address Support")
o.default="114.114.114.114"
o.placeholder = translate("114.114.114.114 or 127.0.0.1#5300")
o:depends("dns_advanced_setting", "1")
custom_domain_dns = s:taboption("dns", Value, "custom_domain_dns")
custom_domain_dns.template = "cbi/tvalue"
custom_domain_dns.description = translate("Domain Names In The List Use The Custom DNS Server, One rule per line")
custom_domain_dns.rows = 20
custom_domain_dns.wrap = "off"
custom_domain_dns:depends("dns_advanced_setting", "1")
function custom_domain_dns.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns.list") or ""
end
function custom_domain_dns.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_domain_dns.list", value)
end
end
end
custom_domain_dns_policy = s:taboption("dns", Value, "custom_domain_dns_core")
custom_domain_dns_policy.template = "cbi/tvalue"
custom_domain_dns_policy.description = translate("Domain Names In The List Use The Custom DNS Server, But Still Return Fake-IP Results, One rule per line")
custom_domain_dns_policy.rows = 20
custom_domain_dns_policy.wrap = "off"
custom_domain_dns_policy:depends("dns_advanced_setting", "1")
function custom_domain_dns_policy.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list") or ""
end
function custom_domain_dns_policy.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list", value)
end
end
end
---- Access Control
if op_mode == "redir-host" then
o = s:taboption("lan_ac", ListValue, "lan_ac_mode", translate("LAN Access Control Mode"))
o:value("0", translate("Black List Mode"))
o:value("1", translate("White List Mode"))
o.default=0
ip_b = s:taboption("lan_ac", DynamicList, "lan_ac_black_ips", translate("LAN Bypassed Host List"))
ip_b:depends("lan_ac_mode", "0")
ip_b.datatype = "ipaddr"
mac_b = s:taboption("lan_ac", DynamicList, "lan_ac_black_macs", translate("LAN Bypassed Mac List"))
mac_b.datatype = "list(macaddr)"
mac_b.rmempty = true
mac_b:depends("lan_ac_mode", "0")
ip_w = s:taboption("lan_ac", DynamicList, "lan_ac_white_ips", translate("LAN Proxied Host List"))
ip_w:depends("lan_ac_mode", "1")
ip_w.datatype = "ipaddr"
mac_w = s:taboption("lan_ac", DynamicList, "lan_ac_white_macs", translate("LAN Proxied Mac List"))
mac_w.datatype = "list(macaddr)"
mac_w.rmempty = true
mac_w:depends("lan_ac_mode", "1")
luci.ip.neighbors({ family = 4 }, function(n)
if n.mac and n.dest then
ip_b:value(n.dest:string())
ip_w:value(n.dest:string())
mac_b:value(n.mac, "%s (%s)" %{ n.mac, n.dest:string() })
mac_w:value(n.mac, "%s (%s)" %{ n.mac, n.dest:string() })
end
end)
if string.len(SYS.exec("/usr/share/openclash/openclash_get_network.lua 'gateway6'")) ~= 0 then
luci.ip.neighbors({ family = 6 }, function(n)
if n.mac and n.dest then
ip_b:value(n.dest:string())
ip_w:value(n.dest:string())
mac_b:value(n.mac, "%s (%s)" %{ n.mac, n.dest:string() })
mac_w:value(n.mac, "%s (%s)" %{ n.mac, n.dest:string() })
end
end)
end
end
o = s:taboption("lan_ac", DynamicList, "wan_ac_black_ips", translate("WAN Bypassed Host List"))
o.datatype = "ipaddr"
o.description = translate("In The Fake-IP Mode, Only Pure IP Requests Are Supported")
---- Rules Settings
o = s:taboption("rules", Flag, "rule_source", translate("Enable Other Rules"))
o.description = translate("Use Other Rules")
o.default=0
if op_mode == "fake-ip" then
o = s:taboption("rules", Flag, "enable_custom_clash_rules", font_red..bold_on..translate("Custom Clash Rules(Access Control)")..bold_off..font_off)
else
o = s:taboption("rules", Flag, "enable_custom_clash_rules", font_red..bold_on..translate("Custom Clash Rules")..bold_off..font_off)
end
o.description = translate("Use Custom Rules")
o.default=0
custom_rules = s:taboption("rules", Value, "custom_rules")
custom_rules:depends("enable_custom_clash_rules", 1)
custom_rules.template = "cbi/tvalue"
custom_rules.description = translate("Custom Priority Rules Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://lancellc.gitbook.io/clash/clash-config-file/rules\")'>https://lancellc.gitbook.io/clash/clash-config-file/rules</a>".." ,"..translate("IP To CIDR:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"http://ip2cidr.com\")'>http://ip2cidr.com</a>"
custom_rules.rows = 20
custom_rules.wrap = "off"
function custom_rules.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_rules.list") or ""
end
function custom_rules.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_rules.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_rules.list", value)
end
end
end
custom_rules_2 = s:taboption("rules", Value, "custom_rules_2")
custom_rules_2:depends("enable_custom_clash_rules", 1)
custom_rules_2.template = "cbi/tvalue"
custom_rules_2.description = translate("Custom Extended Rules Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://lancellc.gitbook.io/clash/clash-config-file/rules\")'>https://lancellc.gitbook.io/clash/clash-config-file/rules</a>".." ,"..translate("IP To CIDR:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"http://ip2cidr.com\")'>http://ip2cidr.com</a>"
custom_rules_2.rows = 20
custom_rules_2.wrap = "off"
function custom_rules_2.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_rules_2.list") or ""
end
function custom_rules_2.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_rules_2.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_rules_2.list", value)
end
end
end
--Stream Enhance
o = s:taboption("stream_enhance", Flag, "stream_domains_prefetch", font_red..bold_on..translate("Prefetch Netflix, Disney Plus Domains")..bold_off..font_off)
o.description = translate("Prevent Some Devices From Directly Using IP Access To Cause Unlocking Failure")
o.default=0
o = s:taboption("stream_enhance", Value, "stream_domains_prefetch_interval", translate("Domains Prefetch Interval(min)"))
o.default=1440
o.datatype = "uinteger"
o.description = translate("Will Run Once Immediately After Started, The Interval Does Not Need To Be Too Short (Take Effect Immediately After Commit)")
o:depends("stream_domains_prefetch", "1")
o = s:taboption("stream_enhance", DummyValue, "stream_domains_update", translate("Update Preset Domains List"))
o:depends("stream_domains_prefetch", "1")
o.template = "openclash/download_stream_domains"
o = s:taboption("stream_enhance", Flag, "stream_auto_select", font_red..bold_on..translate("Auto Select Unlock Proxy")..bold_off..font_off)
o.description = translate("Auto Select Proxy For Streaming Unlock, Support Netflix, Disney Plus, HBO And YouTube Premium, etc")
o.default=0
o = s:taboption("stream_enhance", Value, "stream_auto_select_interval", translate("Auto Select Interval(min)"))
o.default=30
o.datatype = "uinteger"
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_expand_group", font_red..bold_on..translate("Expand Group")..bold_off..font_off)
o.description = translate("Automatically Expand The Group When Selected")
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_netflix", translate("Netflix"))
o.default=1
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_netflix", translate("Netflix Group Filter"))
o.default = "Netflix|奈飞"
o.placeholder = "Netflix|奈飞"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_netflix", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_netflix", translate("Netflix Unlock Region Filter"))
o.default = ""
o.placeholder = "HK|SG|TW"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_netflix", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_disney", translate("Disney Plus"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_disney", translate("Disney Plus Group Filter"))
o.default = "Disney|迪士尼"
o.placeholder = "Disney|迪士尼"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_disney", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_disney", translate("Disney Plus Unlock Region Filter"))
o.default = ""
o.placeholder = "HK|SG|TW"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_disney", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_ytb", translate("YouTube Premium"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_ytb", translate("YouTube Premium Group Filter"))
o.default = "YouTube|油管"
o.placeholder = "YouTube|油管"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_ytb", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_ytb", translate("YouTube Premium Unlock Region Filter"))
o.default = ""
o.placeholder = "HK|US"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_ytb", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_prime_video", translate("Amazon Prime Video"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_prime_video", translate("Amazon Prime Video Group Filter"))
o.default = "Amazon|Prime Video"
o.placeholder = "Amazon|Prime Video"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_prime_video", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_prime_video", translate("Amazon Prime Video Unlock Region Filter"))
o.default = ""
o.placeholder = "HK|US|SG"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_prime_video", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_hbo_now", translate("HBO Now"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_hbo_now", translate("HBO Now Group Filter"))
o.default = "HBO|HBONow|HBO Now"
o.placeholder = "HBO|HBONow|HBO Now"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_hbo_now", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_hbo_max", translate("HBO Max"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_hbo_max", translate("HBO Max Group Filter"))
o.default = "HBO|HBOMax|HBO Max"
o.placeholder = "HBO|HBOMax|HBO Max"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_hbo_max", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_hbo_max", translate("HBO Max Unlock Region Filter"))
o.default = ""
o.placeholder = "US"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_hbo_max", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_hbo_go_asia", translate("HBO GO Asia"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_hbo_go_asia", translate("HBO GO Asia Group Filter"))
o.default = "HBO|HBOGO|HBO GO"
o.placeholder = "HBO|HBOGO|HBO GO"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_hbo_go_asia", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_hbo_go_asia", translate("HBO Max Unlock Region Filter"))
o.default = ""
o.placeholder = "HK|SG|TW"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_hbo_go_asia", "1")
o = s:taboption("stream_enhance", Flag, "stream_auto_select_tvb_anywhere", translate("TVB Anywhere+"))
o.default=0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_tvb_anywhere", translate("TVB Anywhere+ Group Filter"))
o.default = "TVB"
o.placeholder = "TVB"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_tvb_anywhere", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_tvb_anywhere", translate("HBO Max Unlock Region Filter"))
o.default = ""
o.placeholder = "HK|SG|TW"
o.description = translate("It Will Be Selected Region According To The Regex")
o:depends("stream_auto_select_tvb_anywhere", "1")
---- update Settings
o = s:taboption("rules_update", Flag, "other_rule_auto_update", translate("Auto Update"))
o.description = font_red..bold_on..translate("Auto Update Other Rules")..bold_off..font_off
o.default=0
o = s:taboption("rules_update", ListValue, "other_rule_update_week_time", translate("Update Time (Every Week)"))
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default=1
o = s:taboption("rules_update", ListValue, "other_rule_update_day_time", translate("Update time (every day)"))
for t = 0,23 do
o:value(t, t..":00")
end
o.default=0
o = s:taboption("rules_update", Button, translate("Other Rules Update"))
o.title = translate("Update Other Rules")
o.inputtitle = translate("Check And Update")
o.description = translate("Other Rules Update(Only in Use)")
o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_rule.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
o = s:taboption("geo_update", Flag, "geo_auto_update", translate("Auto Update"))
o.description = translate("Auto Update GEOIP Database")
o.default=0
o = s:taboption("geo_update", ListValue, "geo_update_week_time", translate("Update Time (Every Week)"))
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default=1
o = s:taboption("geo_update", ListValue, "geo_update_day_time", translate("Update time (every day)"))
for t = 0,23 do
o:value(t, t..":00")
end
o.default=0
o = s:taboption("geo_update", Value, "geo_custom_url")
o.title = translate("Custom GEOIP URL")
o.rmempty = false
o.description = translate("Custom GEOIP Data URL, Click Button Below To Refresh After Edit")
o:value("https://cdn.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb", translate("Alecthw-lite-Version")..translate("(Default mmdb)"))
o:value("https://cdn.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/Country.mmdb", translate("Alecthw-Version")..translate("(All Info mmdb)"))
o:value("https://cdn.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/Country.mmdb", translate("Hackl0us-Version")..translate("(Only CN)"))
o:value("https://geolite.clash.dev/Country.mmdb", translate("Geolite.clash.dev"))
o.default = "http://www.ideame.top/mmdb/Country.mmdb"
o = s:taboption("geo_update", Button, translate("GEOIP Update"))
o.title = translate("Update GEOIP Database")
o.inputtitle = translate("Check And Update")
o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_ipdb.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
o = s:taboption("chnr_update", Flag, "chnr_auto_update", translate("Auto Update"))
o.description = translate("Auto Update Chnroute Lists")
o.default=0
o = s:taboption("chnr_update", ListValue, "chnr_update_week_time", translate("Update Time (Every Week)"))
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default=1
o = s:taboption("chnr_update", ListValue, "chnr_update_day_time", translate("Update time (every day)"))
for t = 0,23 do
o:value(t, t..":00")
end
o.default=0
o = s:taboption("chnr_update", Value, "chnr_custom_url")
o.title = translate("Custom Chnroute Lists URL")
o.rmempty = false
o.description = translate("Custom Chnroute Lists URL, Click Button Below To Refresh After Edit")
o:value("https://ispip.clang.cn/all_cn.txt", translate("Clang-CN")..translate("(Default)"))
o:value("https://ispip.clang.cn/all_cn_cidr.txt", translate("Clang-CN-CIDR"))
o:value("https://cdn.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us-CN-CIDR")..translate("(Large Size)"))
o.default = "https://ispip.clang.cn/all_cn.txt"
o = s:taboption("chnr_update", Value, "chnr6_custom_url")
o.title = translate("Custom Chnroute6 Lists URL")
o.rmempty = false
o.description = translate("Custom Chnroute6 Lists URL, Click Button Below To Refresh After Edit")
o:value("https://ispip.clang.cn/all_cn_ipv6.txt", translate("Clang-CN-IPV6")..translate("(Default)"))
o.default = "https://ispip.clang.cn/all_cn_ipv6.txt"
o = s:taboption("chnr_update", Button, translate("Chnroute Lists Update"))
o.title = translate("Update Chnroute Lists")
o.inputtitle = translate("Check And Update")
o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_chnroute.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
o = s:taboption("auto_restart", Flag, "auto_restart", translate("Auto Restart"))
o.description = translate("Auto Restart OpenClash")
o.default=0
o = s:taboption("auto_restart", ListValue, "auto_restart_week_time", translate("Restart Time (Every Week)"))
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default=1
o = s:taboption("auto_restart", ListValue, "auto_restart_day_time", translate("Restart time (every day)"))
for t = 0,23 do
o:value(t, t..":00")
end
o.default=0
---- Dashboard Settings
local cn_port=SYS.exec("uci get openclash.config.cn_port 2>/dev/null |tr -d '\n'")
o = s:taboption("dashboard", Value, "cn_port")
o.title = translate("Dashboard Port")
o.default = 9090
o.datatype = "port"
o.rmempty = false
o.description = translate("Dashboard Address Example:").." "..font_green..bold_on..lan_ip.."/luci-static/openclash、"..lan_ip..':'..cn_port..'/ui'..bold_off..font_off
o = s:taboption("dashboard", Value, "dashboard_password")
o.title = translate("Dashboard Secret")
o.rmempty = true
o.description = translate("Set Dashboard Secret")
o = s:taboption("dashboard", Value, "dashboard_forward_domain")
o.title = translate("Public Dashboard Address")
o.datatype = "or(host, string)"
o.placeholder = "example.com"
o.rmempty = true
o.description = translate("Domain Name For Dashboard Login From Public Network")
o = s:taboption("dashboard", Value, "dashboard_forward_port")
o.title = translate("Public Dashboard Port")
o.datatype = "port"
o.rmempty = true
o.description = translate("Port For Dashboard Login From Public Network")
---- version update
core_update = s:taboption("version_update", DummyValue, "", nil)
core_update.template = "openclash/update"
---- debug
o = s:taboption("debug", DummyValue, "", nil)
o.template = "openclash/debug"
---- dlercloud
o = s:taboption("dlercloud", Value, "dler_email")
o.title = translate("Account Email Address")
o.rmempty = true
o = s:taboption("dlercloud", Value, "dler_passwd")
o.title = translate("Account Password")
o.password = true
o.rmempty = true
if m.uci:get("openclash", "config", "dler_token") then
o = s:taboption("dlercloud", Flag, "dler_checkin")
o.title = translate("Checkin")
o.default=0
o.rmempty = true
end
o = s:taboption("dlercloud", Value, "dler_checkin_interval")
o.title = translate("Checkin Interval (hour)")
o:depends("dler_checkin", "1")
o.default=1
o.rmempty = true
o = s:taboption("dlercloud", Value, "dler_checkin_multiple")
o.title = translate("Checkin Multiple")
o.datatype = "uinteger"
o.default=1
o:depends("dler_checkin", "1")
o.rmempty = true
o.description = font_green..bold_on..translate("Multiple Must Be a Positive Integer and No More Than 50")..bold_off..font_off
function o.validate(self, value)
if tonumber(value) < 1 then
return "1"
end
if tonumber(value) > 50 then
return "50"
end
return value
end
o = s:taboption("dlercloud", DummyValue, "dler_login", translate("Account Login"))
o.template = "openclash/dler_login"
if m.uci:get("openclash", "config", "dler_token") then
o.value = font_green..bold_on..translate("Account logged in")..bold_off..font_off
else
o.value = font_red..bold_on..translate("Account not logged in")..bold_off..font_off
end
-- [[ Edit Server ]] --
s = m:section(TypedSection, "dns_servers", translate("Add Custom DNS Servers")..translate("(Take Effect After Choose Above)"))
s.anonymous = true
s.addremove = true
s.sortable = false
s.template = "cbi/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"), font_red..bold_on..translate("(Enable or Disable)")..bold_off..font_off)
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- group
o = s:option(ListValue, "group", translate("DNS Server Group"))
o.description = font_red..bold_on..translate("(NameServer Group Must Be Set)")..bold_off..font_off
o:value("nameserver", translate("NameServer"))
o:value("fallback", translate("FallBack"))
o.default = "nameserver"
o.rempty = false
---- IP address
o = s:option(Value, "ip", translate("DNS Server Address"))
o.description = font_red..bold_on..translate("(Do Not Add Type Ahead)")..bold_off..font_off
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o.rmempty = true
---- port
o = s:option(Value, "port", translate("DNS Server Port"))
o.description = font_red..bold_on..translate("(Require When Use Non-Standard Port)")..bold_off..font_off
o.datatype = "port"
o.rempty = true
---- type
o = s:option(ListValue, "type", translate("DNS Server Type"))
o.description = font_red..bold_on..translate("(Communication protocol)")..bold_off..font_off
o:value("udp", translate("UDP"))
o:value("tcp", translate("TCP"))
o:value("tls", translate("TLS"))
o:value("https", translate("HTTPS"))
o.default = "udp"
o.rempty = false
-- [[ Other Rules Manage ]]--
ss = m:section(TypedSection, "other_rules", translate("Other Rules Edit")..translate("(Take Effect After Choose Above)"))
ss.anonymous = true
ss.addremove = true
ss.sortable = true
ss.template = "cbi/tblsection"
ss.extedit = luci.dispatcher.build_url("admin/services/openclash/other-rules-edit/%s")
function ss.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(ss.extedit % sid)
return
end
end
o = ss:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = ss:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ss:option(DummyValue, "rule_name", translate("Other Rules Name"))
function o.cfgvalue(...)
if Value.cfgvalue(...) == "lhie1" then
return translate("lhie1 Rules")
elseif Value.cfgvalue(...) == "ConnersHua" then
return translate("ConnersHua(Provider-type) Rules")
elseif Value.cfgvalue(...) == "ConnersHua_return" then
return translate("ConnersHua Return Rules")
else
return translate("None")
end
end
o = ss:option(DummyValue, "Note", translate("Note"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Edit Authentication ]] --
s = m:section(TypedSection, "authentication", translate("Set Authentication of SOCKS5/HTTP(S)"))
s.anonymous = true
s.addremove = true
s.sortable = false
s.template = "cbi/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- username
o = s:option(Value, "username", translate("Username"))
o.placeholder = translate("Not Null")
o.rempty = true
---- password
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Not Null")
o.rmempty = true
if op_mode == "redir-host" then
s = m:section(NamedSection, "config", translate("Set Custom Hosts, Only Work with Redir-Host Mode"))
s.anonymous = true
custom_hosts = s:option(Value, "custom_hosts")
custom_hosts.template = "cbi/tvalue"
custom_hosts.description = translate("Custom Hosts Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://lancellc.gitbook.io/clash/clash-config-file/dns/host\")'>https://lancellc.gitbook.io/clash/clash-config-file/dns/host</a>"
custom_hosts.rows = 20
custom_hosts.wrap = "off"
function custom_hosts.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_hosts.list") or ""
end
function custom_hosts.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_hosts.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_hosts.list", value)
end
end
end
end
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/config_editor"))
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,264 @@
--[[
LuCI - Filesystem tools
Description:
A module offering often needed filesystem manipulation functions
FileId:
$Id$
License:
Copyright 2008 Steven Barth <steven@midlink.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--
local io = require "io"
local os = require "os"
local ltn12 = require "luci.ltn12"
local fs = require "nixio.fs"
local nutil = require "nixio.util"
local type = type
local string = string
--- LuCI filesystem library.
module "luci.openclash"
--- Test for file access permission on given path.
-- @class function
-- @name access
-- @param str String value containing the path
-- @return Number containing the return code, 0 on sucess or nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
access = fs.access
--- Evaluate given shell glob pattern and return a table containing all matching
-- file and directory entries.
-- @class function
-- @name glob
-- @param filename String containing the path of the file to read
-- @return Table containing file and directory entries or nil if no matches
-- @return String containing the error description (if no matches)
-- @return Number containing the os specific errno (if no matches)
function glob(...)
local iter, code, msg = fs.glob(...)
if iter then
return nutil.consume(iter)
else
return nil, code, msg
end
end
--- Checks wheather the given path exists and points to a regular file.
-- @param filename String containing the path of the file to test
-- @return Boolean indicating wheather given path points to regular file
function isfile(filename)
return fs.stat(filename, "type") == "reg"
end
--- Checks wheather the given path exists and points to a directory.
-- @param dirname String containing the path of the directory to test
-- @return Boolean indicating wheather given path points to directory
function isdirectory(dirname)
return fs.stat(dirname, "type") == "dir"
end
--- Read the whole content of the given file into memory.
-- @param filename String containing the path of the file to read
-- @return String containing the file contents or nil on error
-- @return String containing the error message on error
readfile = fs.readfile
--- Write the contents of given string to given file.
-- @param filename String containing the path of the file to read
-- @param data String containing the data to write
-- @return Boolean containing true on success or nil on error
-- @return String containing the error message on error
writefile = fs.writefile
--- Copies a file.
-- @param source Source file
-- @param dest Destination
-- @return Boolean containing true on success or nil on error
copy = fs.datacopy
--- Renames a file.
-- @param source Source file
-- @param dest Destination
-- @return Boolean containing true on success or nil on error
rename = fs.move
--- Get the last modification time of given file path in Unix epoch format.
-- @param path String containing the path of the file or directory to read
-- @return Number containing the epoch time or nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
function mtime(path)
return fs.stat(path, "mtime")
end
--- Set the last modification time of given file path in Unix epoch format.
-- @param path String containing the path of the file or directory to read
-- @param mtime Last modification timestamp
-- @param atime Last accessed timestamp
-- @return 0 in case of success nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
function utime(path, mtime, atime)
return fs.utimes(path, atime, mtime)
end
--- Return the last element - usually the filename - from the given path with
-- the directory component stripped.
-- @class function
-- @name basename
-- @param path String containing the path to strip
-- @return String containing the base name of given path
-- @see dirname
basename = fs.basename
--- Return the directory component of the given path with the last element
-- stripped of.
-- @class function
-- @name dirname
-- @param path String containing the path to strip
-- @return String containing the directory component of given path
-- @see basename
dirname = fs.dirname
--- Return a table containing all entries of the specified directory.
-- @class function
-- @name dir
-- @param path String containing the path of the directory to scan
-- @return Table containing file and directory entries or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function dir(...)
local iter, code, msg = fs.dir(...)
if iter then
local t = nutil.consume(iter)
t[#t+1] = "."
t[#t+1] = ".."
return t
else
return nil, code, msg
end
end
--- Create a new directory, recursively on demand.
-- @param path String with the name or path of the directory to create
-- @param recursive Create multiple directory levels (optional, default is true)
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function mkdir(path, recursive)
return recursive and fs.mkdirr(path) or fs.mkdir(path)
end
--- Remove the given empty directory.
-- @class function
-- @name rmdir
-- @param path String containing the path of the directory to remove
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
rmdir = fs.rmdir
local stat_tr = {
reg = "regular",
dir = "directory",
lnk = "link",
chr = "character device",
blk = "block device",
fifo = "fifo",
sock = "socket"
}
--- Get information about given file or directory.
-- @class function
-- @name stat
-- @param path String containing the path of the directory to query
-- @return Table containing file or directory properties or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function stat(path, key)
local data, code, msg = fs.stat(path)
if data then
data.mode = data.modestr
data.type = stat_tr[data.type] or "?"
end
return key and data and data[key] or data, code, msg
end
--- Set permissions on given file or directory.
-- @class function
-- @name chmod
-- @param path String containing the path of the directory
-- @param perm String containing the permissions to set ([ugoa][+-][rwx])
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
chmod = fs.chmod
--- Create a hard- or symlink from given file (or directory) to specified target
-- file (or directory) path.
-- @class function
-- @name link
-- @param path1 String containing the source path to link
-- @param path2 String containing the destination path for the link
-- @param symlink Boolean indicating wheather to create a symlink (optional)
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function link(src, dest, sym)
return sym and fs.symlink(src, dest) or fs.link(src, dest)
end
--- Remove the given file.
-- @class function
-- @name unlink
-- @param path String containing the path of the file to remove
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
unlink = fs.unlink
--- Retrieve target of given symlink.
-- @class function
-- @name readlink
-- @param path String containing the path of the symlink to read
-- @return String containing the link target or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
readlink = fs.readlink
function filename(str)
local idx = str:match(".+()%.%w+$")
if(idx) then
return str:sub(1, idx-1)
else
return str
end
end
function filesize(e)
local t=0
local a={' KB',' MB',' GB',' TB'}
repeat
e=e/1024
t=t+1
until(e<=1024)
return string.format("%.1f",e)..a[t]
end

View File

@ -0,0 +1,18 @@
<%+cbi/valueheader%>
<div style="text-align: center;">
<%
local val = self:cfgvalue(section)
if val == translate("Enable") or val == translate("Config Normal") or val == translate("Exist") then
%>
<div style="color: green; font-weight:bold;">
<%
else
%>
<div style="color: red; font-weight:bold;">
<%
end
write(pcdata(val))
%>
</div>
</div>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,207 @@
<style>
.CodeMirror {
text-align: left !important;
font-size: 15px;
line-height: 150%;
resize: both !important;
}
</style>
<link rel="stylesheet" href="/luci-static/resources/openclash/lib/codemirror.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/idea.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/fold/foldgutter.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/lint/lint.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/display/fullscreen.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/dialog/dialog.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/search/matchesonscrollbar.css">
<script src="/luci-static/resources/openclash/lib/codemirror.js"></script>
<script src="/luci-static/resources/openclash/mode/yaml/yaml.js"></script>
<script src="/luci-static/resources/openclash/mode/lua/lua.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldcode.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldgutter.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/indent-fold.js"></script>
<script src="/luci-static/resources/openclash/addon/edit/matchbrackets.js"></script>
<script src="/luci-static/resources/openclash/addon/selection/active-line.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/yaml-lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/js-yaml.min.js"></script>
<script src="/luci-static/resources/openclash/addon/display/fullscreen.js"></script>
<script src="/luci-static/resources/openclash/addon/display/autorefresh.js"></script>
<script src="/luci-static/resources/openclash/addon/dialog/dialog.js"></script>
<script src="/luci-static/resources/openclash/addon/search/searchcursor.js"></script>
<script src="/luci-static/resources/openclash/addon/search/search.js"></script>
<script src="/luci-static/resources/openclash/addon/scroll/annotatescrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/matchesonscrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/jump-to-line.js"></script>
<script type="text/javascript">//<![CDATA[
function editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "text/yaml",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (readOnly == "true") {
editor.setOption("readOnly","true");
}
if (wid && height) {
editor.setSize(wid, height);
}
}
function other_editor(id, readOnly)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
}
var myEditor_use = document.getElementById("cbid.table.1.user");
var myEditor_def = document.getElementById("cbid.table.1.default");
if (myEditor_use && myEditor_def) {
var myEditor_use_wid = document.getElementById("cbi-table-1-user").offsetWidth;
var myEditor_def_wid = document.getElementById("cbi-table-1-default").offsetWidth;
editor(myEditor_use, 'false', myEditor_use_wid, '700px');
editor(myEditor_def, 'true', myEditor_def_wid, '700px');
}
var myEditor_hosts = document.getElementById("cbid.openclash.config.custom_hosts");
var myEditor_fall_fil = document.getElementById("cbid.openclash.config.custom_fallback_fil");
var myEditor_name_pol = document.getElementById("cbid.openclash.config.custom_domain_dns_core");
var myEditor_name_cus_r1 = document.getElementById("cbid.openclash.config.custom_rules_2");
var myEditor_name_cus_r2 = document.getElementById("cbid.openclash.config.custom_rules");
var myEditor_fake_filter = document.getElementById("cbid.openclash.config.custom_fake_filter");
var myEditor_custom_domain_dns = document.getElementById("cbid.openclash.config.custom_domain_dns");
if (myEditor_hosts) {
editor(myEditor_hosts, 'false');
}
if (myEditor_fall_fil) {
editor(myEditor_fall_fil, 'false');
editor(myEditor_name_pol, 'false');
editor(myEditor_name_cus_r1, 'false');
editor(myEditor_name_cus_r2, 'false');
}
if (myEditor_fake_filter) {
other_editor(myEditor_fake_filter, 'false');
}
if (myEditor_custom_domain_dns) {
other_editor(myEditor_custom_domain_dns, 'false');
}
var core_log = document.getElementById("core_log");
var oc_log = document.getElementById("cbid.openclash.config.clog");
if (core_log && oc_log) {
var core_editor = CodeMirror.fromTextArea(core_log, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "idea",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
}
});
var oc_editor = CodeMirror.fromTextArea(oc_log, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "idea",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
}
});
core_editor.setSize("auto", "540px");
core_editor.setOption("readOnly","true");
oc_editor.setSize("auto", "540px");
oc_editor.setOption("readOnly","true");
}
var proxy_mg = document.getElementById('cbi-table-1-proxy_mg');
var rule_mg = document.getElementById('cbi-table-1-rule_mg');
var game_mg = document.getElementById('cbi-table-1-game_mg');
var Commit = document.getElementById('cbi-table-1-Commit');
var Apply = document.getElementById('cbi-table-1-Apply');
if (proxy_mg) {
proxy_mg.style.textAlign="center";
rule_mg.style.textAlign="center";
game_mg.style.textAlign="center";
Commit.style.textAlign="center";
Apply.style.textAlign="center";
}
//]]>
</script>

View File

@ -0,0 +1,139 @@
<%#
Copyright 2010 Jo-Philipp Wich <jow@openwrt.org>
Licensed to the public under the Apache License 2.0.
-%>
<%
local diag_host = "www.instagram.com"
%>
<script type="text/javascript">//<![CDATA[
function show_diag_info(addr)
{
var addr = addr;
var legend = document.getElementById('diag-rc-legend');
var output = document.getElementById('diag-rc-output');
if (legend && output)
{
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "diag_connection")%>', {addr: addr}, function(x, status) {
if (x && x.status == 200 && x.responseText != "")
{
legend.style.display = 'none';
output.innerHTML = String.format('<pre>%h</pre>', x.responseText);
}
else if (x.status == 500)
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:Bad address specified!%></span>';
}
else
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:Could not find any connection logs!%></br></br><%:1. It may be that the plugin is not running%></br></br><%:2. It may be that the cache causes the browser to directly use the IP for access%></br></br><%:3. It may be that DNS hijacking did not take effect, so clash unable to reverse the domain name%></br></br><%:4. It may be that the filled address cannot be resolved and connected%></span>';
}
});
}
}
function update_status(field)
{
var addr = field.value;
var legend = document.getElementById('diag-rc-legend');
var output = document.getElementById('diag-rc-output');
if (legend && output)
{
output.innerHTML =
'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
legend.parentNode.style.display = 'block';
legend.style.display = 'inline';
}
let HTTP = {
checker: (domain) => {
let img = new Image;
let timeout = setTimeout(() => {
img.onerror = img.onload = null;
show_diag_info(addr);
}, 10000);
img.onerror = () => {
clearTimeout(timeout);
show_diag_info(addr);
}
img.onload = () => {
clearTimeout(timeout);
show_diag_info(addr);
}
img.src = `https://${domain}/favicon.ico?${+(new Date)}`
},
runcheck: () => {
HTTP.checker(addr);
}
};
HTTP.runcheck();
}
function gen_debug_logs()
{
var legend = document.getElementById('debug-rc-legend');
var output = document.getElementById('debug-rc-output');
if (legend && output)
{
output.innerHTML =
'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
legend.parentNode.style.display = 'block';
legend.style.display = 'inline';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "gen_debug_logs")%>', null, function(x, status) {
if (x && x.status == 200 && x.responseText != "")
{
legend.style.display = 'none';
output.innerHTML = '<textarea class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="30" cols="60" readonly="readonly" >'+x.responseText+'</textarea>';
}
else
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:Some error occurred!%></span>';
}
}
);
}
}
//]]></script>
<form>
<fieldset>
<div style="width:50%; float: left; text-align: center;">
<%:Connection Test (Current Browser)%>&nbsp;&nbsp;&nbsp;&nbsp;
<input type="text" value="<%=diag_host%>" name="diag" />
<input type="button" value="<%:Click to Test%>" class="btn cbi-button cbi-button-apply" onclick="update_status(this.form.diag)" />
</div>
<div style="width:50%; float: left; text-align: center;">
<%:Generate Logs%>&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" value="<%:Click to Generate%>" class="btn cbi-button cbi-button-apply" onclick="gen_debug_logs(this)" />
</div>
</fieldset>
<fieldset style="display:none">
<legend id="diag-rc-legend"><%:Collecting data...%></legend>
<br />
<span id="diag-rc-output"></span>
</fieldset>
<fieldset style="display:none">
<legend id="debug-rc-legend"><%:Collecting data...%></legend>
<br />
<span id="debug-rc-output"></span>
</fieldset>
</form>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,105 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function dler_login(btn,option)
{
btn.disabled = true;
if (option == "dler_login") {
var s = document.getElementById(option+'-status');
var e = document.getElementsByName('cbid.openclash.config.dler_email');
var p = document.getElementsByName('cbid.openclash.config.dler_passwd');
var c = document.getElementsByName('cbid.openclash.config.dler_checkin');
if (!e[0].value || !p[0].value) {
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Error Login Info%>"+"</strong></font>";
return false;
};
if (c[0] && c[0].checked) {
c = "1";
var i = document.getElementsByName('cbid.openclash.config.dler_checkin_interval');
var m = document.getElementsByName('cbid.openclash.config.dler_checkin_multiple');
if (!i[0].value || !(/(^[1-9]\d*$)/.test(i[0].value))) { i = "1"} else {i = i[0].value};
if (!m[0].value || !(/(^[1-9]\d*$)/.test(m[0].value)))
{
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Multiple Must Be a Positive Integer and No More Than 50%>"+"</strong></font>";
return false;
}
else if (m[0].value < 1)
{
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Multiple Must Be a Positive Integer and No More Than 50%>"+"</strong></font>";
return false;
}
else if (m[0].value > 50)
{
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Multiple Must Be a Positive Integer and No More Than 50%>"+"</strong></font>";
return false;
}
else {
m = m[0].value;
};
}
else {
c = "0";
var i = "1";
var m = "1";
};
btn.value = '<%:Login...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dler_login_info_save")%>', {email: e[0].value, passwd : p[0].value, checkin: c, interval: i, multiple: m}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dler_login")%>', null, function(x, status) {
if (s)
{
if (x && x.status == 200 && status.dler_login == 200) {
s.innerHTML ="<font color='green'><strong>"+"<%:Dler Cloud Login Successful%>"+"</strong></font>";
window.location.href='<%="settings?tab.openclash.config=dlercloud"%>';
}
else {
s.innerHTML ="<font color='red'><strong>"+"<%:Dler Cloud Login Faild%>"+"</strong></font>";
window.location.href='<%="settings?tab.openclash.config=dlercloud"%>';
}
}
btn.disabled = false;
btn.value = '<%:Login Account%>';
});
}
});
};
if (option == "dler_logout") {
var s = document.getElementById('dler_login-status');
btn.value = '<%:Logout...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dler_logout")%>', null, function(x, status) {
if (s)
{
if (x && x.status == 200 && status.dler_logout == 200) {
s.innerHTML ="<font color='green'><strong>"+"<%:Dler Cloud Logout Successful%>"+"</strong></font>";
window.location.href='<%="settings?tab.openclash.config=dlercloud"%>';
}
else {
s.innerHTML ="<font color='red'><strong>"+"<%:Dler Cloud Logout Faild%>"+"</strong></font>";
}
}
btn.disabled = false;
btn.value = '<%:Logout Account%>';
}
);
};
return false;
}
function web_dler(btn)
{
btn.disabled = true;
url='https://bit.ly/32mrABp';
window.open(url);
btn.disabled = false;
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Login Account%>" onclick="return dler_login(this,'dler_login')" />
<input type="button" class="btn cbi-button cbi-button-remove" value="<%:Logout Account%>" onclick="return dler_login(this,'dler_logout')" />
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Official Website%>" onclick="return web_dler(this)" />
<span id="<%=self.option%>-status"><%=self.value%></span>
<%+cbi/valuefooter%>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function act_download_rule(btn,filename)
{
btn.disabled = true;
btn.value = '<%:Downloading Rule...%> ';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","download_rule")%>',
{
filename: filename
},
function(x,status)
{
if ( x && x.status == 200 ) {
if(status.rule_download_status=="0")
{
btn.value = '<%:Downloading Fail%>';
}
else if (status.rule_download_status=="1")
{
btn.value = '<%:Downloading Successful%>';
}
else if (status.rule_download_status=="2")
{
btn.value = '<%:Rule No Change%>';
}
}
else {
btn.value = '<%:Downloading Timeout%>';
}
}
);
btn.disabled = false;
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Click to Update%>" onclick="return act_download_rule(this,'<%=self:cfgvalue(section)%>')" />
<%+cbi/valuefooter%>

View File

@ -0,0 +1,152 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
var catch_num;
var catch_timeout;
var catch_out;
function act_download_disney_rule(btn)
{
btn.disabled = true;
btn.value = '<%:Downloading Rule...%> ';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","download_disney_domains")%>',
null,
function(x,status)
{
if ( x && x.status == 200 ) {
if(status.rule_download_status=="0")
{
btn.value = '<%:Downloading Fail%>';
}
else if (status.rule_download_status=="1")
{
btn.value = '<%:Downloading Successful%>';
}
else if (status.rule_download_status=="2")
{
btn.value = '<%:Rule No Change%>';
}
}
else {
btn.value = '<%:Downloading Timeout%>';
}
}
);
btn.disabled = false;
return false;
};
function act_download_netflix_rule(btn)
{
btn.disabled = true;
btn.value = '<%:Downloading Rule...%> ';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","download_netflix_domains")%>',
null,
function(x,status)
{
if ( x && x.status == 200 ) {
if(status.rule_download_status=="0")
{
btn.value = '<%:Downloading Fail%>';
}
else if (status.rule_download_status=="1")
{
btn.value = '<%:Downloading Successful%>';
}
else if (status.rule_download_status=="2")
{
btn.value = '<%:Rule No Change%>';
}
}
else {
btn.value = '<%:Downloading Timeout%>';
}
}
);
btn.disabled = false;
return false;
};
function catch_netflix_domains()
{
var legend = document.getElementById('catch-netflix-state');
var output = document.getElementById('catch-netflix-output');
var r = confirm("<%:Attention:%>\n<%:The catch result will be automatically saved%>\n\n1. <%:Please make sure the OpenClash works normally%>\n2. <%:The domains catch time is one minute%>\n3. <%:About to open fast.com%>\n4. <%:You can also try to catch while unlocking device playing%>");
if (r == true) {
winOpen("https://fast.com/");
if (legend && output)
{
output.innerHTML =
'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
legend.parentNode.style.display = 'block';
legend.style.display = 'inline';
catch_num = 0;
catch_out = "";
get_netflix_domains();
}
}
};
function strUnique(str){
var ret = [];
str.replace(/[^,]+/g, function($1, $2) {
(str.indexOf($1) == $2) && ret.push($1);
});
return ret.join('\n');
}
function get_netflix_domains()
{
var legend = document.getElementById('catch-netflix-state');
var output = document.getElementById('catch-netflix-output');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "catch_netflix_domains")%>', null, function(x, status) {
if (x && x.status == 200 && x.responseText != "")
{
if (catch_out != "") {
catch_out = catch_out + x.responseText;
}
else
{
catch_out = x.responseText;
}
}
}
);
catch_num = catch_num + 1;
if ( catch_num < 20 ) {
catch_timeout = setTimeout("get_netflix_domains()", 3000);
}
else {
clearTimeout(catch_timeout);
if (catch_out != "")
{
legend.style.display = 'none';
output.innerHTML = '<textarea class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="10" cols="50" readonly="readonly" >'+strUnique(catch_out)+'</textarea>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "write_netflix_domains")%>', {domains: strUnique(catch_out)}, function(x, status) {});
}
else
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:No domain names were catched...%></span>';
}
}
};
//]]></script>
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Netflix%>" onclick="return act_download_netflix_rule(this)" />
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Disney Plus%>" onclick="return act_download_disney_rule(this)" />
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Catch Netflix%>" onclick="return catch_netflix_domains(this)" />
<fieldset style="display: none;margin: 0 auto;">
<legend id="catch-netflix-state"><%:Collecting data...%></legend>
<br />
<span id="catch-netflix-output"></span>
</fieldset>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,13 @@
<%+cbi/valueheader%>
<div style="text-align: center; margin:0 auto; display: block; width: 100%; height: 50px; text-overflow: ellipsis;">
<div>
<%:Note: Please Upload File According To File Type, File Will Be Saved To The Prompt Path%>
</div>
<div style="color: green; transform:translateY(100%);">
<%
local val = self:cfgvalue(section) or self.default or ""
write(pcdata(val))
%>
</div>
</div>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,308 @@
<%+cbi/valueheader%>
<style type="text/css">
*{margin: 0;padding: 0;}
ul{
list-style: none;
}
#tab{
width: 100%;
height: 100%;
border: 1px solid #ddd;
box-shadow: 0 0 2px #ddd;
overflow: hidden;
}
#tab-header{
background-color: #F7F7F7;
height: 33px;
text-align: center;
position: relative;
}
#tab-header ul{
width: 500px;
position: absolute;
left: -1px;
}
#tab-header ul li{
float: left;
width: 120px;
height: 33px;
line-height: 33px;
padding: 0 1px;
border-bottom: 1px solid #dddddd;
border-right: 1px solid #dddddd;
}
#tab-header ul li.selected{
background-color: white;
font-weight: bolder;
border-bottom: 0;
border-right: 1px solid #dddddd;
padding: 0;
}
#tab-header ul li:hover{
color: orangered;
}
#tab-content .dom{
display: none;
}
#tab-content .dom ul li{
float: left;
margin: 15px 10px;
width: 225px;
}
.radio-button{
width: fit-content;
text-align: center;
overflow: auto;
margin: 10px auto;
background-color: #d1d1d1;
border-radius: 4px;
}
.radio-button input[type="radio"] {
display: none;
}
.radio-button label {
display: inline-block;
padding: 4px 11px;
font-size: 18px;
color: white;
cursor: pointer;
border-radius: 4px;
}
.radio-button input[type="radio"]:checked+label {
background-color: #1080c1;
}
</style>
<body>
<div id="tab">
<div id="tab-header">
<ul>
<li name="tab-header" class="selected"><%:OpenClash Log%></li>
<li name="tab-header"><%:Core Log%></li>
</ul>
</div>
<div id="tab-content">
<div class="dom" style="display: block;">
<textarea id="cbid.openclash.config.clog" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="32" cols="60" readonly="readonly" ></textarea>
</div>
<div class="dom">
<textarea id="core_log" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="32" cols="60" readonly="readonly" ></textarea>
<div class="radio-button">
<input type="radio" id="info" name="radios" value="info" checked onclick="return switch_log_level(this.value)"/>
<label for="info">Info</label>
<input type="radio" id="warning" name="radios" value="warning" onclick="return switch_log_level(this.value)"/>
<label for="warning">Warning</label>
<input type="radio" id="error" name="radios" value="error" onclick="return switch_log_level(this.value)"/>
<label for="error">Error</label>
<input type="radio" id="debug" name="radios" value="debug" onclick="return switch_log_level(this.value)"/>
<label for="debug">Debug</label>
<input type="radio" id="silent" name="radios" value="silent" onclick="return switch_log_level(this.value)"/>
<label for="silent">Silent</label>
</div>
</div>
</div>
</div>
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td style="text-align: center; width: 25%">
<input type="button" class="btn cbi-button cbi-button-apply" id="stop_refresh_button" value="<%:Stop Refresh Log%>" onclick=" return stop_refresh() "/>
</td>
<td style="text-align: center; width: 25%">
<input type="button" class="btn cbi-button cbi-button-apply" id="start_refresh_button" value="<%:Start Refresh Log%>" onclick=" return start_refresh() "/>
</td>
<td style="text-align: center; width: 25%">
<input type="button" class="btn cbi-button cbi-button-apply" id="del_log_button" value="<%:Clean Log%>" style=" display:inline;" onclick=" return del_log() " />
</td>
<td style="text-align: center; width: 25%">
<input type="button" class="btn cbi-button cbi-button-apply" id="down_log_button" value="<%:Download Log%>" style=" display:inline;" onclick=" return download_log() " />
</td>
</tr>
</table>
</fieldset>
</body>
<script type="text/javascript">//<![CDATA[
var r;
var s;
var log_len = 0;
var lv = document.getElementById('cbid.openclash.config.clog');
var cl = document.getElementById('core_log');
document.getElementById('stop_refresh_button').style.textAlign="center";
document.getElementById('start_refresh_button').style.textAlign="center";
document.getElementById('del_log_button').style.textAlign="center";
document.getElementById('down_log_button').style.textAlign="center";
function get_log_level() {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "log_level")%>', null, function(x, status) {
if (x && x.status == 200 && status.log_level != "") {
var radio = document.getElementsByName("radios");
for (i=0; i<radio.length; i++) {
if (radio[i].value == status.log_level && ! radio[i].checked) {
radio[i].checked = true;
}
}
}
});
s=setTimeout("get_log_level()",5000);
};
function switch_log_level(value)
{
clearTimeout(s);
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_log")%>', {log_level: value}, function(x, status) {
if (x && x.status == 200) {
alert(' <%:Log Level%>: ' + value + ' <%:switching succeeded!%>');
get_log_level();
}
else {
alert(' <%:Log Level%>: ' + value + ' <%:switching failed!%>');
get_log_level();
}
});
};
function stop_refresh() {
clearTimeout(r);
return
};
function start_refresh() {
clearTimeout(r);
r=setTimeout("poll_log()",1000*2);
return
};
function createAndDownloadFile(fileName, content) {
var aTag = document.createElement('a');
var blob = new Blob([content]);
aTag.download = fileName;
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(blob);
};
function download_log(){
var dt = new Date();
var timestamp = dt.getFullYear()+"-"+(dt.getMonth()+1)+"-"+dt.getDate()+"-"+dt.getHours()+"-"+dt.getMinutes()+"-"+dt.getSeconds();
createAndDownloadFile("OpenClash-"+timestamp+".log","<%:OpenClash Log%>:\n"+lv.innerHTML+"\n<%:Core Log%>:\n"+cl.innerHTML)
return
};
function del_log() {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "del_log")%>',null,function(x, data){
lv.innerHTML="";
cl.innerHTML="";
log_len = 0;
oc_editor.setValue(lv.value);
core_editor.setValue(cl.value);
core_editor.refresh();
oc_editor.refresh();
});
return
};
function p(s) {
return s < 10 ? '0' + s: s;
};
function line_tolocal(str){
var strt=new Array();
var cstrt=new Array();
var cn = 0;
var sn = 0;
str.trim().split('\n').forEach(function(v, i) {
var regex = /"([^"]*)"/g;
var res = regex.exec(v);
if (res) {
var dt = new Date(res[1].match(/\+08\:00/)? res[1].replace("+08:00", "Z") : res[1]);
}
if (dt && dt != "Invalid Date"){
cstrt[cn]=dt.getFullYear()+"-"+p(dt.getMonth()+1)+"-"+p(dt.getDate())+" "+p(dt.getHours())+":"+p(dt.getMinutes())+":"+p(dt.getSeconds())+v.substring(res[1].length + 7);
cn = cn + 1;
}
else{
strt[sn]=v;
sn = sn + 1;
}
})
return [strt,cstrt]
};
function poll_log(){
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "refresh_log")%>', {log_len: log_len},
function(x, status) {
if ( x && x.status == 200 ) {
if (status && status.log != "" && lv && cl) {
var log = line_tolocal(status.log);
var lines = log[0];
var clines = log[1];
if (lines != "" || clines != "") {
if (lines != "") {
lv.innerHTML = lines.join('\n')+ (log_len != 0 ? '\n' : '') + lv.innerHTML;
oc_editor.setValue(lv.value);
oc_editor.refresh();
}
if (clines != "") {
if (lines[0] != "..." && lines[lines.length-1] != "...") {
cl.innerHTML = clines.join('\n') + (log_len != 0 ? '\n' : '') + cl.innerHTML;
}
else {
cl.innerHTML = clines.join('\n') + (log_len != 0 ? '\n' : cl.innerHTML + '\n...');
}
core_editor.setValue(cl.value);
core_editor.refresh();
}
log_len = status.len;
//lv.innerHTML = x.responseText.split('\n').reverse().join('\n')+lv.innerHTML;
}
}
}
}
);
r=setTimeout("poll_log()",1000*2);
};
window.onload = function(){
var titles = document.getElementsByName('tab-header');
var divs = document.getElementsByClassName('dom');
if(titles.length != divs.length) return;
for(var i=0; i<titles.length; i++){
var li = titles[i];
li.id = i;
li.onclick = function(){
for(var j=0; j<titles.length; j++){
titles[j].className = '';
divs[j].style.display = 'none';
}
this.className = 'selected';
divs[this.id].style.display = 'block';
}
li.onTouchStart = function(){
for(var j=0; j<titles.length; j++){
titles[j].className = '';
divs[j].style.display = 'none';
}
this.className = 'selected';
divs[this.id].style.display = 'block';
}
}
get_log_level();
poll_log();
};
//]]>
</script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,341 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//cdn.jsdelivr.net">
<link rel="dns-prefetch" href="//whois.pconline.com.cn">
<link rel="dns-prefetch" href="//api-ipv4.ip.sb">
<link rel="dns-prefetch" href="//myip.ipip.net">
<link rel="dns-prefetch" href="//api.ipify.org">
<link rel="dns-prefetch" href="//api.ttt.sh">
<link rel="dns-prefetch" href="//api.skk.moe">
<link rel="dns-prefetch" href="//d.skk.moe">
<link rel="preconnect" href="https://whois.pconline.com.cn">
<link rel="preconnect" href="https://api-ipv4.ip.sb">
<link rel="preconnect" href="https://myip.ipip.net">
<link rel="preconnect" href="http://myip.ipip.net">
<link rel="preconnect" href="https://api.ipify.org">
<link rel="preconnect" href="https://api.ttt.sh">
<link rel="preconnect" href="https://api.skk.moe">
<link rel="preconnect" href="https://d.skk.moe">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,minimal-ui">
<title>IP 地址查询</title>
<style>
.ip-title {
font-weight: bold;
color: #444;
font-size:15px;
display: inline-block;
width: 25%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align:bottom;
}
.ip-state_title {
font-weight: bold;
color: #444;
font-size:15px;
display: inline-block;
width: 52%;
vertical-align:bottom;
overflow: hidden;
text-overflow: ellipsis;
transform:translateY(5%);
}
.ip-result {
color: #444;
font-size:16px;
margin:0px 0px 0px 30px;
white-space: nowrap; /*强制span不换行*/
display: inline-block; /*将span当做块级元素对待*/
width: 29%; /*限制宽度*/
overflow: hidden; /*超出宽度部分隐藏*/
text-overflow: ellipsis; /*超出部分以点号代替*/
vertical-align:bottom;
transform:translateY(8%);
}
.ip-geo {
color: #444;
font-size:15px;
line-height:20px;
white-space: nowrap; /*强制span不换行*/
display: inline-block; /*将span当做块级元素对待*/
width: 30%; /*限制宽度*/
overflow: hidden; /*超出宽度部分隐藏*/
text-overflow: ellipsis; /*超出部分以点号代替*/
vertical-align:bottom;
transform:translateY(15%);
}
.ip-checking {
color: #444;
font-size:15px;
line-height:20px;
display: inline-block;
vertical-align:bottom;
width: 29%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transform:translateY(12%);
}
.sk-text-success {
color: #32b643;
font-size:15px;
line-height:20px;
display: inline-block;
vertical-align:bottom;
width: 48%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
transform:translateY(12%);
}
.sk-text-error {
color: #e85600;
font-size:15px;
line-height:20px;
display: inline-block;
vertical-align:bottom;
width: 48%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
transform:translateY(12%);
}
h3 {
margin: 5px 0 6px;
}
p {
margin: 10px 0;
}
a {
text-decoration: none;
color: #666;
}
</style>
</head>
<body>
<fieldset class="cbi-section">
<table width="100%">
<tr><td>
<div style="display: flex;">
<div style="width: 51%">
<h3><%:IP Address%></h3>
<p>
<span class="ip-title">IPIP&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<%:Mainland%>:</span><span class="ip-result" id="ip-ipipnet"></span> <span class="ip-geo" id="ip-ipipnet-geo"></span>
</p>
<p>
<span class="ip-title">IP.PC&nbsp;&nbsp;&nbsp;&nbsp;<%:Mainland%>:</span><span class="ip-result" id="ip-pcol"></span> <span class="ip-geo" id="ip-pcol-ipip"></span>
</p>
<p>
<span class="ip-title">IP.SB&nbsp;&nbsp;&nbsp;&nbsp;<%:Abroad%>:</span><span class="ip-result" id="ip-ipsb"></span> <span class="ip-geo" id="ip-ipsb-geo"></span>
</p>
<p>
<span class="ip-title">IPIFY&nbsp;&nbsp;&nbsp;&nbsp;<%:Abroad%>:</span><span class="ip-result" id="ip-ipify"></span> <span class="ip-geo" id="ip-ipify-ipip"></span>
</p>
</div>
<div style="width: 49%">
<h3><%:Website Access Check%></h3>
<p>
<span class="ip-state_title"><%:Baidu Search%>:</span><span id="http-baidu"></span>
</p>
<p>
<span class="ip-state_title"><%:NetEase Music%>:</span><span id="http-163"></span>
</p>
<p>
<span class="ip-state_title">GitHub:</span><span id="http-github"></span>
</p>
<p>
<span class="ip-state_title">YouTube:</span><span id="http-youtube"></span>
</p>
</div>
</div>
<div>
<p style="float: right; margin-top: 30px; font-size:15px; padding-right: 10px">Powered by <a onclick="return ip_skk()" href="javascript:void(0);">ip.skk.moe</a></p>
</div>
</td></tr>
</table>
</fieldset>
</body>
<script>
function ip_skk()
{
url2='https://ip.skk.moe';
window.open(url2);
}
const $$ = document;
$$.getElementById('ip-pcol').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipify').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipipnet').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipsb').innerHTML = '<%:Querying...%>';
let random = parseInt(Math.random() * 100000000);
let IP = {
get: (url, type) =>
fetch(url, { method: 'GET' }).then((resp) => {
if (type === 'text')
return Promise.all([resp.ok, resp.status, resp.text(), resp.headers]);
else {
return Promise.all([resp.ok, resp.status, resp.json(), resp.headers]);
}
}).then(([ok, status, data, headers]) => {
if (ok) {
let json = {
ok,
status,
data,
headers
}
return json;
} else {
throw new Error(JSON.stringify(json.error));
}
}).catch(error => {
throw error;
}),
parseIPIpip: (ip, elID) => {
var meta = document.createElement('meta');
meta.name = "referrer";
meta.content = "no-referrer-when-downgrade";
document.getElementsByTagName('head')[0].appendChild(meta);
IP.get(`https://qqwry.api.skk.moe/${ip}`, 'json')
.then(resp => {
$$.getElementById(elID).innerHTML = resp.data.geo;
//$$.getElementById(elID).innerHTML = `${resp.data.country} ${resp.data.regionName} ${resp.data.city} ${resp.data.isp}`;
})
var meta = document.createElement('meta');
meta.name = "referrer";
meta.content = "no-referrer";
document.getElementsByTagName('head')[0].appendChild(meta);
},
getIpipnetIP: () => {
IP.get(window.location.protocol+`//myip.ipip.net/?z=${random}`, 'text')
.then((resp) => {
let data = resp.data.replace('当前 IP', '').split(' 来自于:');
$$.getElementById('ip-ipipnet').innerHTML = `${data[0]}`;
$$.getElementById('ip-ipipnet-geo').innerHTML = `${data[1]}`;
});
},
getIPApiIP: () => {
IP.get(`https://ipapi.co/json?z=${random}`, 'json')
.then(resp => {
$$.getElementById('ip-ipapi').innerHTML = resp.data.ip;
IP.parseIPIpip(resp.data.ip, 'ip-ipapi-geo');
})
},
getIpifyIP: () => {
IP.get(`https://api.ipify.org/?format=json&z=${random}`, 'json')
.then(resp => {
$$.getElementById('ip-ipify').innerHTML = resp.data.ip;
return resp.data.ip;
})
.then(ip => {
IP.parseIPIpip(ip, 'ip-ipify-ipip');
})
}
};
$$.getElementById('http-baidu').innerHTML = '<span class="ip-checking"><%:Testing...%></span>';
$$.getElementById('http-163').innerHTML = '<span class="ip-checking"><%:Testing...%></span>';
$$.getElementById('http-github').innerHTML = '<span class="ip-checking"><%:Testing...%></span>';
$$.getElementById('http-youtube').innerHTML = '<span class="ip-checking"><%:Testing...%></span>';
let HTTP = {
checker: (domain, cbElID) => {
let img = new Image;
let timeout = setTimeout(() => {
img.onerror = img.onload = null;
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Timed Out%></span>'
}, 5000);
img.onerror = () => {
clearTimeout(timeout);
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Denied%></span>'
}
img.onload = () => {
clearTimeout(timeout);
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-success"><%:Access Normal%></span>'
}
img.src = `https://${domain}/favicon.ico?${+(new Date)}`
},
runcheck: () => {
HTTP.checker('www.baidu.com', 'http-baidu');
HTTP.checker('s1.music.126.net/style', 'http-163');
HTTP.checker('github.com', 'http-github');
HTTP.checker('www.youtube.com', 'http-youtube');
}
};
HTTP.runcheck();
IP.getIpipnetIP();
IP.getIpifyIP();
function getPcolIP(data){
let pcisp = data.addr.split(' ');
$$.getElementById('ip-pcol').innerHTML = data.ip;
$$.getElementById('ip-pcol-ipip').innerHTML = `${data.pro} ${data.city} ${data.region} ${pcisp[1]}`;
};
function getIpsbIP(data){
$$.getElementById('ip-ipsb').innerHTML = data.ip;
IP.parseIPIpip(data.ip, 'ip-ipsb-geo');
};
window.onload=myip_Load();
function myip_Load()
{
var pcip = document.getElementsByTagName('HEAD').item(0);
var pcipScript= document.createElement("script");
pcipScript.defer = "defer";
pcipScript.src='https://whois.pconline.com.cn/ipJson.jsp?callback=getPcolIP';
pcip.appendChild(pcipScript);
var sbip = document.getElementsByTagName('HEAD').item(0);
var sbipScript= document.createElement("script");
sbipScript.defer = "defer";
sbipScript.src='https://api-ipv4.ip.sb/jsonip?callback=getIpsbIP';
sbip.appendChild(sbipScript);
const $$ = document;
random = parseInt(Math.random() * 100000000);
HTTP.runcheck();
IP.getIpipnetIP();
IP.getIpifyIP();
function getPcolIP(data){
let pcisp = data.addr.split(' ');
$$.getElementById('ip-pcol').innerHTML = data.ip;
$$.getElementById('ip-pcol-ipip').innerHTML = `${data.pro} ${data.city} ${data.region} ${pcisp[1]}`;
};
function getIpsbIP(data){
$$.getElementById('ip-ipsb').innerHTML = data.ip;
IP.parseIPIpip(data.ip, 'ip-ipsb-geo');
};
setTimeout("myip_Load()",1000*10);
}
</script>
<script defer="defer" src="https://whois.pconline.com.cn/ipJson.jsp?callback=getPcolIP"></script>
<script defer="defer" src="https://api-ipv4.ip.sb/jsonip?callback=getIpsbIP"></script>
</html>

View File

@ -0,0 +1,7 @@
<%+cbi/valueheader%>
<% if self:cfgvalue(section) ~= false then %>
<input class="btn cbi-button cbi-input-<%=self.inputstyle or "button" %>" style="display: <%= display %>" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
<% else %>
-
<% end %>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,3 @@
<%+cbi/valueheader%>
<span class="pingtime" hint="<%=self:cfgvalue(section)%>">-- ms</span>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,32 @@
<%#
Copyright 2018-2019 Lienol <lawlienol@gmail.com>
Licensed to the public under the Apache License 2.0.
-%>
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">
//<![CDATA[
var pings = document.getElementsByClassName('pingtime');
for(var i = 0; i < pings.length; i++) {
XHR.get('<%=dsp.build_url("admin", "services", "openclash", "ping")%>', {
index: i,
domain: pings[i].getAttribute("hint")
},
function(x, result) {
pings[result.index].innerHTML = (result.ping ? "<b style=color:green>"+result.ping+"</b> ms" : "<b style=color:red><%:Test failed%></b>");
}
);
XHR.poll(10,'<%=dsp.build_url("admin", "services", "openclash", "ping")%>',{
index: i,
domain: pings[i].getAttribute("hint")
},
function(x, result) {
pings[result.index].innerHTML = (result.ping ? "<b style=color:green>"+result.ping+"</b> ms" : "<b style=color:red><%:Test failed%></b>");
}
);
}
//]]>
</script>

View File

@ -0,0 +1,316 @@
<%+cbi/valueheader%>
<script type="text/javascript">
//<![CDATA[
function padright(str, cnt, pad) {
return str + Array(cnt + 1).join(pad);
}
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function b64encutf8safe(str) {
return b64EncodeUnicode(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, '');
}
function b64DecodeUnicode(str) {
return decodeURIComponent(Array.prototype.map.call(atob(str), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
function b64decutf8safe(str) {
var l;
str = str.replace(/-/g, "+").replace(/_/g, "/");
l = str.length;
l = (4 - l % 4) % 4;
if (l) str = padright(str, l, "=");
return b64DecodeUnicode(str);
}
function b64encsafe(str) {
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, '')
}
function b64decsafe(str) {
var l;
str = str.replace(/-/g, "+").replace(/_/g, "/");
l = str.length;
l = (4 - l % 4) % 4;
if (l) str = padright(str, l, "=");
return atob(str);
}
function dictvalue(d, key) {
var v = d[key];
if (typeof (v) == 'undefined' || v == '') return '';
return b64decsafe(v);
}
function export_ssr_url(btn, urlname, sid) {
var s = document.getElementById(urlname + '-status');
if (!s) return false;
var v_server = document.getElementsByName('cbid.openclash.' + sid + '.server')[0];
var v_port = document.getElementsByName('cbid.openclash.' + sid + '.port')[0];
var v_protocol = document.getElementsByName('cbid.openclash.' + sid + '.protocol')[0];
var v_method = document.getElementsByName('cbid.openclash.' + sid + '.cipher_ssr')[0];
var v_obfs = document.getElementsByName('cbid.openclash.' + sid + '.obfs_ssr')[0];
var v_password = document.getElementsByName('cbid.openclash.' + sid + '.password')[0];
var v_obfs_param = document.getElementsByName('cbid.openclash.' + sid + '.obfs_param')[0];
var v_protocol_param = document.getElementsByName('cbid.openclash.' + sid + '.protocol_param')[0];
var v_alias = document.getElementsByName('cbid.openclash.' + sid + '.name')[0];
var ssr_str = v_server.value + ":" + v_port.value + ":" + v_protocol.value + ":" + v_method.value + ":" + v_obfs.value + ":" + b64encsafe(v_password.value) + "/?obfsparam=" + b64encsafe(v_obfs_param.value) + "&protoparam=" + b64encsafe(v_protocol_param.value) + "&remarks=" + b64encutf8safe(v_alias.value);
var textarea = document.createElement("textarea");
textarea.textContent = "ssr://" + b64encsafe(ssr_str);
textarea.style.position = "fixed";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy"); // Security exception may be thrown by some browsers.
s.innerHTML = "<font style=\"color:green\"><%:Copy SSR to clipboard successfully.%></font>";
} catch (ex) {
s.innerHTML = "<font style=\"color:red\"><%:Unable to copy SSR to clipboard.%></font>";
} finally {
document.body.removeChild(textarea);
}
return false;
}
function import_ssr_url(btn, urlname, sid) {
var s = document.getElementById(urlname + '-status');
if (!s) return false;
var ssrurl = prompt("<%:Paste sharing link here%>", "");
if (ssrurl == null || ssrurl == "") {
s.innerHTML = "<font style=\"color:red\"><%:User cancelled.%></font>";
return false;
}
s.innerHTML = "";
//var ssu = ssrurl.match(/ssr:\/\/([A-Za-z0-9_-]+)/i);
var ssu = ssrurl.split('://');
//console.log(ssu.length);
var event = document.createEvent("HTMLEvents");
event.initEvent("change", true, true);
switch (ssu[0]) {
case "ss":
var url0, param = "";
var sipIndex = ssu[1].indexOf("@");
var ploc = ssu[1].indexOf("#");
if (ploc > 0) {
url0 = ssu[1].substr(0, ploc);
param = ssu[1].substr(ploc + 1);
} else {
url0 = ssu[1];
}
if (sipIndex != -1) {
// SIP002
var userInfo = b64decsafe(url0.substr(0, sipIndex));
var temp = url0.substr(sipIndex + 1).split("/?");
var serverInfo = temp[0].split(":");
var server = serverInfo[0];
var port = serverInfo[1];
var method, password, plugin, pluginOpts, pluginObfs, pluginObfsHost, pluginObfsPath, pluginObfsHeaders;
if (temp[1]) {
var pluginInfo = decodeURIComponent(temp[1]);
var pluginIndex = pluginInfo.indexOf(";");
var pluginNameInfo = pluginInfo.substr(0, pluginIndex);
plugin = pluginNameInfo.substr(pluginNameInfo.indexOf("=") + 1);
pluginOpts = pluginInfo.substr(pluginIndex + 1);
if (pluginOpts.indexOf("obfs=") != -1) {
pluginObfs = pluginOpts.split("obfs=")[1].split(";")[0];
if (pluginObfs == "ws") {
pluginObfs = "websocket"
}
if (pluginOpts.indexOf("obfs-host=") != -1) {
pluginObfsHost = pluginOpts.split("obfs-host=")[1].split("&group=")[0] || pluginOpts.split("obfs-host=")[1].split(";")[0];
}
if (pluginOpts.indexOf("path=") != -1) {
pluginObfsPath = pluginOpts.split("path=")[1].split(";")[0];
}
if (pluginOpts.indexOf("headers=") != -1) {
pluginObfsHeaders = pluginOpts.split("headers=")[1].split(";")[0];
}
}
}
var userInfoSplitIndex = userInfo.indexOf(":");
if (userInfoSplitIndex != -1) {
method = userInfo.substr(0, userInfoSplitIndex);
password = userInfo.substr(userInfoSplitIndex + 1);
}
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].value = ssu[0];
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.openclash.' + sid + '.server')[0].value = server;
document.getElementsByName('cbid.openclash.' + sid + '.port')[0].value = port;
document.getElementsByName('cbid.openclash.' + sid + '.password')[0].value = password || "";
document.getElementsByName('cbid.openclash.' + sid + '.cipher')[0].value = method || "";
document.getElementsByName('cbid.openclash.' + sid + '.obfs')[0].value = pluginObfs || "none";
document.getElementsByName('cbid.openclash.' + sid + '.obfs')[0].dispatchEvent(event);
if (plugin != undefined) {
document.getElementsByName('cbid.openclash.' + sid + '.host')[0].value = pluginObfsHost || "";
if (pluginObfs == "websocket") {
document.getElementsByName('cbid.openclash.' + sid + '.custom')[0].value = pluginObfsHeaders || "";
document.getElementsByName('cbid.openclash.' + sid + '.path')[0].value = pluginObfsPath || "";
}
}
if (param != undefined) {
document.getElementsByName('cbid.openclash.' + sid + '.name')[0].value = decodeURI(param);
}
s.innerHTML = "<font style=\"color:green\"><%:Import configuration information successfully.%></font>";
} else {
var sstr = b64decsafe(url0);
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].value = ssu[0];
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].dispatchEvent(event);
var team = sstr.split('@');
var part1 = team[0].split(':');
var part2 = team[1].split(':');
document.getElementsByName('cbid.openclash.' + sid + '.server')[0].value = part2[0];
document.getElementsByName('cbid.openclash.' + sid + '.port')[0].value = part2[1];
document.getElementsByName('cbid.openclash.' + sid + '.password')[0].value = part1[1];
document.getElementsByName('cbid.openclash.' + sid + '.cipher')[0].value = part1[0];
if (param != undefined) {
document.getElementsByName('cbid.openclash.' + sid + '.name')[0].value = decodeURI(param);
}
s.innerHTML = "<font style=\"color:green\"><%:Import configuration information successfully.%></font>";
}
return false;
case "ssr":
var sstr = b64decsafe(ssu[1]);
var ploc = sstr.indexOf("/?");
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].value = ssu[0];
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].dispatchEvent(event);
var url0, param = "";
if (ploc > 0) {
url0 = sstr.substr(0, ploc);
param = sstr.substr(ploc + 2);
}
var ssm = url0.match(/^(.+):([^:]+):([^:]*):([^:]+):([^:]*):([^:]+)/);
if (!ssm || ssm.length < 7) return false;
var pdict = {};
if (param.length > 2) {
var a = param.split('&');
for (var i = 0; i < a.length; i++) {
var b = a[i].split('=');
pdict[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || '');
}
}
document.getElementsByName('cbid.openclash.' + sid + '.server')[0].value = ssm[1];
document.getElementsByName('cbid.openclash.' + sid + '.port')[0].value = ssm[2];
document.getElementsByName('cbid.openclash.' + sid + '.protocol')[0].value = ssm[3];
document.getElementsByName('cbid.openclash.' + sid + '.cipher_ssr')[0].value = ssm[4];
document.getElementsByName('cbid.openclash.' + sid + '.obfs_ssr')[0].value = ssm[5];
document.getElementsByName('cbid.openclash.' + sid + '.password')[0].value = b64decsafe(ssm[6]);
document.getElementsByName('cbid.openclash.' + sid + '.obfs_param')[0].value = dictvalue(pdict, 'obfsparam');
document.getElementsByName('cbid.openclash.' + sid + '.protocol_param')[0].value = dictvalue(pdict, 'protoparam');
var rem = pdict['remarks'];
if (typeof (rem) != 'undefined' && rem != '' && rem.length > 0) document.getElementsByName('cbid.openclash.' + sid + '.name')[0].value = b64decutf8safe(rem);
s.innerHTML = "<font style=\"color:green\"><%:Import configuration information successfully.%></font>";
return false;
case "trojan":
var url0, param = "";
var ploc = ssu[1].indexOf("#");
if (ploc > 0) {
url0 = ssu[1].substr(0, ploc);
param = ssu[1].substr(ploc + 1);
} else {
url0 = ssu[1]
}
var sstr = url0;
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].value = "trojan";
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].dispatchEvent(event);
var team = sstr.split('@');
var password = team[0]
var serverPart = team[1].split(':');
var others = serverPart[1].split('?');
var port = parseInt(others[0]);
var queryParam = {}
if (others.length > 1) {
var queryParams = others[1]
var queryArray = queryParams.split('&');
for (i = 0; i < queryArray.length; i++) {
var params = queryArray[i].split('=');
queryParam[decodeURIComponent(params[0])] = decodeURIComponent(params[1] || '');
}
}
document.getElementsByName('cbid.openclash.' + sid + '.server')[0].value = serverPart[0];
document.getElementsByName('cbid.openclash.' + sid + '.port')[0].value = port || '443';
document.getElementsByName('cbid.openclash.' + sid + '.password')[0].value = password;
document.getElementsByName('cbid.openclash.' + sid + '.sni')[0].value = queryParam.sni || '';
if (queryParam.type != undefined) {
for (i = 0; i < document.getElementById('cbi.combobox.cbid.openclash.' + sid + '.alpn.1').getElementsByTagName("option").length; i++) {
if ( document.getElementById('cbi.combobox.cbid.openclash.' + sid + '.alpn.1').getElementsByTagName("option")[i].value == queryParam.type ) {
document.getElementById('cbi.combobox.cbid.openclash.' + sid + '.alpn.1').getElementsByTagName("option")[i].selected=true;
}
}
}
if (param != undefined) {
document.getElementsByName('cbid.openclash.' + sid + '.name')[0].value = decodeURI(param);
}
s.innerHTML = "<font style=\"color:green\"><%:Import configuration information successfully.%></font>";
return false;
case "vmess":
var sstr = b64DecodeUnicode(ssu[1]);
var ploc = sstr.indexOf("/?");
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].value = "vmess";
document.getElementsByName('cbid.openclash.' + sid + '.type')[0].dispatchEvent(event);
var url0, param = "";
if (ploc > 0) {
url0 = sstr.substr(0, ploc);
param = sstr.substr(ploc + 2);
}
var ssm = JSON.parse(sstr);
document.getElementsByName('cbid.openclash.' + sid + '.name')[0].value = ssm.ps;
document.getElementsByName('cbid.openclash.' + sid + '.server')[0].value = ssm.add;
document.getElementsByName('cbid.openclash.' + sid + '.port')[0].value = ssm.port;
document.getElementsByName('cbid.openclash.' + sid + '.alterId')[0].value = ssm.aid;
document.getElementsByName('cbid.openclash.' + sid + '.uuid')[0].value = ssm.id;
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].value = ssm.net;
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].dispatchEvent(event);
if (ssm.method) {
document.getElementsByName('cbid.openclash.' + sid + '.securitys')[0].value = ssm.method;
}
if (ssm.net == "tcp") {
if (ssm.type && ssm.type != "http") {
ssm.type = "none"
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].value = ssm.type;
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].dispatchEvent(event);
} else {
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].value = "http";
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].dispatchEvent(event);
document.getElementsByName('cbid.openclash.' + sid + '.http_path')[0].value = ssm.path;
}
}
if (ssm.net == "ws") {
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].value = "websocket";
document.getElementsByName('cbid.openclash.' + sid + '.obfs_vmess')[0].dispatchEvent(event);
document.getElementsByName('cbid.openclash.' + sid + '.ws_opts_path')[0].value = ssm.path;
document.getElementsByName('cbid.openclash.' + sid + '.ws_opts_headers')[0].value = "Host: " + ssm.host;
if (ssm.maxearlydata) {
document.getElementsByName('cbid.openclash.' + sid + '.max_early_data')[0].value = ssm.maxearlydata;
}
if (ssm.earlydataheadername) {
document.getElementsByName('cbid.openclash.' + sid + '.early_data_header_name')[0].value = ssm.earlydataheadername;
}
}
if (ssm.net == "h2") {
document.getElementsByName('cbid.openclash.' + sid + '.h2_host')[0].value = ssm.host;
document.getElementsByName('cbid.openclash.' + sid + '.h2_path')[0].value = ssm.path;
}
if (ssm.tls == "tls") {
document.getElementsByName('cbid.openclash.' + sid + '.tls')[0].value = "true";
}
if (ssm.sni) {
document.getElementsByName('cbid.openclash.' + sid + '.servername')[0].value = ssm.sni;
}
s.innerHTML = "<font style=\"color:green\"><%:Import configuration information successfully.%></font>";
return false;
default:
s.innerHTML = "<font style=\"color:red\"><%:Invalid format.%></font>";
return false;
}
}
//]]>
</script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Import%>" onclick="return import_ssr_url(this, '<%=self.option%>', '<%=self.value%>')" />
<span id="<%=self.option%>-status"></span>
<%+cbi/valuefooter%>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,80 @@
<%+cbi/valueheader%>
<style>
.sub_tab{
display: inline-block;
white-space: nowrap;
color: black;
font-size: 12px;
font-style: italic;
margin-top: 5px;
opacity: 0;
}
.sub_tab_show{
display: inline-block;
white-space: nowrap;
color: black;
font-size: 12px;
font-style: italic;
margin-top: 5px;
-webkit-transition: all 1.5s;
-moz-transition: all 1.5s;
-ms-transition: all 1.5s;
-o-transition: all 1.5s;
transition: all 1s;
opacity: 1;
}
</style>
<div style="text-align: center;">
<%
local fs = require "luci.openclash"
local val = self:cfgvalue(section)
local filename = fs.filename(val)
local idname = math.random(1000)..(string.match(filename, "[%w_]+") or "")
write(pcdata(val))
%>
<br/>
<div id='<%=idname%>' class="sub_tab"></div>
</div>
<script type="text/javascript">//<![CDATA[
var new_sub_info_get_<%=idname%> = true;
sub_info_get_<%=idname%>();
function sub_info_get_<%=idname%>()
{
if (document.getElementById('<%=idname%>').innerHTML != "" && ! new_sub_info_get_<%=idname%>) {
clearTimeout(s_<%=idname%>);
return
}
else {
if (localStorage.getItem("<%=filename%>")) {
var save_info = JSON.parse(localStorage.getItem("<%=filename%>"));
document.getElementById('<%=idname%>').className = "sub_tab_show";
document.getElementById('<%=idname%>').innerHTML = "- <%:Plan Traffic%>" + ": " + "<span style=color:green>" + save_info.used + "</span> | <span style=color:green>" + save_info.total + "</span> - <%:Plan Expiration Time%>: " + "<span style=color:green>" + save_info.expire + "</span> -";
}
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "sub_info_get")%>', {filename: "<%=filename%>"}, function(x, status) {
if (x && x.status == 200 && status.sub_info != "" && status.sub_info != "No Sub Info Found") {
localStorage.setItem("<%=filename%>",JSON.stringify(status));
new_sub_info_get_<%=idname%> = false;
document.getElementById('<%=idname%>').className = "sub_tab_show";
document.getElementById('<%=idname%>').innerHTML = "- <%:Plan Traffic%>" + ": " + "<span style=color:green>" + status.used + "</span> | <span style=color:green>" + status.total + "</span> - <%:Plan Expiration Time%>: " + "<span style=color:green>" + status.expire + "</span> -";
}
else if ( x && x.status == 200 && status.sub_info == "No Sub Info Found" ) {
document.getElementById('<%=idname%>').style.display = "none";
document.getElementById('<%=idname%>').style.width = "0";
clearTimeout(s_<%=idname%>);
return
};
});
var s_<%=idname%> = setTimeout("sub_info_get_<%=idname%>()",1000*60);
};
//]]></script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,45 @@
<fieldset class="cbi-section">
<table width="100%">
<tr><td width="100%" colspan="4">
<p align="center" id="switch_mode">
<%:Collecting data...%>
</p>
</td></tr>
</table>
</fieldset>
<script type="text/javascript">//<![CDATA[
var switch_mode = document.getElementById('switch_mode');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "op_mode")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.op_mode == "redir-host" ) {
switch_mode.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch page to Fake-IP mode%>" onclick="return switch_modes(this)"/>';
}
else {
switch_mode.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch page to Redir-Host mode%>" onclick="return switch_modes(this)"/>';
}
}
});
function switch_modes(btn)
{
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_mode")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.switch_mode == "redir-host" ) {
alert('<%:Page has been switched to Fake-IP mode!%>')
window.location.href='<%="settings"%>';
}
else {
alert('<%:Page has been switched to Redir-Host mode!%>')
window.location.href='<%="settings"%>';
}
}
});
btn.disabled = false;
return false;
}
//]]></script>

View File

@ -0,0 +1,121 @@
<head>
<style>
.tool_label {
display: inline-block;
padding: 0.6rem 0rem;
}
</style>
</head>
<div id="tool_label" class="tool_label">
<span>
&nbsp;&nbsp;<%:Current Config File%>:&nbsp;
<select id="cfg_name">
</select>&nbsp;&nbsp;
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Switch Config%>" onclick="return switch_config(this)" />
&nbsp;
</span>
</div>
<script type="text/javascript">//<![CDATA[
var config_name = document.getElementById('cfg_name');
var tool_label = document.getElementById('tool_label');
var Commit = document.getElementById('cbi-table-1-Commit');
var Apply = document.getElementById('cbi-table-1-Apply');
var Load_Config = document.getElementById('cbi-table-1-Load_Config');
var Delete_Unused_Servers = document.getElementById('cbi-table-1-Delete_Unused_Servers');
var Delete_Servers = document.getElementById('cbi-table-1-Delete_Servers');
var Delete_Proxy_Provider = document.getElementById('cbi-table-1-Delete_Proxy_Provider');
var Delete_Groups = document.getElementById('cbi-table-1-Delete_Groups');
var rule_mg = document.getElementById('cbi-table-1-rule_mg');
var pro_mg = document.getElementById('cbi-table-1-pro_mg');
setTimeout("get_header()",100);
if (Commit) {
Commit.style.textAlign="center";
Apply.style.textAlign="center";
}
if (Load_Config) {
Load_Config.style.textAlign="center";
Delete_Unused_Servers.style.textAlign="center";
Delete_Servers.style.textAlign="center";
Delete_Proxy_Provider.style.textAlign="center";
Delete_Groups.style.textAlign="center";
}
if (rule_mg) {
rule_mg.style.textAlign="center";
pro_mg.style.textAlign="center";
}
if (tool_label.style.display != "none") {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "config_name")%>', null, function(x, status) {
if (x && x.status == 200 && status.config_name != "") {
for(var i in status.config_name){
config_name.options.add(new Option(status.config_name[i].name,status.config_name[i].name));
}
if (status.config_path != "") {
config_name.value = status.config_path;
}
else
{
config_name.options.add(new Option("<%:Not Select%>",""));
config_name.value = "";
}
}
else if (x && x.status == 200 && status.config_path != "") {
config_name.options.add(new Option(status.config_path,status.config_path));
config_name.value = status.config_path;
}
else {
tool_label.style.display = "none";
}
});
};
function get_header() {
var header = document.getElementsByClassName("tabmenu-item-log ")[0];
if (header) {
insertAfter(tool_label,header);
}
else {
setTimeout("get_header()",100);
}
}
function insertAfter(newElement, targetElement) {
var parent = targetElement.parentNode;
if(parent.lastChild == targetElement) {
parent.appendChild(newElement, targetElement);
}
else {
parent.insertBefore(newElement, targetElement.nextSibling);
};
};
function switch_config(btn)
{
if (config_name.value && config_name.value != "") {
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_config")%>', {config_name: config_name.value}, function(x, status) {
if (x && x.status == 200) {
btn.disabled = false;
alert(' <%:Config File%>: ' + config_name.value + ' <%:switching succeeded!%>')
}
else {
alert(' <%:Config File%>: ' + config_name.value + ' <%:switching failed!%>')
}
});
return false;
}
}
function winOpen(url)
{
var winOpen = window.open(url);
if(winOpen == null || typeof(winOpen) == 'undefined'){
window.location.href=url;
}
}
//]]></script>

View File

@ -0,0 +1,424 @@
<fieldset class="cbi-section">
<table width="100%">
<tr><td width="100%" colspan="4">
<p align="center" id="update_tip">
<b><%:Note: if the update fails, you can manually download and upload%></b>
</p>
</td></tr>
<tr><td width="25%"><%:Compiled Version Selected%></td>
<td width="25%" align="left"><select id="CORE_VERSION">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64"><%:linux-amd64(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-armv8"><%:linux-armv8%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select></td>
<td width="25%"><%:Release Branch Selected%></td>
<td width="25%" align="left"><select id="RELEASE_BRANCH">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select></td>
</tr>
<tr>
<td width="25%"><%:Last Check Update%></td><td width="25%" align="left" id="CHECKTIME"><%:Collecting data...%></td>
<td width="25%"><%:CPU Architecture%></td><td width="25%" align="left" id="CPU_MODEL"><%:Collecting data...%></td></tr>
<tr><td width="100%" colspan="4">
<p align="center">
<b><%:Core path:%> /etc/openclash/core/clash</b>
</p>
</td></tr>
<tr><td width="25%">[dev] <%:Current Core%></td><td width="25%" align="left" id="CORE_CV"><%:Collecting data...%></td><td width="25%">[dev] <%:Latest Core%></td><td width="25%" align="left" id="CORE_LV"><%:Collecting data...%></td></tr>
<tr><td width="25%"><%:Update Core%></td><td width="25%" align="left" id="core_up"><%:Collecting data...%></td><td width="25%"><%:Download Latest Core%></td><td width="25%" align="left" id="ma_core_up"><%:Collecting data...%></td></tr>
<tr><td width="100%" colspan="4">
<p align="center">
<b><%:Core path:%>/etc/openclash/core/clash_tun </b>
</p>
</td></tr>
<tr><td width="25%">[TUN] <%:Current Core%></td><td width="25%" align="left" id="CORE_TUN_CV"><%:Collecting data...%></td><td width="25%">[TUN] <%:Latest Core%></td><td width="25%" align="left" id="CORE_TUN_LV"><%:Collecting data...%></td></tr>
<tr><td width="25%"><%:Update Core%></td><td width="25%" align="left" id="core_tun_up"><%:Collecting data...%></td><td width="25%"><%:Download Latest Core%></td><td width="25%" align="left" id="ma_core_tun_up"><%:Collecting data...%></td></tr>
</table>
</fieldset>
<fieldset class="cbi-section">
<table width="100%">
<tr><td width="100%" colspan="4">
<p align="center">
<b><%:Client Update%></b>
</p>
</td></tr>
<tr><td width="25%"><%:Current Client%></td><td width="25%" align="left" id="OP_CV"><%:Collecting data...%></td><td width="25%"><%:Latest Client%></td><td width="25%" align="left" id="OP_LV"><%:Collecting data...%></td></tr>
<tr><td width="25%"><%:Update Client%></td><td width="25%" align="left" id="op_up"><%:Collecting data...%></td><td width="25%"><%:Download Latest Client%></td><td width="25%" align="left" id="ma_op_up"><%:Collecting data...%></td></tr>
</table>
</fieldset>
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td width="25%">
<p align="center" id="restore">
<%:Collecting data...%>
</p>
</td>
<td width="25%">
<p align="center" id="backup">
<%:Collecting data...%>
</p>
</td>
<td width="25%">
<p align="center" id="remove_core">
<%:Collecting data...%>
</p>
</td>
<td width="25%">
<p align="center" id="one_key_update">
<%:Collecting data...%>
</p>
</td>
</tr>
</table>
</fieldset>
<script type="text/javascript">//<![CDATA[
var core_version = document.getElementById('CORE_VERSION');
var checktime = document.getElementById('CHECKTIME');
var cpu_model = document.getElementById('CPU_MODEL');
var core_cv = document.getElementById('CORE_CV');
var core_lv = document.getElementById('CORE_LV');
var core_tun_cv = document.getElementById('CORE_TUN_CV');
var core_tun_lv = document.getElementById('CORE_TUN_LV');
var op_cv = document.getElementById('OP_CV');
var op_lv = document.getElementById('OP_LV');
var core_up = document.getElementById('core_up');
var core_tun_up = document.getElementById('core_tun_up');
var op_up = document.getElementById('op_up');
var update_tip = document.getElementById('update_tip');
var ma_core_up = document.getElementById('ma_core_up');
var ma_core_tun_up = document.getElementById('ma_core_tun_up');
var ma_op_up = document.getElementById('ma_op_up');
var restore = document.getElementById('restore');
var backup = document.getElementById('backup');
var one_key_update = document.getElementById('one_key_update');
var remove_core = document.getElementById('remove_core');
var release_branch = document.getElementById('RELEASE_BRANCH');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.corever != "0" && status.corever != "" ) {
core_version.value = status.corever;
}
else {
core_version.value = "0";
}
if ( status.release_branch != "" ) {
release_branch.value = status.release_branch;
}
else {
release_branch.value = "master";
}
}
});
XHR.poll(3, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "update")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
cpu_model.innerHTML = status.coremodel ? "<b style=color:green>"+status.coremodel+"</b>" : "<b style=color:red><%:Model Not Found%></b>";
if ( status.upchecktime != "1" ) {
checktime.innerHTML = "<b style=color:green>"+status.upchecktime+"</b>";
}
else {
checktime.innerHTML = "<b style=color:red><%:Check Failed%></b>";
}
if ( status.corecv == "0" ) {
core_cv.innerHTML = "<b style=color:red><%:File Not Exist%></b>";
}
else if (status.corecv != "") {
core_cv.innerHTML = "<b style=color:green>"+status.corecv+"</b>";
}
else {
core_cv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
if ( status.coretuncv == "0" ) {
core_tun_cv.innerHTML = "<b style=color:red><%:File Not Exist%></b>";
}
else if (status.coretuncv != "") {
core_tun_cv.innerHTML = "<b style=color:green>"+status.coretuncv+"</b>";
}
else {
core_tun_cv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
var corelv = status.corelv;
var arr_core = corelv.split(",");
var corelvis = arr_core[0];
var coretunlvis = arr_core[1];
if (corelvis != status.corecv && corelvis != "") {
core_lv.innerHTML = "<b style=color:green>"+corelvis+"<%:<New>%></b>";
}
else if (corelvis != "" && corelvis == status.corecv) {
core_lv.innerHTML = "<b style=color:green>"+corelvis+"</b>";
}
else {
core_lv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
if (coretunlvis != status.coretuncv && coretunlvis != "") {
core_tun_lv.innerHTML = "<b style=color:green>"+coretunlvis+"<%:<New>%></b>";
}
else if (coretunlvis != "" && coretunlvis == status.coretuncv) {
core_tun_lv.innerHTML = "<b style=color:green>"+coretunlvis+"</b>";
}
else {
core_tun_lv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
var oplv = status.oplv;
var arr_op = oplv.split(",");
var oplvis = arr_op[0];
var new_op = arr_op[1];
op_cv.innerHTML = status.opcv ? "<b style=color:green>"+status.opcv+"</b>" : "<b style=color:red><%:Unknown%></b>";
if ( new_op == "2" && oplvis != "") {
op_lv.innerHTML = "<b style=color:green>"+oplvis+"<%:<New>%></b>";
}
else if (oplvis != "") {
op_lv.innerHTML = "<b style=color:green>"+oplvis+"</b>";
}
else {
op_lv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
}
});
core_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check And Update%>" onclick="return core_update(this,\'Dev\')"/>';
core_tun_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check And Update%>" onclick="return core_update(this,\'TUN\')"/>';
op_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check And Update%>" onclick="return op_update(this)"/>';
ma_core_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Download%>" onclick="return ma_core_update(this,\'Dev\')"/>';
ma_core_tun_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Download%>" onclick="return ma_core_update(this,\'TUN\')"/>';
ma_op_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Download%>" onclick="return ma_op_update(this)"/>';
restore.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Restore Default Config%>" onclick="return restore_config(this)"/>';
one_key_update.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:One Click Check Update%>" onclick="return all_one_key_update(this)"/>';
remove_core.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Remove Core%>" onclick="return remove_all_core(this)"/>';
backup.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Backup OpenClash%>" onclick="return backup_all_file(this)"/>';
function core_update(btn,type)
{
var v = core_version.value;
var r = release_branch.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "coreupdate")%>', {core_type: type}, function(x, status) {
btn.value = '<%:Check And Update%>';
btn.disabled = false;
return false;
});
}
});
}
function op_update(btn)
{
var r = release_branch.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {release_branch: r}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "opupdate")%>', null, function(x, status) {
btn.value = '<%:Check And Update%>';
btn.disabled = false;
return false;
});
}
});
}
function ma_core_update(btn,type)
{
var v = core_version.value;
var r = release_branch.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r}, function(x, status) {
if (x && x.status == 200) {
btn.value = '<%:Download%>';
btn.disabled = false;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_ma")%>', status.corever, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.corever != "0" ) {
if (type == "Dev") {
if (r == "dev") {
url1='https://raw.githubusercontent.com/vernesong/OpenClash/'+r+'/core-lateset/dev/clash-'+status.corever+'.tar.gz';
window.location.href=url1;
}
else {
url1='https://github.com/vernesong/OpenClash/releases/download/Clash/clash-'+status.corever+'.tar.gz';
window.location.href=url1;
}
}
if (type == "TUN") {
var corelv = status.corelv;
var arr_core = corelv.split(",");
var coretunlvis = arr_core[1];
if ( coretunlvis != "" ) {
if (r == "dev") {
url3='https://raw.githubusercontent.com/vernesong/OpenClash/'+r+'/core-lateset/premium/clash-'+status.corever+'-'+coretunlvis+'.gz';
window.location.href=url3;
}
else {
url3='https://github.com/vernesong/OpenClash/releases/download/TUN-Premium/clash-'+status.corever+'-'+coretunlvis+'.gz';
window.location.href=url3;
}
}
else {
alert('<%:Failed to get the latest version. Please try again later!%>')
}
}
}
else {
alert('<%:No Compiled Version is Selected, Please Select on The Top and Try Again!%>')
}
}
});
return false;
}
});
}
function ma_op_update(btn)
{
btn.value = '<%:Download%>';
btn.disabled = false;
var r = release_branch.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {release_branch: r}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_ma")%>', status.oplv, function(x, status) {
if ( x && x.status == 200 ) {
var oplv = status.oplv;
var oplvis = oplv.substring(oplv.indexOf("v") + 1,oplv.indexOf(","));
if ( oplvis != "" ) {
if (r == "dev") {
url2='https://raw.githubusercontent.com/vernesong/OpenClash/'+r+'/luci-app-openclash_'+oplvis+'_all.ipk';
window.location.href=url2;
}
else {
url2='https://github.com/vernesong/OpenClash/releases/download/v'+oplvis+'/luci-app-openclash_'+oplvis+'_all.ipk';
window.location.href=url2;
}
}
else {
alert('<%:Failed to get the latest version. Please try again later!%>')
}
}
});
}
});
return false;
}
function remove_all_core(btn)
{
btn.value = '<%:Remove Core%>';
btn.disabled = true;
var r = confirm("<%:Are you sure want to remove all core files?%>")
if (r == true) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "remove_all_core")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
alert('<%:Remove succeeded!%>')
window.location.href='<%="settings?tab.openclash.config=version_update"%>';
}
else {
alert('<%:Remove failed!%>')
}
});
} else {
}
btn.disabled = false;
return false;
}
function restore_config(btn)
{
btn.value = '<%:Restore Default Config%>';
btn.disabled = true;
var r = confirm("<%:Are you sure want to restore the default config?%>")
if (r == true) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "restore")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
alert('<%:Restore succeeded!%>')
window.location.href='<%="settings"%>';
}
else {
alert('<%:Restore failed!%>')
window.location.href='<%="settings"%>';
}
});
}
btn.disabled = false;
return false;
}
function backup_all_file(btn)
{
btn.value = '<%:Backup OpenClash%>';
btn.disabled = true;
window.location.href='<%="backup"%>';
btn.disabled = false;
return false;
}
function all_one_key_update(btn)
{
var v = core_version.value;
var r = release_branch.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r}, function(x, status) {
if (x && x.status == 200) {
btn.value = '<%:One Click Check Update%>';
btn.disabled = true;
var r = confirm("<%:Check and update all Cores and plug-ins?%>")
if (r == true) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "one_key_update_check")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.corever != "0" ) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "one_key_update")%>', null, function(x, status) {
if ( x && x.status != 200 ) {
alert('<%:Check failed, Please try again later!%>')
}
});
}
else {
alert('<%:No Compiled Version is Selected, Please Select on The Top and Try Again!%>')
}
}
else {
alert('<%:Check failed, Please try again later!%>')
}
});
}
btn.disabled = false;
return false;
}
});
}
XHR.poll(7, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "startlog")%>', status.startlog, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.startlog == "\n" || status.startlog == "" ) {
var rdmdl=Math.floor(Math.random()*2)+1;
if(rdmdl==1)
{
update_tip.innerHTML = '<b><font><%:Note: if the update fails, you can manually download and upload%></font></b>';
}
if(rdmdl==2)
{
update_tip.innerHTML = '<b><font><%:Note: the client may not support update, because the firmware with squashfs format will not release flash space after updating%></font></b>';
}
}
}
});
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "startlog")%>', status.startlog, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.startlog != "\n" && status.startlog != "" ) {
update_tip.innerHTML = '<b style=color:green>'+status.startlog+'</b>';
}
}
});
//]]></script>

View File

@ -0,0 +1,27 @@
<%+cbi/valueheader%>
<div style="text-align: center; margin:0 auto; display:block; white-space: nowrap;">
<label class="cbi-value" style="display:inline-block; width: 100%;" for="ulfile"><%:Upload File Type%>&nbsp;&nbsp;&nbsp;
<select name="file_type" style="width:auto">&nbsp;&nbsp;&nbsp;
<option value="config" selected="selected"><%:Config File%></option>
<option value="proxy-provider"><%:Proxy Provider File%></option>
<option value="rule-provider"><%:Rule Provider File%></option>
<option value="backup-file"><%:Backup File%></option>
</select>
<input class="cbi-input-file" style="width: 30%" type="file" id="ulfile" name="ulfile" />
<input type="submit" class="btn cbi-button cbi-input-reload" name="upload" value="<%:Upload%>" />
<input type="submit" class="btn cbi-button cbi-button-reset" value="<%:Backup%>" onclick="return backup_all_file(this)"/>
</div>
<%+cbi/valuefooter%>
<script type="text/javascript">//<![CDATA[
function backup_all_file(btn)
{
btn.value = '<%:Backup%>';
btn.disabled = true;
window.location.href='<%="backup"%>';
btn.disabled = false;
return false;
};
//]]></script>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More