From b83374b3401d33f1f1bc40bbb367991cc34cc918 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Mon, 4 Nov 2019 10:02:03 +0800 Subject: luci-app-shadowsocks-libev: port to client side Signed-off-by: Richard Yu --- .../luci-static/resources/shadowsocks-libev.js | 242 +++++++++++++++++++++ .../resources/view/shadowsocks-libev/instances.js | 162 ++++++++++++++ .../resources/view/shadowsocks-libev/rules.js | 123 +++++++++++ .../resources/view/shadowsocks-libev/servers.js | 37 ++++ 4 files changed, 564 insertions(+) create mode 100644 applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/shadowsocks-libev.js create mode 100644 applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/instances.js create mode 100644 applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/rules.js create mode 100644 applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/servers.js (limited to 'applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources') diff --git a/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/shadowsocks-libev.js b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/shadowsocks-libev.js new file mode 100644 index 000000000..3aaaa5012 --- /dev/null +++ b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/shadowsocks-libev.js @@ -0,0 +1,242 @@ +'use strict'; +'require uci'; +'require form'; +'require network'; + +var names_options_server = [ + 'server', + 'server_port', + 'method', + 'key', + 'password', + 'plugin', + 'plugin_opts', +]; + +var names_options_client = [ + 'server', + 'local_address', + 'local_port', +]; + +var names_options_common = [ + 'verbose', + 'ipv6_first', + 'fast_open', + 'no_delay', + 'reuse_port', + 'mode', + 'mtu', + 'timeout', + 'user', +]; + +var modes = [ + 'tcp_only', + 'tcp_and_udp', + 'udp_only', +]; + +var methods = [ + // aead + 'aes-128-gcm', + 'aes-192-gcm', + 'aes-256-gcm', + 'chacha20-ietf-poly1305', + 'xchacha20-ietf-poly1305', + // stream + 'table', + 'rc4', + 'rc4-md5', + 'aes-128-cfb', + 'aes-192-cfb', + 'aes-256-cfb', + 'aes-128-ctr', + 'aes-192-ctr', + 'aes-256-ctr', + 'bf-cfb', + 'camellia-128-cfb', + 'camellia-192-cfb', + 'camellia-256-cfb', + 'salsa20', + 'chacha20', + 'chacha20-ietf', +]; + +function ucival_to_bool(val) { + return val === 'true' || val === '1' || val === 'yes' || val === 'on'; +} + +return L.Class.extend({ + values_actions: function(o) { + o.value('bypass'); + o.value('forward'); + if (o.option !== 'dst_default') { + o.value('checkdst'); + } + }, + values_redir: function(o, xmode) { + uci.sections('shadowsocks-libev', 'ss_redir', function(sdata) { + var disabled = ucival_to_bool(sdata['disabled']), + sname = sdata['.name'], + mode = sdata['mode'] || 'tcp_only'; + if (!disabled && mode.indexOf(xmode) !== -1) { + o.value(sname, sname + ' - ' + mode); + } + }); + o.value('', ''); + o.default = ''; + }, + values_serverlist: function(o) { + uci.sections('shadowsocks-libev', 'server', function(sdata) { + var sname = sdata['.name'], + server = sdata['server'], + server_port = sdata['server_port']; + if (server && server_port) { + var disabled = ucival_to_bool(sdata['.disabled']) ? ' - disabled' : '', + desc = '%s - %s:%s%s'.format(sname, server, server_port, disabled); + o.value(sname, desc); + } + }); + }, + values_ipaddr: function(o, netDevs) { + netDevs.forEach(function(v) { + v.getIPAddrs().forEach(function(a) { + var host = a.split('/')[0]; + o.value(host, '%s (%s)'.format(host, v.getShortName())); + }); + }); + }, + options_client: function(s, tab, netDevs) { + var o = s.taboption(tab, form.ListValue, 'server', _('Remote server')); + this.values_serverlist(o); + o = s.taboption(tab, form.Value, 'local_address', _('Local address')); + o.datatype = 'ipaddr'; + o.placeholder = '0.0.0.0'; + this.values_ipaddr(o, netDevs); + o = s.taboption(tab, form.Value, 'local_port', _('Local port')); + o.datatype = 'port'; + }, + options_server: function(s, opts) { + var o, optfunc, + tab = opts && opts.tab || null; + + if (!tab) { + optfunc = function(/* ... */) { + var o = s.option.apply(s, arguments); + o.editable = true; + return o; + }; + } else { + optfunc = function(/* ... */) { + var o = s.taboption.apply(s, L.varargs(arguments, 0, tab)); + o.editable = true; + return o; + }; + } + + o = optfunc(form.Value, 'server', _('Server')); + o.datatype = 'host'; + o.size = 16; + + o = optfunc(form.Value, 'server_port', _('Server port')); + o.datatype = 'port'; + o.size = 5; + + o = optfunc(form.ListValue, 'method', _('Method')); + methods.forEach(function(m) { + o.value(m); + }); + + o = optfunc(form.Value, 'password', _('Password')); + o.password = true; + o.size = 12; + + o = optfunc(form.Value, 'key', _('Key (base64)')); + o.datatype = 'base64'; + o.password = true; + o.size = 12; + o.modalonly = true;; + + optfunc(form.Value, 'plugin', _('Plugin')).modalonly = true; + + optfunc(form.Value, 'plugin_opts', _('Plugin Options')).modalonly = true; + }, + options_common: function(s, tab) { + var o = s.taboption(tab, form.ListValue, 'mode', _('Mode of operation')); + modes.forEach(function(m) { + o.value(m); + }); + o.default = 'tcp_and_udp'; + o = s.taboption(tab, form.Value, 'mtu', _('MTU')); + o.datatype = 'uinteger'; + o = s.taboption(tab, form.Value, 'timeout', _('Timeout (sec)')); + o.datatype = 'uinteger'; + s.taboption(tab, form.Value, 'user', _('Run as')); + + s.taboption(tab, form.Flag, 'verbose', _('Verbose')); + s.taboption(tab, form.Flag, 'ipv6_first', _('IPv6 First'), _('Prefer IPv6 addresses when resolving names')); + s.taboption(tab, form.Flag, 'fast_open', _('Enable TCP Fast Open')); + s.taboption(tab, form.Flag, 'no_delay', _('Enable TCP_NODELAY')); + s.taboption(tab, form.Flag, 'reuse_port', _('Enable SO_REUSEPORT')); + }, + ucival_to_bool: function(val) { + return ucival_to_bool(val); + }, + cfgvalue_overview: function(sdata) { + var stype = sdata['.type'], + lines = []; + + if (stype === 'ss_server') { + this.cfgvalue_overview_(sdata, lines, names_options_server); + this.cfgvalue_overview_(sdata, lines, names_options_common); + this.cfgvalue_overview_(sdata, lines, ['bind_address']); + } else if (stype === 'ss_local' || stype === 'ss_redir' || stype === 'ss_tunnel') { + this.cfgvalue_overview_(sdata, lines, names_options_client); + if (stype === 'ss_tunnel') { + this.cfgvalue_overview_(sdata, lines, ['tunnel_address']); + } + this.cfgvalue_overview_(sdata, lines, names_options_common); + } else { + return []; + } + + return lines; + }, + cfgvalue_overview_: function(sdata, lines, names) { + names.forEach(function(n) { + var v = sdata[n]; + if (v) { + if (n === 'key' || n === 'password') { + v = _(''); + } + var fv = E('var', [v]); + if (sdata['.type'] !== 'ss_server' && n === 'server') { + fv = E('a', { + class: 'label', + href: L.url('admin/services/shadowsocks-libev/servers') + '#edit=' + v, + target: '_blank', + rel: 'noopener' + }, fv); + } + lines.push(n + ': ', fv, E('br')); + } + }); + }, + option_install_package: function(s, tab) { + var bin = s.sectiontype.replace('_', '-'), + opkg_package = 'shadowsocks-libev-' + bin, o; + if (tab) { + o = s.taboption(tab, form.Button, '_install'); + } else { + o = s.option(form.Button, '_install'); + } + o.title = _('Package is not installed'); + o.inputtitle = _('Install package ' + opkg_package); + o.inputstyle = 'apply'; + o.onclick = function() { + window.open(L.url('admin/system/opkg') + + '?query=' + opkg_package, '_blank', 'noopener'); + }; + } +}); diff --git a/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/instances.js b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/instances.js new file mode 100644 index 000000000..27a2b950c --- /dev/null +++ b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/instances.js @@ -0,0 +1,162 @@ +'use strict'; +'require form'; +'require uci'; +'require fs'; +'require network'; +'require rpc'; +'require shadowsocks-libev as ss'; + +var conf = 'shadowsocks-libev'; +var cfgtypes = ['ss_local', 'ss_redir', 'ss_server', 'ss_tunnel']; + +var callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: [ 'name' ], + expect: { '': {} } +}); + +return L.view.extend({ + render: function(stats) { + var m, s, o; + + m = new form.Map(conf, + _('Local Instances'), + _('Instances of shadowsocks-libev components, e.g. ss-local, \ + ss-redir, ss-tunnel, ss-server, etc. To enable an instance it \ + is required to enable both the instance itself and the remote \ + server it refers to.')); + + s = m.section(form.GridSection); + s.addremove = true; + s.cfgsections = function() { + return this.map.data.sections(this.map.config) + .filter(function(s) { return cfgtypes.indexOf(s['.type']) !== -1; }) + .map(function(s) { return s['.name']; }); + }; + s.sectiontitle = function(section_id) { + var s = uci.get(conf, section_id); + return (s ? s['.type'] + '.' : '') + section_id; + }; + s.renderSectionAdd = function(extra_class) { + var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments), + optionEl = [E('option', { value: '_dummy' }, [_('-- instance type --')])]; + cfgtypes.forEach(function(t) { + optionEl.push(E('option', { value: t }, [t.replace('_', '-')])); + }); + var selectEl = E('select', { + class: 'cbi-input-select', + change: function(ev) { + ev.target.parentElement.nextElementSibling.nextElementSibling + .toggleAttribute('disabled', ev.target.value === '_dummy'); + } + }, optionEl); + el.lastElementChild.setAttribute('disabled', ''); + el.prepend(E('div', {}, selectEl)); + return el; + }; + s.handleAdd = function(ev, name) { + var selectEl = ev.target.parentElement.firstElementChild.firstElementChild, + type = selectEl.value; + this.sectiontype = type; + var promise = form.GridSection.prototype.handleAdd.apply(this, arguments); + this.sectiontype = undefined; + return promise; + }; + s.addModalOptions = function(s, section_id, ev) { + var sdata = uci.get(conf, section_id), + stype = sdata ? sdata['.type'] : null; + if (stype) { + s.sectiontype = stype; + return Promise.all([ + L.resolveDefault(fs.stat('/usr/bin/' + stype.replace('_', '-')), null), + network.getDevices() + ]).then(L.bind(function(res) { + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + s.taboption('general', form.Flag, 'disabled', _('Disable')); + if (!res[0]) { + ss.option_install_package(s, 'general'); + } + ss.options_common(s, 'advanced'); + + if (stype === 'ss_server') { + ss.options_server(s, { tab: 'general' }); + o = s.taboption('general', form.Value, 'bind_address', + _('Bind address'), + _('The address ss-server will initiate connection from')); + o.datatype = 'ipaddr'; + o.placeholder = '0.0.0.0'; + ss.values_ipaddr(o, res[1]); + } else { + ss.options_client(s, 'general', res[1]); + if (stype === 'ss_tunnel') { + o = s.taboption('general', form.Value, 'tunnel_address', + _('Tunnel address'), + _('The address ss-tunnel will forward traffic to')); + o.datatype = 'hostport'; + } + } + }, this)); + } + }; + + o = s.option(form.DummyValue, 'overview', _('Overview')); + o.modalonly = false; + o.editable = true; + o.rawhtml = true; + o.renderWidget = function(section_id, option_index, cfgvalue) { + var sdata = uci.get(conf, section_id); + if (sdata) { + return form.DummyValue.prototype.renderWidget.call(this, section_id, option_index, ss.cfgvalue_overview(sdata)); + } + return null; + }; + + o = s.option(form.DummyValue, 'running', _('Running')); + o.modalonly = false; + o.editable = true; + o.default = ''; + + o = s.option(form.Button, 'disabled', _('Enable/Disable')); + o.modalonly = false; + o.editable = true; + o.inputtitle = function(section_id) { + var s = uci.get(conf, section_id); + if (ss.ucival_to_bool(s['disabled'])) { + this.inputstyle = 'reset'; + return _('Disabled'); + } + this.inputstyle = 'save'; + return _('Enabled'); + } + o.onclick = function(ev) { + var inputEl = ev.target.parentElement.nextElementSibling; + inputEl.value = ss.ucival_to_bool(inputEl.value) ? '0' : '1'; + return this.map.save(); + } + + return m.render().finally(function() { + L.Poll.add(function() { + return L.resolveDefault(callServiceList(conf), {}) + .then(function(res) { + var instances = null; + try { + instances = res[conf]['instances']; + } catch (e) {} + if (!instances) return; + uci.sections(conf) + .filter(function(s) { return cfgtypes.indexOf(s['.type']) !== -1; }) + .forEach(function(s) { + var el = document.getElementById('cbi-shadowsocks-libev-' + s['.name'] + '-running'); + if (el) { + var name = s['.type'] + '.' + s['.name'], + running = instances.hasOwnProperty(name)? instances[name].running : false; + el.innerText = running ? 'yes' : 'no'; + } + }); + }); + }); + }); + }, +}); diff --git a/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/rules.js b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/rules.js new file mode 100644 index 000000000..798237adb --- /dev/null +++ b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/rules.js @@ -0,0 +1,123 @@ +'use strict'; +'require uci'; +'require fs'; +'require form'; +'require tools.widgets as widgets'; +'require shadowsocks-libev as ss'; + +var conf = 'shadowsocks-libev'; + +function src_dst_option(s /*, ... */) { + var o = s.taboption.apply(s, L.varargs(arguments, 1)); + o.datatype = 'or(ipaddr,cidr)'; +} + +return L.view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(fs.stat('/usr/lib/iptables/libxt_recent.so'), {}), + L.resolveDefault(fs.stat('/usr/bin/ss-rules'), null), + uci.load(conf).then(function() { + if (!uci.get_first(conf, 'ss_rules')) { + uci.set(conf, uci.add(conf, 'ss_rules', 'ss_rules'), 'disabled', '1'); + } + }) + ]); + }, + render: function(stats) { + var m, s, o; + + m = new form.Map(conf, _('Redir Rules'), + _('On this page you can configure how traffics are to be \ + forwarded to ss-redir instances. \ + If enabled, packets will first have their src ip addresses checked \ + against Src ip/net bypass, Src ip/net forward, \ + Src ip/net checkdst and if none matches Src default \ + will give the default action to be taken. \ + If the prior check results in action checkdst, packets will continue \ + to have their dst addresses checked.')); + + s = m.section(form.NamedSection, 'ss_rules', 'ss_rules'); + s.tab('general', _('General Settings')); + s.tab('src', _('Source Settings')); + s.tab('dst', _('Destination Settings')); + + s.taboption('general', form.Flag, 'disabled', _('Disable')); + if (!stats[1]) { + ss.option_install_package(s, 'general'); + } + + o = s.taboption('general', form.ListValue, 'redir_tcp', + _('ss-redir for TCP')); + ss.values_redir(o, 'tcp'); + o = s.taboption('general', form.ListValue, 'redir_udp', + _('ss-redir for UDP')); + ss.values_redir(o, 'udp'); + + o = s.taboption('general', form.ListValue, 'local_default', + _('Local-out default'), + _('Default action for locally generated TCP packets')); + ss.values_actions(o); + o = s.taboption('general', widgets.DeviceSelect, 'ifnames', + _('Ingress interfaces'), + _('Only apply rules on packets from these network interfaces')); + o.multiple = true; + o.noaliases = true; + o.noinactive = true; + s.taboption('general', form.Value, 'ipt_args', + _('Extra arguments'), + _('Passes additional arguments to iptables. Use with care!')); + + src_dst_option(s, 'src', form.DynamicList, 'src_ips_bypass', + _('Src ip/net bypass'), + _('Bypass ss-redir for packets with src address in this list')); + src_dst_option(s, 'src', form.DynamicList, 'src_ips_forward', + _('Src ip/net forward'), + _('Forward through ss-redir for packets with src address in this list')); + src_dst_option(s, 'src', form.DynamicList, 'src_ips_checkdst', + _('Src ip/net checkdst'), + _('Continue to have dst address checked for packets with src address in this list')); + o = s.taboption('src', form.ListValue, 'src_default', + _('Src default'), + _('Default action for packets whose src address do not match any of the src ip/net list')); + ss.values_actions(o); + + src_dst_option(s, 'dst', form.DynamicList, 'dst_ips_bypass', + _('Dst ip/net bypass'), + _('Bypass ss-redir for packets with dst address in this list')); + src_dst_option(s, 'dst', form.DynamicList, 'dst_ips_forward', + _('Dst ip/net forward'), + _('Forward through ss-redir for packets with dst address in this list')); + + var dir = '/etc/shadowsocks-libev'; + o = s.taboption('dst', form.FileUpload, 'dst_ips_bypass_file', + _('Dst ip/net bypass file'), + _('File containing ip/net for the purposes as with Dst ip/net bypass')); + o.root_directory = dir; + o = s.taboption('dst', form.FileUpload, 'dst_ips_forward_file', + _('Dst ip/net forward file'), + _('File containing ip/net for the purposes as with Dst ip/net forward')); + o.root_directory = dir; + o = s.taboption('dst', form.ListValue, 'dst_default', + _('Dst default'), + _('Default action for packets whose dst address do not match any of the dst ip list')); + ss.values_actions(o); + + if (stats[0].type === 'file') { + o = s.taboption('dst', form.Flag, 'dst_forward_recentrst'); + } else { + uci.set(conf, 'ss_rules', 'dst_forward_recentrst', '0'); + o = s.taboption('dst', form.Button, '_install'); + o.inputtitle = _('Install package iptables-mod-conntrack-extra'); + o.inputstyle = 'apply'; + o.onclick = function() { + window.open(L.url('admin/system/opkg') + + '?query=iptables-mod-conntrack-extra', '_blank', 'noopener'); + } + } + o.title = _('Forward recentrst'); + o.description = _('Forward those packets whose dst have recently sent to us multiple tcp-rst'); + + return m.render(); + }, +}); diff --git a/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/servers.js b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/servers.js new file mode 100644 index 000000000..d46bfb0aa --- /dev/null +++ b/applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/servers.js @@ -0,0 +1,37 @@ +'use strict'; +'require form'; +'require shadowsocks-libev as ss'; + +function startsWith(str, search) { + return str.substring(0, search.length) === search; +} + +return L.view.extend({ + render: function() { + var m, s, o; + + m = new form.Map('shadowsocks-libev', _('Remote Servers'), + _('Definition of remote shadowsocks servers. \ + Disable any of them will also disable instances referring to it.')); + + s = m.section(form.GridSection, 'server'); + s.addremove = true; + + o = s.option(form.Flag, 'disabled', _('Disable')); + o.editable = true; + + ss.options_server(s); + + return m.render(); + }, + addFooter: function() { + var p = '#edit='; + if (startsWith(location.hash, p)) { + var section_id = location.hash.substring(p.length); + var editBtn = document.querySelector('#cbi-shadowsocks-libev-' + section_id + ' button.cbi-button-edit'); + if (editBtn) + editBtn.click(); + } + return this.super('addFooter', arguments); + } +}); -- cgit v1.2.3