'use strict'; 'require form'; 'require poll'; 'require uci'; 'require ui'; 'require view'; 'require fchomo as hm'; return view.extend({ load() { return Promise.all([ uci.load('fchomo'), hm.getFeatures() ]); }, render(data) { const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'); const features = data[1]; let m, s, o; m = new form.Map('fchomo', _('Mihomo server'), _('When used as a server, HomeProxy is a better choice.')); s = m.section(form.TypedSection); s.render = function () { poll.add(function() { return hm.getServiceStatus('mihomo-s').then((isRunning) => { hm.updateStatus(document.getElementById('_server_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-s', true); }); }); return E('div', { class: 'cbi-section' }, [ E('p', [ hm.renderStatus('_server_bar', false, 'mihomo-s', true) ]) ]); } s = m.section(form.NamedSection, 'routing', 'fchomo', null); /* Server switch */ o = s.option(form.Button, '_reload_server', _('Quick Reload')); o.inputtitle = _('Reload'); o.inputstyle = 'apply'; o.onclick = L.bind(hm.handleReload, o, 'mihomo-s'); o = s.option(form.Flag, 'server_enabled', _('Enable')); o.default = o.disabled; o = s.option(form.Flag, 'server_auto_firewall', _('Auto configure firewall')); o.default = o.disabled; /* Server settings START */ s = m.section(hm.GridSection, 'server', null); s.addremove = true; s.rowcolors = true; s.sortable = true; s.nodescriptions = true; s.hm_modaltitle = [ _('Server'), _('Add a server') ]; s.hm_prefmt = hm.glossary[s.sectiontype].prefmt; s.hm_field = hm.glossary[s.sectiontype].field; s.hm_lowcase_only = false; s.tab('field_general', _('General fields')); s.tab('field_tls', _('TLS fields')); s.tab('field_transport', _('Transport fields')); s.tab('field_multiplex', _('Multiplex fields')); s.tab('field_listen', _('Listen fields')); /* General fields */ o = s.taboption('field_general', form.Value, 'label', _('Label')); o.load = L.bind(hm.loadDefaultLabel, o); o.validate = L.bind(hm.validateUniqueValue, o); o.modalonly = true; o = s.taboption('field_general', form.Flag, 'enabled', _('Enable')); o.default = o.enabled; o.editable = true; o = s.taboption('field_general', form.ListValue, 'type', _('Type')); o.default = hm.inbound_type[0][0]; hm.inbound_type.forEach((res) => { o.value.apply(o, res); }) o = s.taboption('field_general', form.Value, 'listen', _('Listen address')); o.datatype = 'ipaddr'; o.placeholder = '::'; o.modalonly = true; o = s.taboption('field_general', form.Value, 'port', _('Listen port') + ' / ' + _('Ports pool')); o.datatype = 'or(port, portrange)'; //o.placeholder = '1080,2079-2080,3080'; // Incompatible with firewall o.rmempty = false; //o.validate = L.bind(hm.validateCommonPort, o); // Incompatible with firewall // dev: Features under development // rule // proxy /* HTTP / SOCKS fields */ /* hm.validateAuth */ o = s.taboption('field_general', form.Value, 'username', _('Username')); o.validate = L.bind(hm.validateAuthUsername, o); o.depends({type: /^(http|socks|mixed|trojan|anytls|hysteria2)$/}); o.modalonly = true; o = s.taboption('field_general', hm.GenValue, 'password', _('Password')); o.password = true; o.validate = L.bind(hm.validateAuthPassword, o); o.rmempty = false; o.depends({type: /^(http|socks|mixed|trojan|anytls|hysteria2)$/, username: /.+/}); o.depends({type: /^(tuic)$/, uuid: /.+/}); o.modalonly = true; /* Hysteria2 fields */ o = s.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'), _('In Mbps.')); o.datatype = 'uinteger'; o.depends('type', 'hysteria2'); o.modalonly = true; o = s.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'), _('In Mbps.')); o.datatype = 'uinteger'; o.depends('type', 'hysteria2'); o.modalonly = true; o = s.taboption('field_general', form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'), _('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.')); o.default = o.disabled; o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''}); o.modalonly = true; o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); o.value('', _('Disable')); o.value('salamander', _('Salamander')); o.depends('type', 'hysteria2'); o.modalonly = true; o = s.taboption('field_general', hm.GenValue, 'hysteria_obfs_password', _('Obfuscate password'), _('Enabling obfuscation will make the server incompatible with standard QUIC connections, losing the ability to masquerade with HTTP/3.')); o.password = true; o.rmempty = false; o.depends('type', 'hysteria'); o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/}); o.modalonly = true; o = s.taboption('field_general', form.Value, 'hysteria_masquerade', _('Masquerade'), _('HTTP3 server behavior when authentication fails.
A 404 page will be returned if empty.')); o.placeholder = 'file:///var/www or http://127.0.0.1:8080' o.depends('type', 'hysteria2'); o.modalonly = true; /* Shadowsocks fields */ o = s.taboption('field_general', form.ListValue, 'shadowsocks_chipher', _('Chipher')); o.default = hm.shadowsocks_cipher_methods[1][0]; hm.shadowsocks_cipher_methods.forEach((res) => { o.value.apply(o, res); }) o.depends('type', 'shadowsocks'); o.modalonly = true; o = s.taboption('field_general', hm.GenValue, 'shadowsocks_password', _('Password')); o.password = true; o.validate = function(section_id, value) { const encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id); return hm.validateShadowsocksPassword.call(this, encmode, section_id, value); } o.depends({type: 'shadowsocks', shadowsocks_chipher: /.+/}); o.modalonly = true; /* Tuic fields */ o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID')); o.rmempty = false; o.validate = L.bind(hm.validateUUID, o); o.depends('type', 'tuic'); o.modalonly = true; o = s.taboption('field_general', form.ListValue, 'tuic_congestion_controller', _('Congestion controller'), _('QUIC congestion controller.')); o.default = 'cubic'; o.value('cubic', _('cubic')); o.value('new_reno', _('new_reno')); o.value('bbr', _('bbr')); o.depends('type', 'tuic'); o.modalonly = true; o = s.taboption('field_general', form.Value, 'tuic_max_udp_relay_packet_size', _('Max UDP relay packet size')); o.datatype = 'uinteger'; o.default = '1500'; o.depends('type', 'tuic'); o.modalonly = true; o = s.taboption('field_general', form.Value, 'tuic_max_idle_time', _('Idle timeout'), _('In seconds.')); o.default = '15000'; o.validate = L.bind(hm.validateTimeDuration, o); o.depends('type', 'tuic'); o.modalonly = true; o = s.taboption('field_general', form.Value, 'tuic_authentication_timeout', _('Auth timeout'), _('In seconds.')); o.default = '1000'; o.validate = L.bind(hm.validateTimeDuration, o); o.depends('type', 'tuic'); o.modalonly = true; /* Trojan fields */ o = s.taboption('field_general', form.Flag, 'trojan_ss_enabled', _('Shadowsocks encrypt')); o.default = o.disabled; o.depends('type', 'trojan'); o.modalonly = true; o = s.taboption('field_general', form.ListValue, 'trojan_ss_chipher', _('Shadowsocks chipher')); o.default = hm.trojan_cipher_methods[0][0]; hm.trojan_cipher_methods.forEach((res) => { o.value.apply(o, res); }) o.depends({type: 'trojan', trojan_ss_enabled: '1'}); o.modalonly = true; o = s.taboption('field_general', hm.GenValue, 'trojan_ss_password', _('Shadowsocks password')); o.password = true; o.validate = function(section_id, value) { const encmode = this.section.getOption('trojan_ss_chipher').formvalue(section_id); return hm.validateShadowsocksPassword.call(this, encmode, section_id, value); } o.depends({type: 'trojan', trojan_ss_enabled: '1'}); o.modalonly = true; /* AnyTLS fields */ o = s.taboption('field_general', form.TextValue, 'anytls_padding_scheme', _('Padding scheme')); o.depends('type', 'anytls'); o.modalonly = true; /* VMess / VLESS fields */ o = s.taboption('field_general', hm.GenValue, 'vmess_uuid', _('UUID')); o.rmempty = false; o.validate = L.bind(hm.validateUUID, o); o.depends({type: /^(vmess|vless)$/}); o.modalonly = true; o = s.taboption('field_general', form.ListValue, 'vless_flow', _('Flow')); o.default = hm.vless_flow[0][0]; hm.vless_flow.forEach((res) => { o.value.apply(o, res); }) o.depends('type', 'vless'); o.modalonly = true; o = s.taboption('field_general', form.Value, 'vmess_alterid', _('Alter ID'), _('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.')); o.datatype = 'uinteger'; o.placeholder = '0'; o.depends('type', 'vmess'); o.modalonly = true; /* Extra fields */ o = s.taboption('field_general', form.Flag, 'udp', _('UDP')); o.default = o.disabled; o.depends({type: /^(socks|mixed|shadowsocks)$/}); o.modalonly = true; /* TLS fields */ o = s.taboption('field_general', form.Flag, 'tls', _('TLS')); o.default = o.disabled; o.validate = function(section_id, value) { const type = this.section.getOption('type').formvalue(section_id); let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); let tls_alpn = this.section.getUIElement(section_id, 'tls_alpn'); let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input'); // Force enabled if (['vless', 'trojan', 'anytls', 'tuic', 'hysteria2'].includes(type)) { tls.checked = true; tls.disabled = true; if (['tuic', 'hysteria2'].includes(type) && !`${tls_alpn.getValue()}`) tls_alpn.setValue('h3'); } else { tls.disabled = null; } // Force disabled if (!['vmess', 'vless', 'trojan'].includes(type)) { tls_reality.checked = null; tls_reality.disabled = true; } else { tls_reality.disabled = null; } return true; } o.depends({type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2)$/}); o.modalonly = true; o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'), _('List of supported application level protocols, in order of preference.')); o.depends('tls', '1'); o.modalonly = true; o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'), _('The server public key, in PEM format.')); o.value('/etc/fchomo/certs/server_publickey.pem'); o.depends({tls: '1', tls_reality: '0'}); o.rmempty = false; o.modalonly = true; o = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'), _('Save your configuration before uploading files!')); o.inputstyle = 'action'; o.inputtitle = _('Upload...'); o.depends({tls: '1', tls_cert_path: '/etc/fchomo/certs/server_publickey.pem'}); o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'server_publickey'); o.modalonly = true; o = s.taboption('field_tls', form.Value, 'tls_key_path', _('Key path'), _('The server private key, in PEM format.')); o.value('/etc/fchomo/certs/server_privatekey.pem'); o.rmempty = false; o.depends({tls: '1', tls_cert_path: /.+/}); o.modalonly = true; o = s.taboption('field_tls', form.Button, '_upload_key', _('Upload key'), _('Save your configuration before uploading files!')); o.inputstyle = 'action'; o.inputtitle = _('Upload...'); o.depends({tls: '1', tls_key_path: '/etc/fchomo/certs/server_privatekey.pem'}); o.onclick = L.bind(hm.uploadCertificate, o, _('private key'), 'server_privatekey'); o.modalonly = true; // uTLS fields o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY')); o.default = o.disabled; o.depends('tls', '1'); o.modalonly = true; o = s.taboption('field_tls', form.Value, 'tls_reality_dest', _('REALITY handshake server')); o.datatype = 'hostport'; o.placeholder = 'cloud.tencent.com:443'; o.rmempty = false; o.depends('tls_reality', '1'); o.modalonly = true; o = s.taboption('field_tls', hm.GenValue, 'tls_reality_private_key', _('REALITY private key')); const tls_reality_public_key = 'tls_reality_public_key'; o.hm_asymmetric = { type: 'reality-keypair', result: { private_key: o.option, public_key: tls_reality_public_key } }; o.password = true; o.rmempty = false; o.depends('tls_reality', '1'); o.modalonly = true; o = s.taboption('field_tls', form.Value, tls_reality_public_key, _('REALITY public key')); o.depends('tls_reality', '1'); o.modalonly = true; o = s.taboption('field_tls', form.DynamicList, 'tls_reality_short_id', _('REALITY short ID')); //o.value('', '""'); o.rmempty = false; o.depends('tls_reality', '1'); o.modalonly = true; o = s.taboption('field_tls', form.DynamicList, 'tls_reality_server_names', _('REALITY certificate issued to')); o.datatype = 'list(hostname)'; o.placeholder = 'cloud.tencent.com'; o.rmempty = false; o.depends('tls_reality', '1'); o.modalonly = true; /* Transport fields */ o = s.taboption('field_general', form.Flag, 'transport_enabled', _('Transport')); o.default = o.disabled; o.depends({type: /^(vmess|vless|trojan)$/}); o.modalonly = true; o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type')); o.value('grpc', _('gRPC')); o.value('ws', _('WebSocket')); o.validate = function(section_id, value) { const type = this.section.getOption('type').formvalue(section_id); switch (type) { case 'vmess': case 'vless': if (!['http', 'h2', 'grpc', 'ws'].includes(value)) return _('Expecting: only support %s.').format(_('HTTP') + ' / ' + _('HTTPUpgrade') + ' / ' + _('gRPC') + ' / ' + _('WebSocket')); break; case 'trojan': if (!['grpc', 'ws'].includes(value)) return _('Expecting: only support %s.').format(_('gRPC') + ' / ' + _('WebSocket')); break; default: break; } return true; } o.depends('transport_enabled', '1'); o.modalonly = true; o = s.taboption('field_transport', form.Value, 'transport_path', _('Request path')); o.placeholder = '/'; o.default = '/'; o.rmempty = false; o.depends({transport_enabled: '1', transport_type: 'ws'}); o.modalonly = true; o = s.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name')); o.placeholder = 'GunService'; o.rmempty = false; o.depends({transport_enabled: '1', transport_type: 'grpc'}); o.modalonly = true; /* Server settings END */ return m.render(); } });