From 300ac6e0dd1b3fa069f6b27d32b57192ee5883da Mon Sep 17 00:00:00 2001 From: gitea-action Date: Wed, 25 Sep 2024 05:44:33 +0800 Subject: [PATCH] luci-app-mihomo: sync upstream last commit: https://github.com/morytyann/OpenWrt-mihomo/commit/30b31d9d65cee5d1497a743b7f15477e2c4d40e5 --- luci-app-mihomo/Makefile | 10 + .../luci-static/resources/tools/mihomo.js | 113 ++++ .../resources/view/mihomo/config.js | 515 ++++++++++++++++++ .../resources/view/mihomo/editor.js | 63 +++ .../luci-static/resources/view/mihomo/log.js | 98 ++++ luci-app-mihomo/po/zh_Hans/mihomo.po | 416 ++++++++++++++ luci-app-mihomo/root/usr/libexec/mihomo-call | 46 ++ .../share/luci/menu.d/luci-app-mihomo.json | 37 ++ .../usr/share/rpcd/acl.d/luci-app-mihomo.json | 32 ++ 9 files changed, 1330 insertions(+) create mode 100644 luci-app-mihomo/Makefile create mode 100644 luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js create mode 100644 luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js create mode 100644 luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/editor.js create mode 100644 luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/log.js create mode 100644 luci-app-mihomo/po/zh_Hans/mihomo.po create mode 100755 luci-app-mihomo/root/usr/libexec/mihomo-call create mode 100644 luci-app-mihomo/root/usr/share/luci/menu.d/luci-app-mihomo.json create mode 100644 luci-app-mihomo/root/usr/share/rpcd/acl.d/luci-app-mihomo.json diff --git a/luci-app-mihomo/Makefile b/luci-app-mihomo/Makefile new file mode 100644 index 000000000..0dc3a917e --- /dev/null +++ b/luci-app-mihomo/Makefile @@ -0,0 +1,10 @@ +include $(TOPDIR)/rules.mk + +PKG_VERSION:=1.8.3 + +LUCI_TITLE:=LuCI Support for mihomo +LUCI_DEPENDS:=+luci-base +mihomo + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature \ No newline at end of file diff --git a/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js b/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js new file mode 100644 index 000000000..d04476e75 --- /dev/null +++ b/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js @@ -0,0 +1,113 @@ +'use strict'; +'require baseclass'; +'require uci'; +'require fs'; +'require rpc'; + +const homeDir = '/etc/mihomo'; +const profilesDir = `${homeDir}/profiles`; +const mixinFilePath = `${homeDir}/mixin.yaml`; +const runDir = `${homeDir}/run`; +const runAppLogPath = `${runDir}/app.log`; +const runCoreLogPath = `${runDir}/core.log`; +const runProfilePath = `${runDir}/config.yaml`; +const nftDir = `${homeDir}/nftables`; +const reservedIPNFT = `${nftDir}/reserved_ip.nft`; +const reservedIP6NFT = `${nftDir}/reserved_ip6.nft`; + +return baseclass.extend({ + homeDir: homeDir, + profilesDir: profilesDir, + mixinFilePath: mixinFilePath, + runDir: runDir, + runAppLogPath: runAppLogPath, + runCoreLogPath: runCoreLogPath, + runProfilePath: runProfilePath, + reservedIPNFT: reservedIPNFT, + reservedIP6NFT: reservedIP6NFT, + + callServiceList: rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } + }), + + getAppLog: function () { + return L.resolveDefault(fs.read_direct(this.runAppLogPath)); + }, + + getCoreLog: function () { + return L.resolveDefault(fs.read_direct(this.runCoreLogPath)); + }, + + clearAppLog: function () { + return fs.exec_direct('/usr/libexec/mihomo-call', ['clear', 'app_log']); + }, + + clearCoreLog: function () { + return fs.exec_direct('/usr/libexec/mihomo-call', ['clear', 'core_log']); + }, + + listProfiles: function () { + return L.resolveDefault(fs.list(this.profilesDir), []); + }, + + status: async function () { + try { + return (await this.callServiceList('mihomo'))['mihomo']['instances']['mihomo']['running']; + } catch (ignored) { + return false; + } + }, + + reload: function () { + return fs.exec_direct('/usr/libexec/mihomo-call', ['service', 'reload']); + }, + + restart: function () { + return fs.exec_direct('/usr/libexec/mihomo-call', ['service', 'restart']); + }, + + appVersion: function () { + return L.resolveDefault(fs.exec_direct('/usr/libexec/mihomo-call', ['version', 'app']), 'Unknown'); + }, + + coreVersion: function () { + return L.resolveDefault(fs.exec_direct('/usr/libexec/mihomo-call', ['version', 'core']), 'Unknown'); + }, + + callMihomoAPI: async function (method, path, body) { + const running = await this.status(); + if (running) { + const apiPort = uci.get('mihomo', 'mixin', 'api_port'); + const apiSecret = uci.get('mihomo', 'mixin', 'api_secret'); + const url = `http://${window.location.hostname}:${apiPort}${path}`; + await fetch(url, { + method: method, + headers: { 'Authorization': `Bearer ${apiSecret}` }, + body: body + }) + } else { + alert(_('Service is not running.')); + } + }, + + openDashboard: async function () { + const running = await this.status(); + if (running) { + const uiName = uci.get('mihomo', 'mixin', 'ui_name'); + const apiPort = uci.get('mihomo', 'mixin', 'api_port'); + const apiSecret = uci.get('mihomo', 'mixin', 'api_secret'); + let url; + if (uiName) { + url = `http://${window.location.hostname}:${apiPort}/ui/${uiName}/?host=${window.location.hostname}&hostname=${window.location.hostname}&port=${apiPort}&secret=${apiSecret}`; + } else { + url = `http://${window.location.hostname}:${apiPort}/ui/?host=${window.location.hostname}&hostname=${window.location.hostname}&port=${apiPort}&secret=${apiSecret}`; + } + setTimeout(() => window.open(url, '_blank'), 0); + } else { + alert(_('Service is not running.')); + } + }, +}) diff --git a/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js new file mode 100644 index 000000000..2d461bda2 --- /dev/null +++ b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/config.js @@ -0,0 +1,515 @@ +'use strict'; +'require form'; +'require view'; +'require uci'; +'require fs'; +'require network'; +'require rpc'; +'require poll'; +'require tools.widgets as widgets'; +'require tools.mihomo as mihomo'; + +function renderStatus(running) { + return updateStatus(E('input', { id: 'core_status', style: 'border: unset; font-style: italic; font-weight: bold;', readonly: '' }), running); +} + +function updateStatus(element, running) { + if (element) { + element.style.color = running ? 'green' : 'red'; + element.value = running ? _('Running') : _('Not Running'); + } + return element; +} + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('mihomo'), + mihomo.listProfiles(), + mihomo.appVersion(), + mihomo.coreVersion(), + mihomo.status(), + network.getHostHints(), + ]); + }, + render: function (data) { + const subscriptions = uci.sections('mihomo', 'subscription'); + const profiles = data[1]; + const appVersion = data[2]; + const coreVersion = data[3]; + const running = data[4]; + const hosts = data[5].hosts; + + let m, s, o, so; + + m = new form.Map('mihomo', _('MihomoTProxy'), `${_('Transparent Proxy with Mihomo on OpenWrt.')} ${_('How To Use')}`); + + s = m.section(form.NamedSection, 'status', 'status', _('Status')); + + o = s.option(form.Value, '_app_version', _('App Version')); + o.readonly = true; + o.load = function (section_id) { + return appVersion.trim(); + }; + o.write = function () { }; + + o = s.option(form.Value, '_core_version', _('Core Version')); + o.readonly = true; + o.load = function (section_id) { + return coreVersion.trim(); + }; + o.write = function () { }; + + o = s.option(form.DummyValue, '_core_status', _('Core Status')); + o.cfgvalue = function (section_id) { + return renderStatus(running); + }; + poll.add(function () { + return L.resolveDefault(mihomo.status()).then(function (running) { + updateStatus(document.getElementById('core_status'), running); + }); + }); + + o = s.option(form.Button, 'reload', '-'); + o.inputstyle = 'action'; + o.inputtitle = _('Reload Service'); + o.onclick = function () { + return mihomo.reload(); + }; + + o = s.option(form.Button, 'restart', '-'); + o.inputstyle = 'negative'; + o.inputtitle = _('Restart Service'); + o.onclick = function () { + return mihomo.restart(); + }; + + o = s.option(form.Button, 'update_dashboard', '-'); + o.inputstyle = 'positive'; + o.inputtitle = _('Update Dashboard'); + o.onclick = function () { + return mihomo.callMihomoAPI('POST', '/upgrade/ui'); + }; + + o = s.option(form.Button, 'open_dashboard', '-'); + o.inputtitle = _('Open Dashboard'); + o.onclick = function () { + return mihomo.openDashboard(); + }; + + s = m.section(form.NamedSection, 'config', 'config', _('Basic Config')); + + o = s.option(form.Flag, 'enabled', _('Enable')); + o.rmempty = false; + + o = s.option(form.Flag, 'scheduled_restart', _('Scheduled Restart')); + o.rmempty = false; + + o = s.option(form.Value, 'cron_expression', _('Cron Expression')); + o.retain = true; + o.rmempty = false; + o.depends('scheduled_restart', '1'); + + o = s.option(form.ListValue, 'profile', _('Choose Profile')); + o.rmempty = false; + + for (const profile of profiles) { + o.value('file:' + profile.name, _('File:') + profile.name); + } + + for (const subscription of subscriptions) { + o.value('subscription:' + subscription['.name'], _('Subscription:') + subscription.name); + } + + o = s.option(form.FileUpload, 'upload_profile', _('Upload Profile')); + o.root_directory = mihomo.profilesDir; + + o = s.option(form.Flag, 'mixin', _('Mixin')); + o.rmempty = false; + + o = s.option(form.Flag, 'test_profile', _('Test Profile')); + o.rmempty = false; + + o = s.option(form.Flag, 'fast_reload', _('Fast Reload')); + o.rmempty = false; + + s = m.section(form.NamedSection, 'proxy', 'proxy', _('Proxy Config')); + + s.tab('transparent_proxy', _('Transparent Proxy')); + + o = s.taboption('transparent_proxy', form.Flag, 'transparent_proxy', _('Enable')); + o.rmempty = false; + + o = s.taboption('transparent_proxy', form.ListValue, 'tcp_transparent_proxy_mode', _('TCP Proxy Mode')); + o.value('redirect', _('Redirect Mode')); + o.value('tproxy', _('TPROXY Mode')); + o.value('tun', _('TUN Mode')); + + o = s.taboption('transparent_proxy', form.ListValue, 'udp_transparent_proxy_mode', _('UDP Proxy Mode')); + o.value('tproxy', _('TPROXY Mode')); + o.value('tun', _('TUN Mode')); + + o = s.taboption('transparent_proxy', form.Flag, 'ipv4_dns_hijack', _('IPv4 DNS Hijack')); + o.rmempty = false; + + o = s.taboption('transparent_proxy', form.Flag, 'ipv6_dns_hijack', _('IPv6 DNS Hijack')); + o.rmempty = false; + + o = s.taboption('transparent_proxy', form.Flag, 'ipv4_proxy', _('IPv4 Proxy')); + o.rmempty = false; + + o = s.taboption('transparent_proxy', form.Flag, 'ipv6_proxy', _('IPv6 Proxy')); + o.rmempty = false; + + o = s.taboption('transparent_proxy', form.Flag, 'router_proxy', _('Router Proxy')); + o.rmempty = false; + + o = s.taboption('transparent_proxy', form.Flag, 'lan_proxy', _('Lan Proxy')); + o.rmempty = false; + + s.tab('access_control', _('Access Control')); + + o = s.taboption('access_control', form.ListValue, 'access_control_mode', _('Mode')); + o.value('all', _('All Mode')); + o.value('allow', _('Allow Mode')); + o.value('block', _('Block Mode')); + + o = s.taboption('access_control', form.DynamicList, 'acl_ip', 'IP'); + o.datatype = 'ipmask4'; + o.retain = true; + o.depends('access_control_mode', 'allow'); + o.depends('access_control_mode', 'block'); + + for (const mac in hosts) { + const host = hosts[mac]; + for (const ip of host.ipaddrs) { + const hint = host.name || mac; + o.value(ip, hint ? '%s (%s)'.format(ip, hint) : ip); + } + } + + o = s.taboption('access_control', form.DynamicList, 'acl_ip6', 'IP6'); + o.datatype = 'ipmask6'; + o.retain = true; + o.depends('access_control_mode', 'allow'); + o.depends('access_control_mode', 'block'); + + for (const mac in hosts) { + const host = hosts[mac]; + for (const ip of host.ip6addrs) { + const hint = host.name || mac; + o.value(ip, hint ? '%s (%s)'.format(ip, hint) : ip); + } + } + + o = s.taboption('access_control', form.DynamicList, 'acl_mac', 'MAC'); + o.datatype = 'macaddr'; + o.retain = true; + o.depends('access_control_mode', 'allow'); + o.depends('access_control_mode', 'block'); + + for (const mac in hosts) { + const host = hosts[mac]; + const hint = host.name || host.ipaddrs[0]; + o.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac); + } + + s.tab('bypass', _('Bypass')); + + o = s.taboption('bypass', form.Flag, 'bypass_china_mainland_ip', _('Bypass China Mainland IP')); + o.rmempty = false; + + o = s.taboption('bypass', form.Value, 'acl_tcp_dport', _('Destination TCP Port to Proxy')); + o.rmempty = false; + o.value('0-65535', _('All Port')); + o.value('21 22 80 110 143 194 443 465 993 995 8080 8443', _('Commonly Used Port')); + + o = s.taboption('bypass', form.Value, 'acl_udp_dport', _('Destination UDP Port to Proxy')); + o.rmempty = false; + o.value('0-65535', _('All Port')); + o.value('123 443 8443', _('Commonly Used Port')); + + s = m.section(form.TableSection, 'subscription', _('Subscription Config')); + s.addremove = true; + s.anonymous = true; + + o = s.option(form.Value, 'name', _('Subscription Name')); + o.rmempty = false; + o.width = '15%'; + + o = s.option(form.Value, 'url', _('Subscription Url')); + o.rmempty = false; + + o = s.option(form.Value, 'user_agent', _('User Agent')); + o.default = 'mihomo'; + o.rmempty = false; + o.width = '15%'; + o.value('mihomo'); + o.value('clash.meta'); + o.value('clash'); + + s = m.section(form.NamedSection, 'mixin', 'mixin', _('Mixin Config')); + + s.tab('general', _('General Config')); + + o = s.taboption('general', form.ListValue, 'log_level', _('Log Level')); + o.value('silent'); + o.value('error'); + o.value('warning'); + o.value('info'); + o.value('debug'); + + o = s.taboption('general', form.ListValue, 'mode', _('Proxy Mode')); + o.value('global', _('Global Mode')); + o.value('rule', _('Rule Mode')); + o.value('direct', _('Direct Mode')); + + o = s.taboption('general', form.ListValue, 'match_process', _('Match Process')); + o.value('strict', _('Auto')); + o.value('always', _('Enable')); + o.value('off', _('Disable')); + + o = s.taboption('general', widgets.NetworkSelect, 'outbound_interface', _('Outbound Interface')); + o.optional = true; + o.rmempty = false; + + o = s.taboption('general', form.Flag, 'ipv6', _('IPv6')); + o.rmempty = false; + + o = s.taboption('general', form.Value, 'tcp_keep_alive_idle', _('TCP Keep Alive Idle')); + o.datatype = 'integer'; + o.placeholder = '600'; + + o = s.taboption('general', form.Value, 'tcp_keep_alive_interval', _('TCP Keep Alive Interval')); + o.datatype = 'integer'; + o.placeholder = '15'; + + s.tab('external_control', _('External Control Config')); + + o = s.taboption('external_control', form.Value, 'ui_name', _('UI Name')); + o.rmempty = false; + + o = s.taboption('external_control', form.Value, 'ui_url', _('UI Url')); + o.rmempty = false; + o.value('https://mirror.ghproxy.com/https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip', 'MetaCubeXD') + o.value('https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip', 'YACD') + o.value('https://mirror.ghproxy.com/https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip', 'Razord') + + o = s.taboption('external_control', form.Value, 'api_port', _('API Port')); + o.datatype = 'port'; + o.placeholder = '9090'; + + o = s.taboption('external_control', form.Value, 'api_secret', _('API Secret')); + o.rmempty = false; + + o = s.taboption('external_control', form.Flag, 'selection_cache', _('Save Proxy Selection')); + o.rmempty = false; + + s.tab('inbound', _('Inbound Config')); + + o = s.taboption('inbound', form.Flag, 'allow_lan', _('Allow Lan')); + o.rmempty = false; + + o = s.taboption('inbound', form.Value, 'http_port', _('HTTP Port')); + o.datatype = 'port'; + o.placeholder = '8080'; + + o = s.taboption('inbound', form.Value, 'socks_port', _('SOCKS Port')); + o.datatype = 'port'; + o.placeholder = '1080'; + + o = s.taboption('inbound', form.Value, 'mixed_port', _('Mixed Port')); + o.datatype = 'port'; + o.placeholder = '7890'; + + o = s.taboption('inbound', form.Value, 'redir_port', _('Redirect Port')); + o.datatype = 'port'; + o.placeholder = '7891'; + + o = s.taboption('inbound', form.Value, 'tproxy_port', _('TPROXY Port')); + o.datatype = 'port'; + o.placeholder = '7892'; + + o = s.taboption('inbound', form.Flag, 'authentication', _('Authentication')); + o.rmempty = false; + + o = s.taboption('inbound', form.SectionValue, '_authentications', form.TableSection, 'authentication', _('Edit Authentications')); + o.retain = true; + o.depends('authentication', '1'); + + o.subsection.anonymous = true; + o.subsection.addremove = true; + + so = o.subsection.option(form.Flag, 'enabled', _('Enable')); + so.rmempty = false; + + so = o.subsection.option(form.Value, 'username', _('Username')); + so.rmempty = false; + + so = o.subsection.option(form.Value, 'password', _('Password')); + so.rmempty = false; + + s.tab('tun', _('TUN Config')); + + o = s.taboption('tun', form.ListValue, 'tun_stack', _('Stack')); + o.value('system', 'System'); + o.value('gvisor', 'gVisor'); + o.value('mixed', 'Mixed'); + + o = s.taboption('tun', form.Value, 'tun_mtu', _('MTU')); + o.placeholder = '9000'; + + o = s.taboption('tun', form.Flag, 'tun_gso', _('GSO')); + o.rmempty = false; + + o = s.taboption('tun', form.Value, 'tun_gso_max_size', _('GSO Max Size')); + o.placeholder = '65536'; + o.depends('tun_gso', '1'); + + o = s.taboption('tun', form.Flag, 'tun_endpoint_independent_nat', _('Endpoint Independent NAT')); + o.rmempty = false; + + s.tab('dns', _('DNS Config')); + + o = s.taboption('dns', form.Value, 'dns_port', _('DNS Port')); + o.datatype = 'port'; + o.placeholder = '1053'; + + o = s.taboption('dns', form.ListValue, 'dns_mode', _('DNS Mode')); + o.value('normal', 'Normal'); + o.value('fake-ip', 'Fake-IP'); + o.value('redir-host', 'Redir-Host'); + + o = s.taboption('dns', form.Value, 'fake_ip_range', _('Fake-IP Range')); + o.datatype = 'cidr4'; + o.placeholder = '198.18.0.1/16'; + o.retain = true; + o.depends('dns_mode', 'fake-ip'); + + o = s.taboption('dns', form.Flag, 'fake_ip_filter', _('Overwrite Fake-IP Filter')); + o.retain = true; + o.rmempty = false; + o.depends('dns_mode', 'fake-ip'); + + o = s.taboption('dns', form.DynamicList, 'fake_ip_filters', _('Edit Fake-IP Filters')); + o.retain = true; + o.depends({ 'dns_mode': 'fake-ip', 'fake_ip_filter': '1' }); + + o = s.taboption('dns', form.ListValue, 'fake_ip_filter_mode', _('Fake-IP Filter Mode')) + o.value('blacklist', _('Block Mode')); + o.value('whitelist', _('Allow Mode')); + o.depends({ 'dns_mode': 'fake-ip', 'fake_ip_filter': '1' }); + + o = s.taboption('dns', form.Flag, 'fake_ip_cache', _('Fake-IP Cache')); + o.retain = true; + o.rmempty = false; + o.depends('dns_mode', 'fake-ip'); + + o = s.taboption('dns', form.Flag, 'dns_respect_rules', _('Respect Rules')); + o.rmempty = false; + + o = s.taboption('dns', form.Flag, 'dns_ipv6', _('IPv6')); + o.rmempty = false; + + o = s.taboption('dns', form.Flag, 'dns_system_hosts', _('Use System Hosts')); + o.rmempty = false; + + o = s.taboption('dns', form.Flag, 'dns_hosts', _('Use Hosts')); + o.rmempty = false; + + o = s.taboption('dns', form.Flag, 'hosts', _('Overwrite Hosts')); + o.rmempty = false; + + o = s.taboption('dns', form.SectionValue, '_hosts', form.TableSection, 'host', _('Edit Hosts')); + o.retain = true; + o.depends('hosts', '1'); + + o.subsection.anonymous = true; + o.subsection.addremove = true; + + so = o.subsection.option(form.Flag, 'enabled', _('Enable')); + so.rmempty = false; + + so = o.subsection.option(form.Value, 'domain_name', _('Domain Name')); + so.rmempty = false; + + so = o.subsection.option(form.DynamicList, 'ip', _('IP')); + + o = s.taboption('dns', form.Flag, 'dns_nameserver', _('Overwrite Nameserver')); + o.rmempty = false; + + o = s.taboption('dns', form.SectionValue, '_dns_nameserver', form.TableSection, 'nameserver', _('Edit Nameservers')); + o.retain = true; + o.depends('dns_nameserver', '1'); + + o.subsection.anonymous = true; + o.subsection.addremove = false; + + so = o.subsection.option(form.Flag, 'enabled', _('Enable')); + so.rmempty = false; + + so = o.subsection.option(form.ListValue, 'type', _('Type')); + so.readonly = true; + so.value('default-nameserver'); + so.value('proxy-server-nameserver'); + so.value('nameserver'); + so.value('fallback'); + + so = o.subsection.option(form.DynamicList, 'nameserver', _('Nameserver')); + + o = s.taboption('dns', form.Flag, 'dns_nameserver_policy', _('Overwrite Nameserver Policy')); + o.rmempty = false; + + o = s.taboption('dns', form.SectionValue, '_dns_nameserver_policies', form.TableSection, 'nameserver_policy', _('Edit Nameserver Policies')); + o.retain = true; + o.depends('dns_nameserver_policy', '1'); + + o.subsection.anonymous = true; + o.subsection.addremove = true; + + so = o.subsection.option(form.Flag, 'enabled', _('Enable')); + so.rmempty = false; + + so = o.subsection.option(form.Value, 'matcher', _('Matcher')); + so.rmempty = false; + + so = o.subsection.option(form.DynamicList, 'nameserver', _('Nameserver')); + + s.tab('geox', _('GeoX Config')); + + o = s.taboption('geox', form.ListValue, 'geoip_format', _('GeoIP Format')); + o.value('dat', 'DAT'); + o.value('mmdb', 'MMDB'); + + o = s.taboption('geox', form.ListValue, 'geodata_loader', _('GeoData Loader')); + o.value('standard', _('Standard Loader')); + o.value('memconservative', _('Memory Conservative Loader')); + + o = s.taboption('geox', form.Value, 'geosite_url', _('GeoSite Url')); + o.rmempty = false; + + o = s.taboption('geox', form.Value, 'geoip_mmdb_url', _('GeoIP(MMDB) Url')); + o.rmempty = false; + + o = s.taboption('geox', form.Value, 'geoip_dat_url', _('GeoIP(DAT) Url')); + o.rmempty = false; + + o = s.taboption('geox', form.Value, 'geoip_asn_url', _('GeoIP(ASN) Url')); + o.rmempty = false; + + o = s.taboption('geox', form.Flag, 'geox_auto_update', _('GeoX Auto Update')); + o.rmempty = false; + + o = s.taboption('geox', form.Value, 'geox_update_interval', _('GeoX Update Interval'), _('Hour')); + o.datatype = 'integer'; + o.placeholder = '24'; + o.retain = true; + o.depends('geox_auto_update', '1'); + + s.tab('mixin_file_content', _('Mixin File Content'), _('Please go to the editor tab to edit the file for mixin')); + + o = s.taboption('mixin_file_content', form.HiddenValue, '_mixin_file_content'); + + return m.render(); + } +}); diff --git a/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/editor.js b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/editor.js new file mode 100644 index 000000000..f5ac8c47b --- /dev/null +++ b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/editor.js @@ -0,0 +1,63 @@ +'use strict'; +'require form'; +'require view'; +'require uci'; +'require fs'; +'require tools.mihomo as mihomo' + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('mihomo'), + mihomo.listProfiles(), + ]); + }, + render: function (data) { + const profiles = data[1]; + + let m, s, o; + + m = new form.Map('mihomo'); + + s = m.section(form.NamedSection, 'editor', 'editor'); + + o = s.option(form.ListValue, '_profile', _('Choose Profile')); + o.optional = true; + + for (const profile of profiles) { + o.value(mihomo.profilesDir + '/' + profile.name, _('File:') + profile.name); + } + o.value(mihomo.mixinFilePath, _('File for Mixin')); + o.value(mihomo.runProfilePath, _('Profile for Startup')); + o.value(mihomo.reservedIPNFT, _('File for Reserved IP')); + o.value(mihomo.reservedIP6NFT, _('File for Reserved IP6')); + + o.write = function (section_id, formvalue) { + return true; + }; + o.onchange = function (event, section_id, value) { + return L.resolveDefault(fs.read_direct(value), '').then(function (content) { + m.lookupOption('mihomo.editor._profile_content')[0].getUIElement('editor').setValue(content); + }); + }; + + o = s.option(form.TextValue, '_profile_content',); + o.rows = 25; + o.write = function (section_id, formvalue) { + const path = m.lookupOption('mihomo.editor._profile')[0].formvalue('editor'); + return fs.write(path, formvalue); + }; + o.remove = function (section_id) { + const path = m.lookupOption('mihomo.editor._profile')[0].formvalue('editor'); + return fs.write(path); + }; + + return m.render(); + }, + handleSaveApply: function (ev, mode) { + return this.handleSave(ev).finally(function() { + return mode === '0' ? mihomo.reload() : mihomo.restart(); + }); + }, + handleReset: null +}); diff --git a/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/log.js b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/log.js new file mode 100644 index 000000000..79a255c8a --- /dev/null +++ b/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/log.js @@ -0,0 +1,98 @@ +'use strict'; +'require form'; +'require view'; +'require uci'; +'require fs'; +'require poll'; +'require tools.mihomo as mihomo' + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('mihomo'), + mihomo.getAppLog(), + mihomo.getCoreLog() + ]); + }, + render: function (data) { + const appLog = data[1]; + const coreLog = data[2]; + + let m, s, o; + + m = new form.Map('mihomo'); + + s = m.section(form.NamedSection, 'log', 'log'); + + s.tab('app_log', _('App Log')); + + o = s.taboption('app_log', form.Button, 'clear_app_log'); + o.inputstyle = 'negative'; + o.inputtitle = _('Clear Log'); + o.onclick = function () { + m.lookupOption('mihomo.log._app_log')[0].getUIElement('log').setValue(''); + return mihomo.clearAppLog(); + }; + + o = s.taboption('app_log', form.TextValue, '_app_log'); + o.rows = 25; + o.wrap = false; + o.load = function (section_id) { + return appLog; + }; + o.write = function (section_id, formvalue) { + return true; + }; + poll.add(L.bind(function () { + const option = this; + return L.resolveDefault(mihomo.getAppLog()).then(function (log) { + option.getUIElement('log').setValue(log); + }); + }, o)); + + o = s.taboption('app_log', form.Button, 'scroll_app_log_to_bottom'); + o.inputtitle = _('Scroll To Bottom'); + o.onclick = function () { + const element = m.lookupOption('mihomo.log._app_log')[0].getUIElement('log').node.firstChild; + element.scrollTop = element.scrollHeight; + }; + + s.tab('core_log', _('Core Log')); + + o = s.taboption('core_log', form.Button, 'clear_core_log'); + o.inputstyle = 'negative'; + o.inputtitle = _('Clear Log'); + o.onclick = function () { + m.lookupOption('mihomo.log._core_log')[0].getUIElement('log').setValue(''); + return mihomo.clearCoreLog(); + }; + + o = s.taboption('core_log', form.TextValue, '_core_log'); + o.rows = 25; + o.wrap = false; + o.load = function (section_id) { + return coreLog; + }; + o.write = function (section_id, formvalue) { + return true; + }; + poll.add(L.bind(function () { + const option = this; + return L.resolveDefault(mihomo.getCoreLog()).then(function (log) { + option.getUIElement('log').setValue(log); + }); + }, o)); + + o = s.taboption('core_log', form.Button, 'scroll_core_log_to_bottom'); + o.inputtitle = _('Scroll To Bottom'); + o.onclick = function () { + const element = m.lookupOption('mihomo.log._core_log')[0].getUIElement('log').node.firstChild; + element.scrollTop = element.scrollHeight; + }; + + return m.render(); + }, + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-mihomo/po/zh_Hans/mihomo.po b/luci-app-mihomo/po/zh_Hans/mihomo.po new file mode 100644 index 000000000..7eab0a28f --- /dev/null +++ b/luci-app-mihomo/po/zh_Hans/mihomo.po @@ -0,0 +1,416 @@ +msgid "MihomoTProxy" +msgstr "MihomoTProxy" + +msgid "Transparent Proxy with Mihomo on OpenWrt." +msgstr "在 OpenWrt 上使用 Mihomo 进行透明代理。" + +msgid "How To Use" +msgstr "使用说明" + +msgid "Config" +msgstr "配置" + +msgid "Status" +msgstr "状态" + +msgid "App Version" +msgstr "插件版本" + +msgid "Core Version" +msgstr "核心版本" + +msgid "Core Status" +msgstr "核心状态" + +msgid "Running" +msgstr "运行中" + +msgid "Not Running" +msgstr "未在运行" + +msgid "Reload Service" +msgstr "重载服务" + +msgid "Restart Service" +msgstr "重启服务" + +msgid "Update Dashboard" +msgstr "更新面板" + +msgid "Open Dashboard" +msgstr "打开面板" + +msgid "Basic Config" +msgstr "基础配置" + +msgid "Enable" +msgstr "启用" + +msgid "Disable" +msgstr "禁用" + +msgid "Auto" +msgstr "自动" + +msgid "Mode" +msgstr "模式" + +msgid "Type" +msgstr "类型" + +msgid "Value" +msgstr "值" + +msgid "Scheduled Restart" +msgstr "定时重启" + +msgid "Cron Expression" +msgstr "Cron 表达式" + +msgid "Choose Profile" +msgstr "选择配置文件" + +msgid "File:" +msgstr "文件:" + +msgid "Subscription:" +msgstr "订阅:" + +msgid "Upload Profile" +msgstr "上传配置文件" + +msgid "Mixin" +msgstr "混入" + +msgid "Test Profile" +msgstr "检查配置文件" + +msgid "Fast Reload" +msgstr "快速重载" + +msgid "Proxy Config" +msgstr "代理配置" + +msgid "Transparent Proxy" +msgstr "透明代理" + +msgid "TCP Proxy Mode" +msgstr "TCP 代理模式" + +msgid "UDP Proxy Mode" +msgstr "UDP 代理模式" + +msgid "Redirect Mode" +msgstr "Redirect 模式" + +msgid "TPROXY Mode" +msgstr "TPROXY 模式" + +msgid "TUN Mode" +msgstr "TUN 模式" + +msgid "IPv4 DNS Hijack" +msgstr "IPv4 DNS 劫持" + +msgid "IPv6 DNS Hijack" +msgstr "IPv6 DNS 劫持" + +msgid "IPv4 Proxy" +msgstr "IPv4 代理" + +msgid "IPv6 Proxy" +msgstr "IPv6 代理" + +msgid "Router Proxy" +msgstr "路由器代理" + +msgid "Lan Proxy" +msgstr "局域网代理" + +msgid "Access Control" +msgstr "访问控制" + +msgid "All Mode" +msgstr "全部模式" + +msgid "Allow Mode" +msgstr "白名单模式" + +msgid "Block Mode" +msgstr "黑名单模式" + +msgid "Bypass" +msgstr "绕过" + +msgid "Bypass China Mainland IP" +msgstr "绕过中国大陆 IP" + +msgid "Destination TCP Port to Proxy" +msgstr "要代理的 TCP 目标端口" + +msgid "Destination UDP Port to Proxy" +msgstr "要代理的 UDP 目标端口" + +msgid "All Port" +msgstr "全部端口" + +msgid "Commonly Used Port" +msgstr "常用端口" + +msgid "Subscription Config" +msgstr "订阅配置" + +msgid "Subscription Name" +msgstr "订阅名称" + +msgid "Subscription Url" +msgstr "订阅链接" + +msgid "User Agent" +msgstr "用户代理(UA)" + +msgid "Mixin Config" +msgstr "混入配置" + +msgid "General Config" +msgstr "全局配置" + +msgid "Proxy Mode" +msgstr "代理模式" + +msgid "Global Mode" +msgstr "全局模式" + +msgid "Rule Mode" +msgstr "规则模式" + +msgid "Direct Mode" +msgstr "直连模式" + +msgid "Match Process" +msgstr "匹配进程" + +msgid "Outbound Interface" +msgstr "出站接口" + +msgid "TCP Keep Alive Idle" +msgstr "TCP Keep Alive 空闲" + +msgid "TCP Keep Alive Interval" +msgstr "TCP Keep Alive 间隔" + +msgid "Log Level" +msgstr "日志级别" + +msgid "External Control Config" +msgstr "外部控制配置" + +msgid "UI Name" +msgstr "UI 名称" + +msgid "UI Url" +msgstr "UI 下载地址" + +msgid "Service is not running." +msgstr "服务未在运行。" + +msgid "API Port" +msgstr "API 端口" + +msgid "API Secret" +msgstr "API 密钥" + +msgid "Save Proxy Selection" +msgstr "保存节点/策略组选择" + +msgid "Inbound Config" +msgstr "入站配置" + +msgid "Allow Lan" +msgstr "允许局域网访问" + +msgid "HTTP Port" +msgstr "HTTP 端口" + +msgid "SOCKS Port" +msgstr "SOCKS 端口" + +msgid "Mixed Port" +msgstr "混合端口" + +msgid "Redirect Port" +msgstr "Redirect 端口" + +msgid "TPROXY Port" +msgstr "TPROXY 端口" + +msgid "Authentication" +msgid "身份验证" + +msgid "Edit Authentications" +msgstr "编辑身份验证" + +msgid "username" +msgstr "用户名" + +msgid "password" +msgstr "密码" + +msgid "TUN Config" +msgstr "TUN 配置" + +msgid "Stack" +msgstr "栈" + +msgid "Device" +msgstr "设备" + +msgid "MTU" +msgstr "最大传输单元" + +msgid "GSO" +msgstr "通用分段卸载" + +msgid "GSO Max Size" +msgstr "分段最大长度" + +msgid "Endpoint Independent NAT" +msgstr "独立于端点的 NAT" + +msgid "DNS Config" +msgstr "DNS 配置" + +msgid "DNS Port" +msgstr "DNS 端口" + +msgid "DNS Mode" +msgstr "DNS 模式" + +msgid "Fake-IP Range" +msgstr "Fake-IP 范围" + +msgid "Overwrite Fake-IP Filter" +msgstr "覆盖 Fake-IP 过滤列表" + +msgid "Edit Fake-IP Filters" +msgstr "编辑 Fake-IP 过滤列表" + +msgid "Fake-IP Filter Mode" +msgstr "Fake-IP 过滤模式" + +msgid "Fake-IP Cache" +msgstr "Fake-IP 缓存" + +msgid "Respect Rules" +msgstr "遵循分流规则" + +msgid "Use System Hosts" +msgstr "使用系统的 Hosts" + +msgid "Use Hosts" +msgstr "使用 Hosts" + +msgid "Overwrite Hosts" +msgstr "覆盖 Hosts" + +msgid "Edit Hosts" +msgstr "编辑 Hosts" + +msgid "Domain Name" +msgstr "域名" + +msgid "Overwrite Nameserver" +msgstr "覆盖 DNS 服务器" + +msgid "Edit Nameservers" +msgstr "编辑 DNS 服务器" + +msgid "Nameserver" +msgstr "DNS 服务器" + +msgid "Overwrite Fallback Filter" +msgstr "覆盖 Fallback 过滤列表" + +msgid "Edit Fallback Filters" +msgstr "编辑 Fallback 过滤列表" + +msgid "Overwrite Nameserver Policy" +msgstr "覆盖 DNS 服务器查询策略" + +msgid "Edit Nameserver Policies" +msgstr "编辑 DNS 服务器查询策略" + +msgid "Matcher" +msgstr "匹配" + +msgid "GeoX Config" +msgstr "GeoX 配置" + +msgid "GeoIP Format" +msgstr "GeoIP 格式" + +msgid "GeoData Loader" +msgstr "GeoData 加载器" + +msgid "Standard Loader" +msgstr "标准加载器" + +msgid "Memory Conservative Loader" +msgstr "为内存受限设备优化的加载器" + +msgid "GeoSite Url" +msgstr "GeoSite 下载地址" + +msgid "GeoIP(MMDB) Url" +msgstr "GeoIP(MMDB) 下载地址" + +msgid "GeoIP(DAT) Url" +msgstr "GeoIP(DAT) 下载地址" + +msgid "GeoIP(ASN) Url" +msgstr "GeoIP(ASN) 下载地址" + +msgid "GeoX Auto Update" +msgstr "定时更新GeoX文件" + +msgid "GeoX Update Interval" +msgstr "GeoX 文件更新间隔" + +msgid "Hour" +msgstr "小时" + +msgid "Mixin File Content" +msgstr "混入文件内容" + +msgid "Please go to the editor tab to edit the file for mixin" +msgstr "请前往编辑器标签编辑用于混入的文件" + +msgid "Editor" +msgstr "编辑器" + +msgid "File for Mixin" +msgstr "用于混入的文件" + +msgid "Profile for Startup" +msgstr "用于启动的配置文件" + +msgid "File for Reserved IP" +msgstr "IPv4 保留地址" + +msgid "File for Reserved IP6" +msgstr "IPv6 保留地址" + +msgid "Log" +msgstr "日志" + +msgid "App Log" +msgstr "应用日志" + +msgid "Core Log" +msgstr "核心日志" + +msgid "Clear Log" +msgstr "清空日志" + +msgid "Scroll To Bottom" +msgstr "滚动到底部" diff --git a/luci-app-mihomo/root/usr/libexec/mihomo-call b/luci-app-mihomo/root/usr/libexec/mihomo-call new file mode 100755 index 000000000..bb8a17baf --- /dev/null +++ b/luci-app-mihomo/root/usr/libexec/mihomo-call @@ -0,0 +1,46 @@ +#!/bin/sh + +. $IPKG_INSTROOT/etc/mihomo/scripts/constants.sh + +action=$1 +shift + +case "$action" in + clear) + case "$1" in + app_log) + echo -n > "$RUN_APP_LOG_PATH" + ;; + core_log) + echo -n > "$RUN_CORE_LOG_PATH" + ;; + esac + ;; + load) + case "$1" in + profile) + yq -M -o json < "$RUN_PROFILE_PATH" + ;; + esac + ;; + service) + case "$1" in + reload) + /etc/init.d/mihomo reload + ;; + restart) + /etc/init.d/mihomo restart + ;; + esac + ;; + version) + case "$1" in + app) + opkg list-installed | grep "luci-app-mihomo" | cut -d " " -f 3 + ;; + core) + mihomo -v | grep "Mihomo" | cut -d " " -f 3 + ;; + esac + ;; +esac diff --git a/luci-app-mihomo/root/usr/share/luci/menu.d/luci-app-mihomo.json b/luci-app-mihomo/root/usr/share/luci/menu.d/luci-app-mihomo.json new file mode 100644 index 000000000..90c6b2ec3 --- /dev/null +++ b/luci-app-mihomo/root/usr/share/luci/menu.d/luci-app-mihomo.json @@ -0,0 +1,37 @@ +{ + "admin/services/mihomo": { + "title": "MihomoTProxy", + "action": { + "type": "alias", + "path": "admin/services/mihomo/config" + }, + "depends": { + "acl": [ "luci-app-mihomo" ], + "uci": { "mihomo": true } + } + }, + "admin/services/mihomo/config": { + "title": "Config", + "order": 10, + "action": { + "type": "view", + "path": "mihomo/config" + } + }, + "admin/services/mihomo/editor": { + "title": "Editor", + "order": 20, + "action": { + "type": "view", + "path": "mihomo/editor" + } + }, + "admin/services/mihomo/log": { + "title": "Log", + "order": 30, + "action": { + "type": "view", + "path": "mihomo/log" + } + } +} \ No newline at end of file diff --git a/luci-app-mihomo/root/usr/share/rpcd/acl.d/luci-app-mihomo.json b/luci-app-mihomo/root/usr/share/rpcd/acl.d/luci-app-mihomo.json new file mode 100644 index 000000000..d1de3b1c9 --- /dev/null +++ b/luci-app-mihomo/root/usr/share/rpcd/acl.d/luci-app-mihomo.json @@ -0,0 +1,32 @@ +{ + "luci-app-mihomo": { + "description": "Grant access to mihomo procedures", + "read": { + "uci": [ "mihomo" ], + "ubus": { + "service": [ "list" ] + }, + "file": { + "/etc/mihomo/profiles/*.yaml": ["read"], + "/etc/mihomo/profiles/*.yml": ["read"], + "/etc/mihomo/mixin.yaml": ["read"], + "/etc/mihomo/run/config.yaml": ["read"], + "/etc/mihomo/nftables/reserved_ip.nft": ["read"], + "/etc/mihomo/nftables/reserved_ip6.nft": ["read"], + "/etc/mihomo/run/*.log": ["read"], + "/usr/libexec/mihomo-call": ["exec"] + } + }, + "write": { + "uci": [ "mihomo" ], + "file": { + "/etc/mihomo/profiles/*.yaml": ["write"], + "/etc/mihomo/profiles/*.yml": ["write"], + "/etc/mihomo/mixin.yaml": ["write"], + "/etc/mihomo/run/config.yaml": ["write"], + "/etc/mihomo/nftables/reserved_ip.nft": ["write"], + "/etc/mihomo/nftables/reserved_ip6.nft": ["write"] + } + } + } +} \ No newline at end of file