mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 02:55:13 +08:00
646 lines
22 KiB
JavaScript
646 lines
22 KiB
JavaScript
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Copyright (C) 2022-2025 ImmortalWrt.org
|
|
*/
|
|
|
|
'use strict';
|
|
'require form';
|
|
'require poll';
|
|
'require uci';
|
|
'require ui';
|
|
'require view';
|
|
|
|
'require homeproxy-pro as hp';
|
|
'require tools.widgets as widgets';
|
|
|
|
/* Module-scoped guard for the view-level status poll: see the comment
|
|
* next to the registration site in render(). Without it, the section's
|
|
* render() leaks a fresh poll handler every time LuCI re-renders on
|
|
* a UCI commit, and the bar updates multiple times per tick. */
|
|
/* Built on the first render, so the poll closure captures that render's
|
|
* features.version. The "have we registered" state lives inside
|
|
* hp.statusPoller(), shared with the other view. */
|
|
let ensureStatusPoll = null;
|
|
|
|
const CBIGenValue = form.Value.extend({
|
|
__name__: 'CBI.GenValue',
|
|
|
|
renderWidget(/* ... */) {
|
|
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
|
|
|
if (!this.password)
|
|
node.classList.add('control-group');
|
|
|
|
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
|
class: 'cbi-button cbi-button-add',
|
|
title: _('Generate'),
|
|
click: ui.createHandlerFn(this, handleGenKey, this.hp_options || this.option)
|
|
}, [ _('Generate') ]));
|
|
|
|
return node;
|
|
}
|
|
});
|
|
|
|
function getServiceStatus() {
|
|
/* The fallback is `null`, not `{}`: that is what lets the caller tell
|
|
* "the status query did not answer" from "the service is not running".
|
|
* With the default `{}` the try/catch below swallowed the difference and
|
|
* reported a failed query as NOT RUNNING. */
|
|
return hp.rpcCall('list', ['homeproxy-pro'],
|
|
{ object: 'service', params: ['name'], expect: { '': {} }, fallback: null }).then((res) => {
|
|
if (res === null)
|
|
return null;
|
|
|
|
try {
|
|
return res['homeproxy-pro']['instances']['sing-box-s']['running'] === true;
|
|
} catch (e) {
|
|
/* It answered, and there is no such instance. */
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderStatus(isRunning, version) {
|
|
/* Same allow-list gate as client.js renderStatus(): the sing-box
|
|
* version string is interpolated into an innerHTML sink, so we
|
|
* force it through a strict regex before formatting. Anything that
|
|
* does not look like a version (alnum + . - +) becomes 'unknown'. */
|
|
let safeVersion = 'unknown';
|
|
if (typeof version === 'string' && /^[\w.\-+]+$/.test(version))
|
|
safeVersion = version;
|
|
|
|
const state = hp.statusLabel(isRunning);
|
|
return '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>'
|
|
.format(state.color, _('HomeProxy Server'), safeVersion, state.text);
|
|
}
|
|
|
|
function handleGenKey(option) {
|
|
let section_id = this.section.section;
|
|
let type = this.section.getOption('type')?.formvalue(section_id);
|
|
let widget = L.bind((option) => {
|
|
return this.map.findElement('id', 'widget.' + this.cbid(section_id).replace(/\.[^\.]+$/, '.') + option);
|
|
}, this);
|
|
|
|
if (typeof option === 'object') {
|
|
return hp.rpcCall('singbox_generator', [option.type, option.params],
|
|
{ params: ['type', 'params'], expect: { '': {} } }).then((res) => {
|
|
if (res.result)
|
|
option.callback.call(this, res.result).forEach(([k, v]) => {
|
|
widget(k).value = v ?? '';
|
|
});
|
|
else
|
|
ui.addNotification(null, E('p', _('Failed to generate %s, error: %s.').format(type, res.error)));
|
|
});
|
|
} else {
|
|
let password, required_method;
|
|
|
|
if (option === 'uuid')
|
|
required_method = 'uuid';
|
|
else if (type === 'shadowsocks')
|
|
required_method = this.section.getOption('shadowsocks_encrypt_method')?.formvalue(section_id);
|
|
|
|
switch (required_method) {
|
|
case 'none':
|
|
password = '';
|
|
break;
|
|
case 'uuid':
|
|
password = hp.generateRand('uuid');
|
|
break;
|
|
default:
|
|
password = hp.generateRand('hex', 16);
|
|
break;
|
|
}
|
|
/* AEAD */
|
|
((length) => {
|
|
if (length && length > 0)
|
|
password = hp.generateRand('base64', length);
|
|
})(hp.shadowsocks_encrypt_length[required_method]);
|
|
|
|
return widget(option).value = password;
|
|
}
|
|
}
|
|
|
|
return view.extend({
|
|
load() {
|
|
return Promise.all([
|
|
uci.load('homeproxy-pro'),
|
|
hp.getBuiltinFeatures()
|
|
]);
|
|
},
|
|
|
|
render(data) {
|
|
let m, s, o;
|
|
let features = data[1];
|
|
|
|
m = new form.Map('homeproxy-pro', _('HomeProxy Server'),
|
|
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
|
|
|
s = m.section(form.TypedSection);
|
|
s.render = function() {
|
|
/* The status bar is rendered into the page body by the
|
|
* view-level poll handler below. Registering poll.add()
|
|
* here would leak a fresh handler every time the user calls
|
|
* map.reset() (every UCI write re-renders this section). */
|
|
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
|
E('p', { id: 'service_status' }, _('Collecting data...'))
|
|
]);
|
|
}
|
|
|
|
/* View-level poll: registered exactly once per navigation. The
|
|
* getElementById lookup may return null when LuCI swaps the DOM
|
|
* between renders, so guard against it. */
|
|
if (ensureStatusPoll === null)
|
|
ensureStatusPoll = hp.statusPoller({
|
|
poll: poll,
|
|
read: getServiceStatus,
|
|
paint: function(res) {
|
|
let view = document.getElementById('service_status');
|
|
if (view)
|
|
view.innerHTML = renderStatus(res, features.version);
|
|
}
|
|
});
|
|
|
|
ensureStatusPoll();
|
|
|
|
s = m.section(form.NamedSection, 'server', 'homeproxy-pro', _('Global settings'));
|
|
|
|
o = s.option(form.Flag, 'enabled', _('Enable'));
|
|
o.rmempty = false;
|
|
|
|
s = m.section(form.GridSection, 'server', _('Server settings'));
|
|
s.addremove = true;
|
|
s.rowcolors = true;
|
|
s.sortable = true;
|
|
s.nodescriptions = true;
|
|
s.modaltitle = L.bind(hp.loadModalTitle, hp, _('Server'), _('Add a server'), 'homeproxy-pro');
|
|
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, 'homeproxy-pro');
|
|
/* Plain wrapper, not L.bind(): see hp.renderSectionAdd() for why the
|
|
* bound form hands a call site's button factory to the wrong argument. */
|
|
s.renderSectionAdd = function(extra_class) {
|
|
return hp.renderSectionAdd(s, extra_class);
|
|
};
|
|
|
|
o = s.option(form.Value, 'label', _('Label'));
|
|
o.load = L.bind(hp.loadDefaultLabel, this, 'homeproxy-pro');
|
|
o.validate = L.bind(hp.validateUniqueValue, this, 'homeproxy-pro', 'server', 'label');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'enabled', _('Enable'));
|
|
o.default = o.enabled;
|
|
o.rmempty = false;
|
|
o.editable = true;
|
|
|
|
o = s.option(form.Flag, 'firewall', _('Firewall'),
|
|
_('Allow access from the Internet.'));
|
|
o.editable = true;
|
|
|
|
o = hp.renderProtocolOptions(s, { features: features, side: 'server' });
|
|
|
|
o = s.option(form.Value, 'address', _('Listen address'));
|
|
o.placeholder = '::';
|
|
o.datatype = 'ipaddr';
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'port', _('Listen port'),
|
|
_('The port must be unique.'));
|
|
o.datatype = 'port';
|
|
o.validate = L.bind(hp.validateUniqueValue, this, 'homeproxy-pro', 'server', 'port');
|
|
|
|
o = s.option(form.Value, 'username', _('Username'));
|
|
o.depends('type', 'http');
|
|
o.depends('type', 'mixed');
|
|
o.depends('type', 'naive');
|
|
o.depends('type', 'socks');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(CBIGenValue, 'password', _('Password'));
|
|
o.password = true;
|
|
o.depends('type', 'anytls');
|
|
o.depends({'type': /^(http|mixed|naive|socks)$/, 'username': /[\s\S]/});
|
|
o.depends('type', 'hysteria2');
|
|
o.depends('type', 'shadowsocks');
|
|
o.depends('type', 'snell');
|
|
o.depends('type', 'trojan');
|
|
o.depends('type', 'tuic');
|
|
/* Same list, same reason as the node form's: an empty password on
|
|
* hysteria2/tuic makes InboundFactory.create() die() and the server side
|
|
* never generates. */
|
|
o.validate = hp.validatePassword([ 'anytls', 'http', 'hysteria2', 'mixed', 'naive', 'shadowsocks', 'snell', 'socks', 'trojan', 'tuic' ]);
|
|
o.modalonly = true;
|
|
|
|
/* AnyTLS config */
|
|
o = s.option(form.DynamicList, 'anytls_padding_scheme', _('Padding scheme'),
|
|
_('AnyTLS padding scheme in array.'));
|
|
o.depends('type', 'anytls');
|
|
o.modalonly = true;
|
|
|
|
/* Hysteria (2) config start */
|
|
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
|
o.value('udp');
|
|
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
|
|
o.value('wechat-video');
|
|
o.value('faketcp');
|
|
*/
|
|
o.default = 'udp';
|
|
o.depends('type', 'hysteria');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
hp.renderHysteriaBandwidth(s);
|
|
hp.renderHysteriaAuthObfs(s, { obfsPasswordWidget: CBIGenValue });
|
|
|
|
o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
|
|
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
|
|
o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''});
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'),
|
|
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
|
o.depends('type', 'hysteria2');
|
|
o.modalonly = true;
|
|
/* Hysteria (2) config end */
|
|
|
|
/* Snell config start */
|
|
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
|
|
_('The pre-shared key (password) above must be 12-255 bytes for v6.'));
|
|
/* {5, 6} is the INBOUND's valid set: sing-box 1.14 rejects version 4
|
|
on a snell inbound and accepts 5 and 6 (6 needs a >= 12 byte psk).
|
|
The node form offers {4, 6} for the same reason, mirrored. Do not
|
|
"unify" these two lists - see the plan's 2.11. */
|
|
o.value('5', _('v5'));
|
|
o.value('6', _('v6'));
|
|
o.default = '5';
|
|
o.depends('type', 'snell');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.ListValue, 'snell_obfs_mode', _('Obfuscation mode'),
|
|
_('HTTP obfuscation. v5 only.'));
|
|
o.value('', _('none'));
|
|
o.value('http', _('http'));
|
|
o.depends({'type': 'snell', 'snell_version': '5'});
|
|
o.modalonly = true;
|
|
/* Snell config end */
|
|
|
|
/* Shadowsocks config */
|
|
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
|
for (let i of hp.shadowsocks_encrypt_methods)
|
|
o.value(i);
|
|
/* The legacy stream ciphers are client-only; see homeproxy-pro.js's
|
|
shadowsocks_stream_methods for why, and the plan's 2.11. */
|
|
o.default = 'aes-128-gcm';
|
|
o.depends('type', 'shadowsocks');
|
|
o.modalonly = true;
|
|
|
|
/* Tuic config start */
|
|
hp.renderTuicOptions(s, { side: 'server', uuidWidget: CBIGenValue });
|
|
/* Tuic config end */
|
|
|
|
/* VLESS / VMess config start */
|
|
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
|
o.value('', _('None'));
|
|
o.value('xtls-rprx-vision');
|
|
o.depends('type', 'vless');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'vmess_alterid', _('Alter ID'),
|
|
_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
|
|
o.datatype = 'uinteger';
|
|
o.depends('type', 'vmess');
|
|
o.modalonly = true;
|
|
/* VMess config end */
|
|
|
|
/* Transport config start */
|
|
hp.renderTransportOptions(s, { features: features, side: 'server' });
|
|
/* Transport config end */
|
|
|
|
/* Mux config start */
|
|
hp.renderMuxOptions(s, { features: features });
|
|
/* Mux config end */
|
|
|
|
/* TLS config start */
|
|
hp.renderTlsOptions(s, {
|
|
side: 'server',
|
|
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'naive', 'trojan', 'tuic', 'vless', 'vmess' ],
|
|
tls_forced_types: [ 'hysteria', 'hysteria2', 'tuic' ]
|
|
});
|
|
|
|
if (features.with_acme) {
|
|
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
|
|
_('Use ACME TLS certificate issuer.'));
|
|
o.depends('tls', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains'));
|
|
o.datatype = 'hostname';
|
|
o.depends('tls_acme', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'),
|
|
_('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.'));
|
|
o.depends('tls_acme', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_email', _('Email'),
|
|
_('The email address to use when creating or selecting an existing ACME server account.'));
|
|
o.depends('tls_acme', '1');
|
|
o.validate = function(section_id, value) {
|
|
if (section_id) {
|
|
if (!value)
|
|
return _('Expecting: %s').format('non-empty value');
|
|
else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
|
|
return _('Expecting: %s').format('valid email address');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_key_type', _('ACME key type'),
|
|
_('Private key type for new certificates (1.14). Empty uses the default.'));
|
|
o.value('ed25519');
|
|
o.value('p256');
|
|
o.value('p384');
|
|
o.value('rsa2048');
|
|
o.value('rsa4096');
|
|
o.depends('tls_acme', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_profile', _('ACME profile'),
|
|
_('ACME profile for certificate issuance (1.14). Example: shortlived.'));
|
|
o.placeholder = 'shortlived';
|
|
o.depends('tls_acme', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.TextValue, 'tls_acme_account_key', _('ACME account key'),
|
|
_('PEM-encoded private key of an existing ACME account (1.14).'));
|
|
o.monospace = true;
|
|
o.rows = 3;
|
|
o.depends('tls_acme', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_provider', _('CA provider'),
|
|
_('The ACME CA provider to use.'));
|
|
o.value('letsencrypt', _('Let\'s Encrypt'));
|
|
o.value('zerossl', _('ZeroSSL'));
|
|
o.depends('tls_acme', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'tls_dns01_challenge', _('DNS01 challenge'))
|
|
o.depends('tls_acme', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
|
o.value('alidns', _('Alibaba Cloud DNS'));
|
|
o.value('cloudflare', _('Cloudflare'));
|
|
o.depends('tls_dns01_challenge', '1');
|
|
o.default = 'cloudflare';
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID'));
|
|
o.password = true;
|
|
o.depends('tls_dns01_provider', 'alidns');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret'));
|
|
o.password = true;
|
|
o.depends('tls_dns01_provider', 'alidns');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID'));
|
|
o.depends('tls_dns01_provider', 'alidns');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
|
o.password = true;
|
|
o.depends('tls_dns01_provider', 'cloudflare');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
|
|
o.depends('tls_dns01_challenge', '0');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
|
|
o.depends('tls_dns01_challenge', '0');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'),
|
|
_('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.'));
|
|
o.datatype = 'port';
|
|
o.depends('tls_dns01_challenge', '0');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'),
|
|
_('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.'));
|
|
o.datatype = 'port';
|
|
o.depends('tls_dns01_challenge', '0');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'),
|
|
_('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' +
|
|
'<br/>External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.'));
|
|
o.depends('tls_acme', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
|
|
o.password = true;
|
|
o.depends('tls_acme_external_account', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key'));
|
|
o.password = true;
|
|
o.depends('tls_acme_external_account', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
}
|
|
|
|
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
|
|
o.depends({'tls': '1', 'tls_acme': '0', 'type': /^(anytls|vless)$/});
|
|
o.depends({'tls': '1', 'tls_acme': null, 'type': /^(anytls|vless)$/});
|
|
o.modalonly = true;
|
|
|
|
o = s.option(CBIGenValue, 'tls_reality_private_key', _('REALITY private key'));
|
|
o.password = true;
|
|
o.hp_options = {
|
|
type: 'reality-keypair',
|
|
params: '',
|
|
callback: function(result) {
|
|
return [
|
|
[this.option, result.private_key],
|
|
['tls_reality_public_key', result.public_key]
|
|
]
|
|
}
|
|
}
|
|
o.depends('tls_reality', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_reality_public_key', _('REALITY public key'));
|
|
o.depends('tls_reality', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
|
|
o.depends('tls_reality', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
|
|
_('The maximum time difference between the server and the client.'));
|
|
o.depends('tls_reality', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
|
|
o.datatype = 'hostname';
|
|
o.depends('tls_reality', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
|
|
o.datatype = 'port';
|
|
o.depends('tls_reality', '1');
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
|
|
_('The server public key, in PEM format.'));
|
|
o.value('/etc/homeproxy-pro/certs/server_publickey.pem');
|
|
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
|
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
|
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
|
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
|
o.validate = hp.validateCertificatePath;
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
|
|
_('<strong>Save your configuration before uploading files!</strong>'));
|
|
o.inputstyle = 'action';
|
|
o.inputtitle = _('Upload...');
|
|
o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy-pro/certs/server_publickey.pem'});
|
|
o.onclick = L.bind(hp.uploadCertificate, hp, _('certificate'), 'server_publickey');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'tls_key_path', _('Key path'),
|
|
_('The server private key, in PEM format.'));
|
|
o.value('/etc/homeproxy-pro/certs/server_privatekey.pem');
|
|
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
|
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
|
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
|
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
|
o.validate = hp.validateCertificatePath;
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Button, '_upload_key', _('Upload key'),
|
|
_('<strong>Save your configuration before uploading files!</strong>'));
|
|
o.inputstyle = 'action';
|
|
o.inputtitle = _('Upload...');
|
|
o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy-pro/certs/server_privatekey.pem'});
|
|
o.onclick = L.bind(hp.uploadCertificate, hp, _('private key'), 'server_privatekey');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.TextValue, 'tls_ech_key', _('ECH key'));
|
|
o.placeholder = '-----BEGIN ECH KEYS-----\nACBE2+piYBLrOywCbRYU+ZpEkk8keeBlUXbKqLRmQ/68FwBL/g0ARwAAIAAgn8HI\n93RfdV/LaDk+LC9H4h+4WhVBFmWKdhiT3vvpGi8ACAABAAEAAQADABRvdXRlci1z\nbmkuYW55LmRvbWFpbgAA\n-----END ECH KEYS-----';
|
|
o.monospace = true;
|
|
o.cols = 30
|
|
o.rows = 3;
|
|
o.hp_options = {
|
|
type: 'ech-keypair',
|
|
params: '',
|
|
callback: function(result) {
|
|
return [
|
|
[this.option, result.ech_key],
|
|
['tls_ech_config', result.ech_cfg]
|
|
]
|
|
}
|
|
}
|
|
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
|
let node = form.TextValue.prototype.renderWidget.apply(this, arguments);
|
|
const cbid = this.cbid(section_id) + '._outer_sni';
|
|
|
|
node.appendChild(E('div', { 'class': 'control-group' }, [
|
|
E('input', {
|
|
id: cbid,
|
|
class: 'cbi-input-text',
|
|
style: 'width: 10em',
|
|
placeholder: 'outer-sni.any.domain'
|
|
}),
|
|
E('button', {
|
|
class: 'cbi-button cbi-button-add',
|
|
click: ui.createHandlerFn(this, () => {
|
|
this.hp_options.params = document.getElementById(cbid).value;
|
|
|
|
return handleGenKey.call(this, this.hp_options);
|
|
})
|
|
}, [ _('Generate') ])
|
|
]));
|
|
|
|
return node;
|
|
}
|
|
o.depends('tls', '1');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.TextValue, 'tls_ech_config', _('ECH config'));
|
|
o.placeholder = '-----BEGIN ECH CONFIGS-----\nAEv+DQBHAAAgACCfwcj3dF91X8toOT4sL0fiH7haFUEWZYp2GJPe++kaLwAIAAEA\nAQABAAMAFG91dGVyLXNuaS5hbnkuZG9tYWluAAA=\n-----END ECH CONFIGS-----';
|
|
o.monospace = true;
|
|
o.cols = 30
|
|
o.rows = 3;
|
|
o.depends('tls', '1');
|
|
o.modalonly = true;
|
|
/* TLS config end */
|
|
|
|
/* Extra settings start */
|
|
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
|
|
_('Enable tcp fast open for listener.'));
|
|
o.depends({'network': 'udp', '!reverse': true});
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
|
|
o.depends({'network': 'udp', '!reverse': true});
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
|
|
_('Enable UDP fragmentation.'));
|
|
o.depends({'network': 'tcp', '!reverse': true});
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Value, 'udp_timeout', _('UDP NAT expiration time'),
|
|
_('In seconds.'));
|
|
o.datatype = 'uinteger';
|
|
o.placeholder = '300';
|
|
o.depends({'network': 'tcp', '!reverse': true});
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.ListValue, 'network', _('Network'));
|
|
o.value('tcp', _('TCP'));
|
|
o.value('udp', _('UDP'));
|
|
o.value('', _('Both'));
|
|
o.depends('type', 'naive');
|
|
o.depends('type', 'shadowsocks');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
|
|
_('The network interface to bind to.'));
|
|
o.multiple = false;
|
|
o.noaliases = true;
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'reuse_addr', _('Reuse address'),
|
|
_('Reuse listener address.'));
|
|
o.modalonly = true;
|
|
/* Extra settings end */
|
|
|
|
return m.render();
|
|
}
|
|
});
|