mirror of
https://github.com/kenzok8/small-package
synced 2025-01-07 09:16:47 +08:00
update 2024-11-03 20:36:08
This commit is contained in:
parent
53f4143f61
commit
63a1d9df6d
@ -15,7 +15,7 @@ mkdir -p "$PKG_BUILD_BIN"
|
||||
curl -L "https://github.com/jqlang/jq/releases/download/jq-${JQVERSION}/jq-${OS}-${ARCH}" -o "$PKG_BUILD_BIN"/jq
|
||||
chmod +x "$PKG_BUILD_BIN"/jq
|
||||
latest="$(curl -L https://api.github.com/repos/kpym/gm/releases/latest | jq -rc '.tag_name' 2>/dev/null)"
|
||||
curl -L "https://github.com/kpym/gm/releases/download/${latest}/gm_${latest#v}_Linux_64bit.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN"
|
||||
curl -L "https://github.com/kpym/gm/releases/download/${latest}/gm_${latest#v}_Linux_intel64.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN"
|
||||
latest="$(curl -L https://api.github.com/repos/tdewolff/minify/releases/latest | jq -rc '.tag_name' 2>/dev/null)"
|
||||
curl -L "https://github.com/tdewolff/minify/releases/download/${latest}/minify_${OS}_${ARCH}.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN"
|
||||
chmod -R +x "$PKG_BUILD_BIN"
|
||||
|
@ -54,6 +54,17 @@ return baseclass.extend({
|
||||
['https://www.gstatic.com/generate_204']
|
||||
],
|
||||
|
||||
inbound_type: [
|
||||
['http', _('HTTP')],
|
||||
['socks', _('SOCKS')],
|
||||
['mixed', _('Mixed')],
|
||||
['shadowsocks', _('Shadowsocks')],
|
||||
['vmess', _('VMess')],
|
||||
['tuic', _('TUIC')],
|
||||
['hysteria2', _('Hysteria2')],
|
||||
//['tunnel', _('Tunnel')]
|
||||
],
|
||||
|
||||
ip_version: [
|
||||
['', _('Keep default')],
|
||||
['dual', _('Dual stack')],
|
||||
@ -163,6 +174,34 @@ return baseclass.extend({
|
||||
//'SUB-RULE': 0,
|
||||
},
|
||||
|
||||
shadowsocks_cipher_methods: [
|
||||
/* Stream */
|
||||
['none', _('none')],
|
||||
/* AEAD */
|
||||
['aes-128-gcm', _('aes-128-gcm')],
|
||||
['aes-192-gcm', _('aes-192-gcm')],
|
||||
['aes-256-gcm', _('aes-256-gcm')],
|
||||
['chacha20-ietf-poly1305', _('chacha20-ietf-poly1305')],
|
||||
['xchacha20-ietf-poly1305', _('xchacha20-ietf-poly1305')],
|
||||
/* AEAD 2022 */
|
||||
['2022-blake3-aes-128-gcm', _('2022-blake3-aes-128-gcm')],
|
||||
['2022-blake3-aes-256-gcm', _('2022-blake3-aes-256-gcm')],
|
||||
['2022-blake3-chacha20-poly1305', _('2022-blake3-chacha20-poly1305')]
|
||||
],
|
||||
|
||||
shadowsocks_cipher_length: {
|
||||
/* AEAD */
|
||||
'aes-128-gcm': 0,
|
||||
'aes-192-gcm': 0,
|
||||
'aes-256-gcm': 0,
|
||||
'chacha20-ietf-poly1305': 0,
|
||||
'xchacha20-ietf-poly1305': 0,
|
||||
/* AEAD 2022 */
|
||||
'2022-blake3-aes-128-gcm': 16,
|
||||
'2022-blake3-aes-256-gcm': 32,
|
||||
'2022-blake3-chacha20-poly1305': 32
|
||||
},
|
||||
|
||||
tls_client_fingerprints: [
|
||||
['chrome'],
|
||||
['firefox'],
|
||||
@ -264,7 +303,7 @@ return baseclass.extend({
|
||||
|
||||
/* Thanks to luci-app-ssr-plus */
|
||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var padding = (4 - str.length % 4) % 4;
|
||||
var padding = (4 - (str.length % 4)) % 4;
|
||||
if (padding)
|
||||
str = str + Array(padding + 1).join('=');
|
||||
|
||||
@ -273,6 +312,29 @@ return baseclass.extend({
|
||||
).join(''));
|
||||
},
|
||||
|
||||
generateRand: function(type, length) {
|
||||
var byteArr;
|
||||
if (['base64', 'hex'].includes(type))
|
||||
byteArr = crypto.getRandomValues(new Uint8Array(length));
|
||||
switch (type) {
|
||||
case 'base64':
|
||||
/* Thanks to https://stackoverflow.com/questions/9267899 */
|
||||
return btoa(String.fromCharCode.apply(null, byteArr));
|
||||
case 'hex':
|
||||
return Array.from(byteArr, (byte) =>
|
||||
(byte & 255).toString(16).padStart(2, '0')
|
||||
).join('');
|
||||
case 'uuid':
|
||||
/* Thanks to https://stackoverflow.com/a/2117523 */
|
||||
return (location.protocol === 'https:') ? crypto.randomUUID() :
|
||||
([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
};
|
||||
},
|
||||
|
||||
getFeatures: function() {
|
||||
var callGetFeatures = rpc.declare({
|
||||
object: 'luci.fchomo',
|
||||
@ -554,6 +616,29 @@ return baseclass.extend({
|
||||
return this.vallist[i];
|
||||
},
|
||||
|
||||
validateAuth: function(section_id, value) {
|
||||
if (!value)
|
||||
return true;
|
||||
if (!value.match(/^[\w-]{3,}:[^:]+$/))
|
||||
return _('Expecting: %s').format('[A-Za-z0-9_-]{3,}:[^:]+');
|
||||
|
||||
return true;
|
||||
},
|
||||
validateAuthUsername: function(section_id, value) {
|
||||
if (!value)
|
||||
return true;
|
||||
if (!value.match(/^[\w-]{3,}$/))
|
||||
return _('Expecting: %s').format('[A-Za-z0-9_-]{3,}');
|
||||
|
||||
return true;
|
||||
},
|
||||
validateAuthPassword: function(section_id, value) {
|
||||
if (!value.match(/^[^:]+$/))
|
||||
return _('Expecting: %s').format('[^:]+');
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateCommonPort: function(section_id, value) {
|
||||
// thanks to homeproxy
|
||||
var stubValidator = {
|
||||
@ -617,7 +702,7 @@ return baseclass.extend({
|
||||
uci.sections(this.config, this.section.sectiontype, (res) => {
|
||||
if (res['.name'] !== section_id)
|
||||
if (res[this.option] === value)
|
||||
duplicate = true
|
||||
duplicate = true;
|
||||
});
|
||||
if (duplicate)
|
||||
return _('Expecting: %s').format(_('unique value'));
|
||||
@ -641,6 +726,15 @@ return baseclass.extend({
|
||||
return true;
|
||||
},
|
||||
|
||||
validateUUID: function(section_id, value) {
|
||||
if (!value)
|
||||
return true;
|
||||
else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
|
||||
return _('Expecting: %s').format(_('valid uuid'));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
lsDir: function(type) {
|
||||
var callLsDir = rpc.declare({
|
||||
object: 'luci.fchomo',
|
||||
@ -719,5 +813,26 @@ return baseclass.extend({
|
||||
} else
|
||||
throw res.error || 'unknown error';
|
||||
});
|
||||
},
|
||||
|
||||
// thanks to homeproxy
|
||||
uploadCertificate: function(type, filename, ev) {
|
||||
var callWriteCertificate = rpc.declare({
|
||||
object: 'luci.fchomo',
|
||||
method: 'certificate_write',
|
||||
params: ['filename'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return ui.uploadFile('/tmp/fchomo_certificate.tmp', ev.target)
|
||||
.then(L.bind((btn, res) => {
|
||||
return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
|
||||
if (ret.result === true)
|
||||
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error)));
|
||||
});
|
||||
}, this, ev.target))
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
|
||||
}
|
||||
});
|
||||
|
@ -295,7 +295,7 @@ return view.extend({
|
||||
]);
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'global', 'fchomo');
|
||||
s = m.section(form.NamedSection, 'routing', 'fchomo', null);
|
||||
|
||||
/* Proxy Group START */
|
||||
s.tab('group', _('Proxy Group'));
|
||||
@ -307,7 +307,6 @@ return view.extend({
|
||||
o.onclick = L.bind(hm.handleReload, o, 'mihomo-c');
|
||||
|
||||
o = s.taboption('group', form.ListValue, 'default_proxy', _('Default outbound'));
|
||||
o.ucisection = 'routing';
|
||||
o.load = L.bind(hm.loadProxyGroupLabel, o, hm.preset_outbound.direct);
|
||||
|
||||
/* Proxy Group */
|
||||
|
@ -34,7 +34,7 @@ function handleResUpdate(type, repo) {
|
||||
});
|
||||
|
||||
// Dynamic repo
|
||||
var label
|
||||
var label;
|
||||
if (repo) {
|
||||
var section_id = this.section.section;
|
||||
var weight = document.getElementById(this.cbid(section_id));
|
||||
@ -188,7 +188,7 @@ return view.extend({
|
||||
});
|
||||
})
|
||||
}, [ _('Check') ]),
|
||||
E('strong', { id: ElId}, [
|
||||
E('strong', { id: ElId }, [
|
||||
E('span', { style: 'color:gray' }, ' ' + _('unchecked'))
|
||||
])
|
||||
]);
|
||||
@ -328,14 +328,7 @@ return view.extend({
|
||||
so = ss.option(form.DynamicList, 'authentication', _('User Authentication'));
|
||||
so.datatype = 'list(string)';
|
||||
so.placeholder = 'user1:pass1';
|
||||
so.validate = function(section_id, value) {
|
||||
if (!value)
|
||||
return true;
|
||||
if (!value.match(/^[\w-]{3,}:[^:]+$/))
|
||||
return _('Expecting: %s').format('[A-Za-z0-9_-]{3,}:[^:]+');
|
||||
|
||||
return true;
|
||||
}
|
||||
so.validate = L.bind(hm.validateAuth, so);
|
||||
|
||||
so = ss.option(form.DynamicList, 'skip_auth_prefixes', _('No Authentication IP ranges'));
|
||||
so.datatype = 'list(cidr)';
|
||||
@ -350,22 +343,22 @@ return view.extend({
|
||||
ss = o.subsection;
|
||||
|
||||
so = ss.option(form.Value, 'mixed_port', _('Mixed port'));
|
||||
so.datatype = 'port'
|
||||
so.datatype = 'port';
|
||||
so.placeholder = '7890';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.Value, 'redir_port', _('Redir port'));
|
||||
so.datatype = 'port'
|
||||
so.datatype = 'port';
|
||||
so.placeholder = '7891';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.Value, 'tproxy_port', _('Tproxy port'));
|
||||
so.datatype = 'port'
|
||||
so.datatype = 'port';
|
||||
so.placeholder = '7892';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.Value, 'tunnel_port', _('DNS port'));
|
||||
so.datatype = 'port'
|
||||
so.datatype = 'port';
|
||||
so.placeholder = '7893';
|
||||
so.rmempty = false;
|
||||
|
||||
@ -397,7 +390,7 @@ return view.extend({
|
||||
so.onchange = function(ev, section_id, value) {
|
||||
var desc = ev.target.nextSibling;
|
||||
if (value === 'mixed')
|
||||
desc.innerHTML = _('Mixed <code>system</code> TCP stack and <code>gVisor</code> UDP stack.')
|
||||
desc.innerHTML = _('Mixed <code>system</code> TCP stack and <code>gVisor</code> UDP stack.');
|
||||
else if (value === 'gvisor')
|
||||
desc.innerHTML = _('Based on google/gvisor.');
|
||||
else if (value === 'system')
|
||||
@ -588,20 +581,20 @@ return view.extend({
|
||||
|
||||
so = ss.taboption('interface', form.Value, 'route_table_id', _('Routing table ID'));
|
||||
so.ucisection = 'config';
|
||||
so.datatype = 'uinteger'
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '2022';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.taboption('interface', form.Value, 'route_rule_pref', _('Routing rule priority'));
|
||||
so.ucisection = 'config';
|
||||
so.datatype = 'uinteger'
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '9000';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.taboption('interface', form.Value, 'self_mark', _('Routing mark'),
|
||||
_('Priority: Proxy Node > Proxy Group > Global.'));
|
||||
so.ucisection = 'config';
|
||||
so.datatype = 'uinteger'
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '200';
|
||||
so.rmempty = false;
|
||||
|
||||
|
@ -0,0 +1,390 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
'require fchomo as hm';
|
||||
|
||||
function handleGenKey(option) {
|
||||
var section_id = this.section.section;
|
||||
var type = this.section.getOption('type').formvalue(section_id);
|
||||
var widget = this.map.findElement('id', 'widget.cbid.fchomo.%s.%s'.format(section_id, option));
|
||||
var password, required_method;
|
||||
|
||||
if (option === 'uuid' || option.match(/_uuid/))
|
||||
required_method = 'uuid';
|
||||
else if (type === 'shadowsocks')
|
||||
required_method = this.section.getOption('shadowsocks_chipher')?.formvalue(section_id);
|
||||
|
||||
switch (required_method) {
|
||||
/* NONE */
|
||||
case 'none':
|
||||
password = '';
|
||||
break;
|
||||
/* UUID */
|
||||
case 'uuid':
|
||||
password = hm.generateRand('uuid');
|
||||
break;
|
||||
/* DEFAULT */
|
||||
default:
|
||||
password = hm.generateRand('hex', 16);
|
||||
break;
|
||||
}
|
||||
/* AEAD */
|
||||
(function(length) {
|
||||
if (length && length > 0)
|
||||
password = hm.generateRand('base64', length);
|
||||
}(hm.shadowsocks_cipher_length[required_method]));
|
||||
|
||||
return widget.value = password;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('fchomo'),
|
||||
hm.getFeatures()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'),
|
||||
features = data[1];
|
||||
|
||||
var 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(hm, document.getElementById('_server_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-s', true);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section' }, [
|
||||
E('p', [
|
||||
hm.renderStatus(hm, '_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(form.GridSection, 'server', null);
|
||||
var prefmt = { 'prefix': 'server_', 'suffix': '' };
|
||||
s.addremove = true;
|
||||
s.rowcolors = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.modaltitle = L.bind(hm.loadModalTitle, s, _('Server'), _('Add a server'));
|
||||
s.sectiontitle = L.bind(hm.loadDefaultLabel, s);
|
||||
s.renderSectionAdd = L.bind(hm.renderSectionAdd, s, prefmt, false);
|
||||
s.handleAdd = L.bind(hm.handleAdd, s, prefmt);
|
||||
|
||||
/* General fields */
|
||||
o = s.option(form.Value, 'label', _('Label'));
|
||||
o.load = L.bind(hm.loadDefaultLabel, o);
|
||||
o.validate = L.bind(hm.validateUniqueValue, o);
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.enabled;
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.ListValue, 'type', _('Type'));
|
||||
o.default = hm.inbound_type[0][0];
|
||||
hm.inbound_type.forEach((res) => {
|
||||
o.value.apply(o, res);
|
||||
})
|
||||
|
||||
o = s.option(form.Value, 'listen', _('Listen address'));
|
||||
o.datatype = 'ipaddr';
|
||||
o.placeholder = '::';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Listen port'));
|
||||
o.datatype = 'port';
|
||||
o.rmempty = false;
|
||||
|
||||
// dev: Features under development
|
||||
// rule
|
||||
// proxy
|
||||
|
||||
/* HTTP / SOCKS fields */
|
||||
/* hm.validateAuth */
|
||||
o = s.option(form.Value, 'username', _('Username'));
|
||||
o.validate = L.bind(hm.validateAuthUsername, o);
|
||||
o.depends({type: /^(http|socks|mixed|hysteria2)$/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.renderWidget = function() {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'title': _('Generate'),
|
||||
'click': ui.createHandlerFn(this, handleGenKey, this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.validate = function(section_id, value) {
|
||||
}
|
||||
o.validate = L.bind(hm.validateAuthPassword, o);
|
||||
o.rmempty = false;
|
||||
o.depends({type: /^(http|socks|mixed|hysteria2)$/, username: /.+/});
|
||||
o.depends({type: /^(tuic)$/, uuid: /.+/});
|
||||
o.modalonly = true;
|
||||
|
||||
/* Hysteria2 fields */
|
||||
o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'),
|
||||
_('In Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'),
|
||||
_('In Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
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.default = o.disabled;
|
||||
o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('salamander', _('Salamander'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, '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.renderWidget = function() {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'title': _('Generate'),
|
||||
'click': ui.createHandlerFn(this, handleGenKey, this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.rmempty = false;
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/});
|
||||
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.placeholder = 'file:///var/www or http://127.0.0.1:8080'
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
/* Shadowsocks fields */
|
||||
o = s.option(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.option(form.Value, 'shadowsocks_password', _('Password'));
|
||||
o.password = true;
|
||||
o.renderWidget = function() {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'title': _('Generate'),
|
||||
'click': ui.createHandlerFn(this, handleGenKey, this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.validate = function(section_id, value) {
|
||||
var encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id);
|
||||
var length = hm.shadowsocks_cipher_length[encmode];
|
||||
if (length) {
|
||||
length = Math.ceil(length/3)*4;
|
||||
if (encmode.match(/^2022-/)) {
|
||||
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
|
||||
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
|
||||
} else {
|
||||
if (length !== 0 && value.length !== length)
|
||||
return _('Expecting: %s').format(_('valid key length with %d characters').format(length));
|
||||
}
|
||||
} else
|
||||
return true;
|
||||
|
||||
return true;
|
||||
}
|
||||
o.depends({type: 'shadowsocks', shadowsocks_chipher: /.+/});
|
||||
o.modalonly = true;
|
||||
|
||||
/* Tuic fields */
|
||||
o = s.option(form.Value, 'uuid', _('UUID'));
|
||||
o.renderWidget = function() {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'title': _('Generate'),
|
||||
'click': ui.createHandlerFn(this, handleGenKey, this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.rmempty = false;
|
||||
o.validate = L.bind(hm.validateUUID, o);
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(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.option(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.option(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;
|
||||
|
||||
o = s.option(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;
|
||||
|
||||
/* VMess fields */
|
||||
o = s.option(form.Value, 'vmess_uuid', _('UUID'));
|
||||
o.renderWidget = function() {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'title': _('Generate'),
|
||||
'click': ui.createHandlerFn(this, handleGenKey, this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.rmempty = false;
|
||||
o.validate = L.bind(hm.validateUUID, o);
|
||||
o.depends('type', 'vmess');
|
||||
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.placeholder = '0';
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
|
||||
/* TLS fields */
|
||||
o = s.option(form.Flag, 'tls', _('TLS'));
|
||||
o.default = o.disabled;
|
||||
o.validate = function(section_id, value) {
|
||||
var type = this.section.getOption('type').formvalue(section_id);
|
||||
var tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input');
|
||||
var tls_alpn = this.section.getUIElement(section_id, 'tls_alpn');
|
||||
|
||||
// Force enabled
|
||||
if (['tuic', 'hysteria2'].includes(type)) {
|
||||
tls.checked = true;
|
||||
tls.disabled = true;
|
||||
if (!`${tls_alpn.getValue()}`)
|
||||
tls_alpn.setValue('h3');
|
||||
} else {
|
||||
tls.disabled = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.depends({type: /^(vmess|tuic|hysteria2)$/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(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.option(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');
|
||||
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/fchomo/certs/server_publickey.pem'});
|
||||
o.onclick = L.bind(hm.uploadCertificate, o, _('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/fchomo/certs/server_privatekey.pem');
|
||||
o.rmempty = false;
|
||||
o.depends({tls: '1', tls_cert_path: /.+/});
|
||||
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/fchomo/certs/server_privatekey.pem'});
|
||||
o.onclick = L.bind(hm.uploadCertificate, o, _('private key'), 'server_privatekey');
|
||||
o.modalonly = true;
|
||||
|
||||
/* Extra fields */
|
||||
o = s.option(form.Flag, 'udp', _('UDP'));
|
||||
o.default = o.disabled;
|
||||
o.depends({type: /^(socks|mixed|shadowsocks)$/});
|
||||
o.modalonly = true;
|
||||
/* Server settings END */
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -46,7 +46,46 @@ export function strToInt(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
return !match(str, /^\d+$/) ? str : int(str) || null;
|
||||
return !match(str, /^\d+$/) ? str : int(str) ?? null;
|
||||
};
|
||||
|
||||
export function durationToSecond(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
let seconds = 0;
|
||||
let arr = match(str, /^(\d+)(s|m|h|d)?$/);
|
||||
if (arr) {
|
||||
if (arr[2] === 's') {
|
||||
seconds = strToInt(arr[1]);
|
||||
} else if (arr[2] === 'm') {
|
||||
seconds = strToInt(arr[1]) * 60;
|
||||
} else if (arr[2] === 'h') {
|
||||
seconds = strToInt(arr[1]) * 3600;
|
||||
} else if (arr[2] === 'd') {
|
||||
seconds = strToInt(arr[1]) * 86400;
|
||||
} else
|
||||
seconds = strToInt(arr[1]);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
};
|
||||
|
||||
export function arrToObj(res) {
|
||||
if (isEmpty(res))
|
||||
return null;
|
||||
|
||||
let object;
|
||||
if (type(res) === 'array') {
|
||||
object = {};
|
||||
map(res, (e) => {
|
||||
if (type(e) === 'array')
|
||||
object[e[0]] = e[1];
|
||||
});
|
||||
} else
|
||||
return res;
|
||||
|
||||
return object;
|
||||
};
|
||||
|
||||
export function removeBlankAttrs(res) {
|
||||
|
@ -9,7 +9,7 @@ import { cursor } from 'uci';
|
||||
import { urldecode, urlencode } from 'luci.http';
|
||||
|
||||
import {
|
||||
isEmpty, strToBool, strToInt,
|
||||
isEmpty, strToBool, strToInt, durationToSecond,
|
||||
removeBlankAttrs,
|
||||
HM_DIR, RUN_DIR, PRESET_OUTBOUND
|
||||
} from 'fchomo';
|
||||
@ -105,28 +105,6 @@ function parse_filter(cfg) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function parse_time_duration(time) {
|
||||
if (isEmpty(time))
|
||||
return null;
|
||||
|
||||
let seconds = 0;
|
||||
let arr = match(time, /^(\d+)(s|m|h|d)?$/);
|
||||
if (arr) {
|
||||
if (arr[2] === 's') {
|
||||
seconds = strToInt(arr[1]);
|
||||
} else if (arr[2] === 'm') {
|
||||
seconds = strToInt(arr[1]) * 60;
|
||||
} else if (arr[2] === 'h') {
|
||||
seconds = strToInt(arr[1]) * 3600;
|
||||
} else if (arr[2] === 'd') {
|
||||
seconds = strToInt(arr[1]) * 86400;
|
||||
} else
|
||||
seconds = strToInt(arr[1]);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
function get_proxynode(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
@ -191,8 +169,8 @@ config["etag-support"] = (uci.get(uciconf, uciglobal, 'etag_support') === '0') ?
|
||||
config.ipv6 = (uci.get(uciconf, uciglobal, 'ipv6') === '0') ? false : true;
|
||||
config["unified-delay"] = strToBool(uci.get(uciconf, uciglobal, 'unified_delay')) || false;
|
||||
config["tcp-concurrent"] = strToBool(uci.get(uciconf, uciglobal, 'tcp_concurrent')) || false;
|
||||
config["keep-alive-interval"] = parse_time_duration(uci.get(uciconf, uciglobal, 'keep_alive_interval')) || 30;
|
||||
config["keep-alive-idle"] = parse_time_duration(uci.get(uciconf, uciglobal, 'keep_alive_idle')) || 600;
|
||||
config["keep-alive-interval"] = durationToSecond(uci.get(uciconf, uciglobal, 'keep_alive_interval')) || 30;
|
||||
config["keep-alive-idle"] = durationToSecond(uci.get(uciconf, uciglobal, 'keep_alive_idle')) || 600;
|
||||
/* ACL settings */
|
||||
config["interface-name"] = bind_interface;
|
||||
config["routing-mark"] = self_mark;
|
||||
@ -345,7 +323,7 @@ if (match(proxy_mode, /tun/))
|
||||
"route-exclude-address-set": [],
|
||||
"include-interface": [],
|
||||
"exclude-interface": [],
|
||||
"udp-timeout": parse_time_duration(uci.get(uciconf, uciinbound, 'tun_udp_timeout')) || 300,
|
||||
"udp-timeout": durationToSecond(uci.get(uciconf, uciinbound, 'tun_udp_timeout')) || 300,
|
||||
"endpoint-independent-nat": strToBool(uci.get(uciconf, uciinbound, 'tun_endpoint_independent_nat')),
|
||||
"auto-detect-interface": true
|
||||
});
|
||||
@ -452,7 +430,7 @@ uci.foreach(uciconf, ucipgrp, (cfg) => {
|
||||
["routing-mark"]: strToInt(cfg.routing_mark),
|
||||
// Health fields
|
||||
url: cfg.url,
|
||||
interval: cfg.url ? parse_time_duration(cfg.interval) || 600 : null,
|
||||
interval: cfg.url ? durationToSecond(cfg.interval) || 600 : null,
|
||||
timeout: cfg.url ? strToInt(cfg.timeout) || 5000 : null,
|
||||
lazy: (cfg.lazy === '0') ? false : null,
|
||||
"expected-status": cfg.url ? cfg.expected_status || '204' : null,
|
||||
@ -476,7 +454,7 @@ uci.foreach(uciconf, uciprov, (cfg) => {
|
||||
type: cfg.type,
|
||||
path: HM_DIR + '/provider/' + cfg['.name'],
|
||||
url: cfg.url,
|
||||
interval: (cfg.type === 'http') ? parse_time_duration(cfg.interval) || 86400 : null,
|
||||
interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) || 86400 : null,
|
||||
proxy: get_proxygroup(cfg.proxy),
|
||||
header: cfg.header ? json(cfg.header) : null,
|
||||
"health-check": {},
|
||||
@ -513,7 +491,7 @@ uci.foreach(uciconf, uciprov, (cfg) => {
|
||||
config["proxy-providers"][cfg['.name']]["health-check"] = {
|
||||
enable: true,
|
||||
url: cfg.health_url,
|
||||
interval: parse_time_duration(cfg.health_interval) || 600,
|
||||
interval: durationToSecond(cfg.health_interval) || 600,
|
||||
timeout: strToInt(cfg.health_timeout) || 5000,
|
||||
lazy: (cfg.health_lazy === '0') ? false : null,
|
||||
"expected-status": cfg.health_expected_status || '204'
|
||||
@ -535,7 +513,7 @@ uci.foreach(uciconf, ucirule, (cfg) => {
|
||||
behavior: cfg.behavior,
|
||||
path: HM_DIR + '/ruleset/' + cfg['.name'],
|
||||
url: cfg.url,
|
||||
interval: (cfg.type === 'http') ? parse_time_duration(cfg.interval) || 259200 : null,
|
||||
interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) || 259200 : null,
|
||||
proxy: get_proxygroup(cfg.proxy)
|
||||
};
|
||||
});
|
||||
|
105
luci-app-fchomo/root/etc/fchomo/scripts/generate_server.uc
Executable file
105
luci-app-fchomo/root/etc/fchomo/scripts/generate_server.uc
Executable file
@ -0,0 +1,105 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import {
|
||||
isEmpty, strToBool, strToInt, durationToSecond,
|
||||
arrToObj, removeBlankAttrs,
|
||||
HM_DIR, RUN_DIR, PRESET_OUTBOUND
|
||||
} from 'fchomo';
|
||||
|
||||
/* UCI config START */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconf = 'fchomo';
|
||||
uci.load(uciconf);
|
||||
|
||||
const uciserver = 'server';
|
||||
|
||||
/* UCI config END */
|
||||
|
||||
/* Config helper START */
|
||||
function parse_users(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
let uap, arr, users=[];
|
||||
for (uap in cfg) {
|
||||
arr = split(uap, ':');
|
||||
users[arr[0]] = arr[1];
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
/* Config helper END */
|
||||
|
||||
/* Main */
|
||||
const config = {};
|
||||
|
||||
/* Inbound START */
|
||||
config.listeners = [];
|
||||
uci.foreach(uciconf, uciserver, (cfg) => {
|
||||
if (cfg.enabled === '0')
|
||||
return;
|
||||
|
||||
push(config.listeners, {
|
||||
name: cfg['.name'],
|
||||
type: cfg.type,
|
||||
|
||||
listen: cfg.listen || '::',
|
||||
port: strToInt(cfg.port),
|
||||
proxy: 'DIRECT',
|
||||
udp: strToBool(cfg.udp),
|
||||
|
||||
/* Hysteria2 */
|
||||
up: strToInt(cfg.hysteria_up_mbps),
|
||||
down: strToInt(cfg.hysteria_down_mbps),
|
||||
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||
obfs: cfg.hysteria_obfs_type,
|
||||
"obfs-password": cfg.hysteria_obfs_password,
|
||||
masquerade: cfg.hysteria_masquerade,
|
||||
|
||||
/* Shadowsocks */
|
||||
cipher: cfg.shadowsocks_chipher,
|
||||
password: cfg.shadowsocks_password,
|
||||
|
||||
/* Tuic */
|
||||
"congestion-controller": cfg.tuic_congestion_controller,
|
||||
"max-idle-time": durationToSecond(cfg.tuic_max_idle_time),
|
||||
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
|
||||
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size),
|
||||
|
||||
/* HTTP / SOCKS / VMess / Tuic / Hysteria2 */
|
||||
users: (cfg.type in ['http', 'socks', 'mixed', 'vmess']) ? [
|
||||
{
|
||||
/* HTTP / SOCKS */
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
|
||||
/* VMess */
|
||||
uuid: cfg.vmess_uuid,
|
||||
alterId: strToInt(cfg.vmess_alterid)
|
||||
}
|
||||
/*{
|
||||
}*/
|
||||
] : ((cfg.type in ['tuic', 'hysteria2']) ? {
|
||||
/* Hysteria2 */
|
||||
...arrToObj([[cfg.username, cfg.password]]),
|
||||
|
||||
/* Tuic */
|
||||
...arrToObj([[cfg.uuid, cfg.password]])
|
||||
} : null),
|
||||
|
||||
/* TLS */
|
||||
...(cfg.tls === '1' ? {
|
||||
alpn: cfg.tls_alpn,
|
||||
certificate: cfg.tls_cert_path,
|
||||
"private-key": cfg.tls_key_path
|
||||
} : {})
|
||||
});
|
||||
});
|
||||
/* Inbound END */
|
||||
|
||||
printf('%.J\n', removeBlankAttrs(config));
|
@ -275,20 +275,22 @@ start_service() {
|
||||
|
||||
if [ "$server_auto_firewall" = "1" ]; then
|
||||
add_firewall() {
|
||||
local enabled udp proto port
|
||||
local enabled listen port
|
||||
config_get_bool enabled "$1" "enabled" "1"
|
||||
config_get_bool udp "$1" "udp" "1"
|
||||
[ "$udp" = "0" ] && proto='tcp' || proto='tcp udp'
|
||||
config_get listen "$1" "listen" "::"
|
||||
config_get port "$1" "port"
|
||||
|
||||
|
||||
[ "$enabled" = "0" ] && return 0
|
||||
|
||||
json_add_object ''
|
||||
json_add_string type rule
|
||||
json_add_string target ACCEPT
|
||||
json_add_string name "$1"
|
||||
json_add_string proto "$proto"
|
||||
#json_add_string family '' # '' = IPv4 and IPv6
|
||||
json_add_string proto 'tcp udp'
|
||||
json_add_string src "*"
|
||||
#json_add_string dest '' # '' = input
|
||||
json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')"
|
||||
json_add_string dest_port "$port"
|
||||
json_close_object
|
||||
}
|
||||
@ -323,6 +325,8 @@ start_service() {
|
||||
log "$(mihomo -v | awk 'NR==1{print $1,$3}') started."
|
||||
}
|
||||
|
||||
service_started() { procd_set_config_changed firewall; }
|
||||
|
||||
stop_service() {
|
||||
# Client
|
||||
[ -z "$1" -o "$1" = "mihomo-c" ] && stop_client
|
||||
@ -388,7 +392,8 @@ service_stopped() {
|
||||
# Client
|
||||
[ -n "$(/etc/init.d/$CONF info | jsonfilter -q -e '@.'"$CONF"'.instances["mihomo-c"]')" ] || client_stopped
|
||||
# Server
|
||||
return 0
|
||||
|
||||
procd_set_config_changed firewall;
|
||||
}
|
||||
|
||||
client_stopped() {
|
||||
|
@ -15,6 +15,9 @@
|
||||
"uci": [ "fchomo" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/fchomo_certificate.tmp": [ "write" ]
|
||||
},
|
||||
"uci": [ "fchomo" ]
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,14 @@ function shellquote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function isBinary(str) {
|
||||
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
|
||||
if (byte <= 8 || (byte >= 14 && byte <= 31))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasKernelModule(kmod) {
|
||||
return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0);
|
||||
}
|
||||
@ -274,6 +282,74 @@ const methods = {
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
},
|
||||
|
||||
// thanks to homeproxy
|
||||
certificate_write: {
|
||||
args: { filename: 'filename' },
|
||||
call: function(req) {
|
||||
const writeCertificate = function(filename, priv) {
|
||||
const tmpcert = '/tmp/fchomo_certificate.tmp';
|
||||
const filestat = lstat(tmpcert);
|
||||
|
||||
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'empty certificate file' };
|
||||
}
|
||||
|
||||
let filecontent = readfile(tmpcert);
|
||||
if (isBinary(filecontent)) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'illegal file type: binary' };
|
||||
}
|
||||
|
||||
/* Kanged from luci-proto-openconnect */
|
||||
const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
|
||||
end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
|
||||
lines = split(trim(filecontent), /[\r\n]/);
|
||||
let start = false, i;
|
||||
|
||||
for (i = 0; i < length(lines); i++) {
|
||||
if (match(lines[i], beg))
|
||||
start = true;
|
||||
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'this does not look like a correct PEM file' };
|
||||
}
|
||||
|
||||
/* Sanitize certificate */
|
||||
filecontent = trim(filecontent);
|
||||
filecontent = replace(filecontent, /\r\n?/g, '\n');
|
||||
if (!match(filecontent, /\n$/))
|
||||
filecontent += '\n';
|
||||
|
||||
system(`mkdir -p ${HM_DIR}/certs`);
|
||||
writefile(`${HM_DIR}/certs/${filename}.pem`, filecontent);
|
||||
system(`rm -f ${tmpcert}`);
|
||||
|
||||
return { result: true };
|
||||
};
|
||||
|
||||
const filename = req.args?.filename;
|
||||
if (!filename || match(filename, /\.\.\//))
|
||||
return { result: false, error: 'illegal cerificate filename' };
|
||||
switch (filename) {
|
||||
case 'client_ca':
|
||||
case 'server_publickey':
|
||||
return writeCertificate(filename, false);
|
||||
break;
|
||||
case 'server_privatekey':
|
||||
return writeCertificate(filename, true);
|
||||
break;
|
||||
default:
|
||||
return { result: false, error: 'illegal cerificate filename' };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -37,7 +37,7 @@ include './cfg.php';
|
||||
<script type="text/javascript" src="./assets/js/feather.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/jquery-2.1.3.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/bootstrap.min.js"></script>
|
||||
<?php include './status-bar.php'; ?>
|
||||
<?php include './ping.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-sm container-bg text-center callout border border-3 rounded-4 col-11">
|
||||
|
@ -30,7 +30,6 @@ $dashboard_link = $neko_cfg['ctrl_host'] . ':' . $neko_cfg['ctrl_port'] . '/ui/d
|
||||
<link href="./assets/theme/<?php echo $neko_theme ?>" rel="stylesheet">
|
||||
<script type="text/javascript" src="./assets/js/feather.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/jquery-2.1.3.min.js"></script>
|
||||
<?php include './status-bar.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<head>
|
||||
|
@ -526,7 +526,7 @@ if (isset($_GET['ajax'])) {
|
||||
<script type="text/javascript" src="./assets/js/feather.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/jquery-2.1.3.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/neko.js"></script>
|
||||
<?php include './status-bar.php'; ?>
|
||||
<?php include './ping.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<?php if ($isNginx): ?>
|
||||
|
513
luci-app-nekobox/htdocs/nekobox/ping.php
Normal file
513
luci-app-nekobox/htdocs/nekobox/ping.php
Normal file
@ -0,0 +1,513 @@
|
||||
<?php
|
||||
ob_start();
|
||||
include './cfg.php';
|
||||
$translate = [
|
||||
'United States' => '美国',
|
||||
'China' => '中国',
|
||||
'ISP' => '互联网服务提供商',
|
||||
'Japan' => '日本',
|
||||
'South Korea' => '韩国',
|
||||
'Germany' => '德国',
|
||||
'France' => '法国',
|
||||
'United Kingdom' => '英国',
|
||||
'Canada' => '加拿大',
|
||||
'Australia' => '澳大利亚',
|
||||
'Russia' => '俄罗斯',
|
||||
'India' => '印度',
|
||||
'Brazil' => '巴西',
|
||||
'Netherlands' => '荷兰',
|
||||
'Singapore' => '新加坡',
|
||||
'Hong Kong' => '香港',
|
||||
'Saudi Arabia' => '沙特阿拉伯',
|
||||
'Turkey' => '土耳其',
|
||||
'Italy' => '意大利',
|
||||
'Spain' => '西班牙',
|
||||
'Thailand' => '泰国',
|
||||
'Malaysia' => '马来西亚',
|
||||
'Indonesia' => '印度尼西亚',
|
||||
'South Africa' => '南非',
|
||||
'Mexico' => '墨西哥',
|
||||
'Israel' => '以色列',
|
||||
'Sweden' => '瑞典',
|
||||
'Switzerland' => '瑞士',
|
||||
'Norway' => '挪威',
|
||||
'Denmark' => '丹麦',
|
||||
'Belgium' => '比利时',
|
||||
'Finland' => '芬兰',
|
||||
'Poland' => '波兰',
|
||||
'Austria' => '奥地利',
|
||||
'Greece' => '希腊',
|
||||
'Portugal' => '葡萄牙',
|
||||
'Ireland' => '爱尔兰',
|
||||
'New Zealand' => '新西兰',
|
||||
'United Arab Emirates' => '阿拉伯联合酋长国',
|
||||
'Argentina' => '阿根廷',
|
||||
'Chile' => '智利',
|
||||
'Colombia' => '哥伦比亚',
|
||||
'Philippines' => '菲律宾',
|
||||
'Vietnam' => '越南',
|
||||
'Pakistan' => '巴基斯坦',
|
||||
'Egypt' => '埃及',
|
||||
'Nigeria' => '尼日利亚',
|
||||
'Kenya' => '肯尼亚',
|
||||
'Morocco' => '摩洛哥',
|
||||
'Google' => '谷歌',
|
||||
'Amazon' => '亚马逊',
|
||||
'Microsoft' => '微软',
|
||||
'Facebook' => '脸书',
|
||||
'Apple' => '苹果',
|
||||
'IBM' => 'IBM',
|
||||
'Alibaba' => '阿里巴巴',
|
||||
'Tencent' => '腾讯',
|
||||
'Baidu' => '百度',
|
||||
'Verizon' => '威瑞森',
|
||||
'AT&T' => '美国电话电报公司',
|
||||
'T-Mobile' => 'T-移动',
|
||||
'Vodafone' => '沃达丰',
|
||||
'China Telecom' => '中国电信',
|
||||
'China Unicom' => '中国联通',
|
||||
'China Mobile' => '中国移动',
|
||||
'Chunghwa Telecom' => '中华电信',
|
||||
'Amazon Web Services (AWS)' => '亚马逊网络服务 (AWS)',
|
||||
'Google Cloud Platform (GCP)' => '谷歌云平台 (GCP)',
|
||||
'Microsoft Azure' => '微软Azure',
|
||||
'Oracle Cloud' => '甲骨文云',
|
||||
'Alibaba Cloud' => '阿里云',
|
||||
'Tencent Cloud' => '腾讯云',
|
||||
'DigitalOcean' => '数字海洋',
|
||||
'Linode' => '林诺德',
|
||||
'OVHcloud' => 'OVH 云',
|
||||
'Hetzner' => '赫兹纳',
|
||||
'Vultr' => '沃尔特',
|
||||
'OVH' => 'OVH',
|
||||
'DreamHost' => '梦想主机',
|
||||
'InMotion Hosting' => '动态主机',
|
||||
'HostGator' => '主机鳄鱼',
|
||||
'Bluehost' => '蓝主机',
|
||||
'A2 Hosting' => 'A2主机',
|
||||
'SiteGround' => '站点地',
|
||||
'Liquid Web' => '液态网络',
|
||||
'Kamatera' => '卡玛特拉',
|
||||
'IONOS' => 'IONOS',
|
||||
'InterServer' => '互联服务器',
|
||||
'Hostwinds' => '主机之风',
|
||||
'ScalaHosting' => '斯卡拉主机',
|
||||
'GreenGeeks' => '绿色极客'
|
||||
];
|
||||
$lang = $_GET['lang'] ?? 'en';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo htmlspecialchars($lang); ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="on">
|
||||
<link rel="dns-prefetch" href="//cdn.jsdelivr.net">
|
||||
<link rel="dns-prefetch" href="//whois.pconline.com.cn">
|
||||
<link rel="dns-prefetch" href="//forge.speedtest.cn">
|
||||
<link rel="dns-prefetch" href="//api-ipv4.ip.sb">
|
||||
<link rel="dns-prefetch" href="//api.ipify.org">
|
||||
<link rel="dns-prefetch" href="//api.ttt.sh">
|
||||
<link rel="dns-prefetch" href="//qqwry.api.skk.moe">
|
||||
<link rel="dns-prefetch" href="//d.skk.moe">
|
||||
<link rel="preconnect" href="https://forge.speedtest.cn">
|
||||
<link rel="preconnect" href="https://whois.pconline.com.cn">
|
||||
<link rel="preconnect" href="https://api-ipv4.ip.sb">
|
||||
<link rel="preconnect" href="https://api.ipify.org">
|
||||
<link rel="preconnect" href="https://api.ttt.sh">
|
||||
<link rel="preconnect" href="https://qqwry.api.skk.moe">
|
||||
<link rel="preconnect" href="https://d.skk.moe">
|
||||
<style>
|
||||
.img-con {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#flag {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.container-sm.container-bg.callout.border {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.row.align-items-center {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col.text-center {
|
||||
position: static;
|
||||
left: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.container-sm .row .col-4 {
|
||||
position: static !important;
|
||||
order: 2 !important;
|
||||
width: 100% !important;
|
||||
padding-left: 54px !important;
|
||||
margin-top: 5px !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
#ping-result {
|
||||
font-size: 18px !important;
|
||||
margin: 0 !important;
|
||||
white-space: nowrap !important;
|
||||
font-weight: bold !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
#d-ip {
|
||||
color: #09B63F;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.info.small {
|
||||
color: #ff69b4;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.site-icon, .img-con {
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
position: relative !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
.site-icon:hover, .img-con:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
}
|
||||
|
||||
.site-icon:active, .img-con:active {
|
||||
transform: translateY(1px) !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.container-sm.container-bg.callout.border {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.row.align-items-center {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
width: 100% !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.col-auto {
|
||||
flex: 0 0 30px !important;
|
||||
width: 30px !important;
|
||||
min-width: 30px !important;
|
||||
}
|
||||
|
||||
.img-con, #flag {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
object-fit: contain !important;
|
||||
}
|
||||
|
||||
.col-3 {
|
||||
flex: 1 1 auto !important;
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
#d-ip, .info.small {
|
||||
font-size: 14px !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.col-4 {
|
||||
flex: 0 1 auto !important;
|
||||
padding: 0 !important;
|
||||
order: 3 !important;
|
||||
}
|
||||
|
||||
#ping-result {
|
||||
font-size: 14px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex !important;
|
||||
flex: 0 0 auto !important;
|
||||
margin-left: auto !important;
|
||||
order: 2 !important;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
object-fit: contain !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#status-bar-component {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?php if (in_array($lang, ['zh-cn', 'en', 'auto'])): ?>
|
||||
<div id="status-bar-component" class="container-sm container-bg callout border">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="img-con">
|
||||
<img src="./assets/neko/img/loading.svg" id="flag" title="国旗" onclick="IP.getIpipnetIP()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<p id="d-ip" class="ip-address mb-0">Checking...</p>
|
||||
<p id="ipip" class="info small mb-0"></p>
|
||||
</div>
|
||||
<div class="col-4 d-flex justify-content-center">
|
||||
<p id="ping-result" class="mb-0"></p>
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<div class="status-icons d-flex">
|
||||
<div class="site-icon mx-1" onclick="pingHost('baidu', 'Baidu')">
|
||||
<img src="./assets/neko/img/site_icon_01.png" id="baidu-normal" class="status-icon" style="display: none;">
|
||||
<img src="./assets/neko/img/site_icon1_01.png" id="baidu-gray" class="status-icon">
|
||||
</div>
|
||||
<div class="site-icon mx-1" onclick="pingHost('taobao', '淘宝')">
|
||||
<img src="./assets/neko/img/site_icon_02.png" id="taobao-normal" class="status-icon" style="display: none;">
|
||||
<img src="./assets/neko/img/site_icon1_02.png" id="taobao-gray" class="status-icon">
|
||||
</div>
|
||||
<div class="site-icon mx-1" onclick="pingHost('google', 'Google')">
|
||||
<img src="./assets/neko/img/site_icon_03.png" id="google-normal" class="status-icon" style="display: none;">
|
||||
<img src="./assets/neko/img/site_icon1_03.png" id="google-gray" class="status-icon">
|
||||
</div>
|
||||
<div class="site-icon mx-1" onclick="pingHost('youtube', 'YouTube')">
|
||||
<img src="./assets/neko/img/site_icon_04.png" id="youtube-normal" class="status-icon" style="display: none;">
|
||||
<img src="./assets/neko/img/site_icon1_04.png" id="youtube-gray" class="status-icon">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<script src="./assets/neko/js/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
const _IMG = './assets/neko/';
|
||||
const translate = <?php echo json_encode($translate, JSON_UNESCAPED_UNICODE); ?>;
|
||||
let cachedIP = null;
|
||||
let cachedInfo = null;
|
||||
let random = parseInt(Math.random() * 100000000);
|
||||
|
||||
const checkSiteStatus = {
|
||||
sites: {
|
||||
baidu: 'https://www.baidu.com',
|
||||
taobao: 'https://www.taobao.com',
|
||||
google: 'https://www.google.com',
|
||||
youtube: 'https://www.youtube.com'
|
||||
},
|
||||
|
||||
check: async function() {
|
||||
for (let [site, url] of Object.entries(this.sites)) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
document.getElementById(`${site}-normal`).style.display = 'inline';
|
||||
document.getElementById(`${site}-gray`).style.display = 'none';
|
||||
} catch (error) {
|
||||
document.getElementById(`${site}-normal`).style.display = 'none';
|
||||
document.getElementById(`${site}-gray`).style.display = 'inline';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function pingHost(site, siteName) {
|
||||
const url = checkSiteStatus.sites[site];
|
||||
const resultElement = document.getElementById('ping-result');
|
||||
|
||||
try {
|
||||
resultElement.innerHTML = `<span style="font-size: 18px">正在测试 ${siteName} 的连接延迟...`;
|
||||
resultElement.style.color = '#87CEFA';
|
||||
const startTime = performance.now();
|
||||
await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
const endTime = performance.now();
|
||||
const pingTime = Math.round(endTime - startTime);
|
||||
resultElement.innerHTML = `<span style="font-size: 18px">${siteName} 连接延迟: ${pingTime}ms</span>`;
|
||||
if(pingTime <= 100) {
|
||||
resultElement.style.color = '#09B63F';
|
||||
} else if(pingTime <= 200) {
|
||||
resultElement.style.color = '#FFA500';
|
||||
} else {
|
||||
resultElement.style.color = '#ff6b6b';
|
||||
}
|
||||
} catch (error) {
|
||||
resultElement.innerHTML = `<span style="font-size: 18px">${siteName} 连接超时`;
|
||||
resultElement.style.color = '#ff6b6b';
|
||||
}
|
||||
}
|
||||
|
||||
let IP = {
|
||||
isRefreshing: false,
|
||||
fetchIP: async () => {
|
||||
try {
|
||||
const [ipifyResp, ipsbResp, chinaIpResp] = await Promise.all([
|
||||
IP.get('https://api.ipify.org?format=json', 'json'),
|
||||
IP.get('https://api-ipv4.ip.sb/geoip', 'json'),
|
||||
IP.get('https://myip.ipip.net', 'text')
|
||||
]);
|
||||
|
||||
const ipData = ipifyResp.data.ip || ipsbResp.data.ip;
|
||||
cachedIP = ipData;
|
||||
document.getElementById('d-ip').innerHTML = ipData;
|
||||
return ipData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching IP:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
get: (url, type) =>
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store'
|
||||
}).then((resp) => {
|
||||
if (type === 'text')
|
||||
return Promise.all([resp.ok, resp.status, resp.text(), resp.headers]);
|
||||
else
|
||||
return Promise.all([resp.ok, resp.status, resp.json(), resp.headers]);
|
||||
}).then(([ok, status, data, headers]) => {
|
||||
if (ok) {
|
||||
return { ok, status, data, headers };
|
||||
} else {
|
||||
throw new Error(JSON.stringify(data.error));
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Error fetching data:", error);
|
||||
throw error;
|
||||
}),
|
||||
|
||||
Ipip: async (ip, elID) => {
|
||||
try {
|
||||
const [ipsbResp, chinaIpResp] = await Promise.all([
|
||||
IP.get(`https://api.ip.sb/geoip/${ip}`, 'json'),
|
||||
IP.get(`https://myip.ipip.net`, 'text')
|
||||
]);
|
||||
|
||||
cachedIP = ip;
|
||||
cachedInfo = ipsbResp.data;
|
||||
|
||||
let chinaIpInfo = null;
|
||||
try {
|
||||
if(chinaIpResp.data) {
|
||||
chinaIpInfo = chinaIpResp.data;
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Error parsing China IP info:", e);
|
||||
}
|
||||
|
||||
const mergedData = {
|
||||
...ipsbResp.data,
|
||||
chinaIpInfo: chinaIpInfo
|
||||
};
|
||||
|
||||
IP.updateUI(mergedData, elID);
|
||||
} catch (error) {
|
||||
console.error("Error in Ipip function:", error);
|
||||
document.getElementById(elID).innerHTML = "获取IP信息失败";
|
||||
}
|
||||
},
|
||||
|
||||
updateUI: (data, elID) => {
|
||||
try {
|
||||
if (!data || !data.country_code) {
|
||||
document.getElementById('d-ip').innerHTML = "无法获取IP信息";
|
||||
return;
|
||||
}
|
||||
|
||||
let country = translate[data.country] || data.country || "未知";
|
||||
let isp = translate[data.isp] || data.isp || "";
|
||||
let asnOrganization = translate[data.asn_organization] || data.asn_organization || "";
|
||||
|
||||
if (data.country === 'Taiwan') {
|
||||
country = (navigator.language === 'en') ? 'China Taiwan' : '中国台湾';
|
||||
}
|
||||
|
||||
const countryAbbr = data.country_code.toLowerCase();
|
||||
const isChinaIP = ['cn', 'hk', 'mo', 'tw'].includes(countryAbbr);
|
||||
|
||||
let firstLineInfo = `<div style="white-space: nowrap;">`;
|
||||
|
||||
firstLineInfo += cachedIP + ' ';
|
||||
|
||||
let ipLocation = isChinaIP ?
|
||||
'<span style="color: #00FF00;">[国内 IP]</span> ' :
|
||||
'<span style="color: #FF0000;">[境外 IP]</span> ';
|
||||
firstLineInfo += ipLocation;
|
||||
|
||||
if (data.chinaIpInfo) {
|
||||
firstLineInfo += `[${data.chinaIpInfo}]`;
|
||||
}
|
||||
firstLineInfo += `</div>`;
|
||||
|
||||
document.getElementById('d-ip').innerHTML = firstLineInfo;
|
||||
document.getElementById('ipip').innerHTML = `${country} ${isp} ${asnOrganization}`;
|
||||
document.getElementById('ipip').style.color = '#FF00FF';
|
||||
$("#flag").attr("src", _IMG + "flags/" + countryAbbr + ".png");
|
||||
} catch (error) {
|
||||
console.error("Error in updateUI:", error);
|
||||
document.getElementById('d-ip').innerHTML = "更新IP信息显示失败";
|
||||
}
|
||||
},
|
||||
|
||||
getIpipnetIP: async () => {
|
||||
if(IP.isRefreshing) return;
|
||||
|
||||
try {
|
||||
IP.isRefreshing = true;
|
||||
document.getElementById('d-ip').innerHTML = "Checking...";
|
||||
document.getElementById('ipip').innerHTML = "Loading...";
|
||||
$("#flag").attr("src", _IMG + "img/loading.svg");
|
||||
|
||||
const ip = await IP.fetchIP();
|
||||
await IP.Ipip(ip, 'ipip');
|
||||
} catch (error) {
|
||||
console.error("Error in getIpipnetIP function:", error);
|
||||
document.getElementById('ipip').innerHTML = "获取IP信息失败";
|
||||
} finally {
|
||||
IP.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IP.getIpipnetIP();
|
||||
checkSiteStatus.check();
|
||||
setInterval(() => checkSiteStatus.check(), 30000);
|
||||
setInterval(IP.getIpipnetIP, 180000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -72,7 +72,7 @@ $uiVersion = getUiVersion();
|
||||
<script type="text/javascript" src="./assets/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/jquery-2.1.3.min.js"></script>
|
||||
<script type="text/javascript" src="./assets/js/neko.js"></script>
|
||||
<?php include './status-bar.php'; ?>
|
||||
<?php include './ping.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-sm container-bg text-center callout border border-3 rounded-4 col-11">
|
||||
@ -186,10 +186,13 @@ $uiVersion = getUiVersion();
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<pre id="logOutput" style="white-space: pre-wrap; word-wrap: break-word; text-align: left; display: inline-block;">开始下载更新...</pre>
|
||||
<div class="alert alert-info mt-3" role="alert">
|
||||
提示: 如遇到更新失败,请在终端输入 <code>nokobox</code> 进行更新!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logOutput" class="mt-3"></div>
|
||||
|
||||
<style>
|
||||
|
@ -1,458 +0,0 @@
|
||||
<?php
|
||||
$translate = [
|
||||
'United States' => '美国',
|
||||
'China' => '中国',
|
||||
'ISP' => '互联网服务提供商',
|
||||
'Japan' => '日本',
|
||||
'South Korea' => '韩国',
|
||||
'Germany' => '德国',
|
||||
'France' => '法国',
|
||||
'United Kingdom' => '英国',
|
||||
'Canada' => '加拿大',
|
||||
'Australia' => '澳大利亚',
|
||||
'Russia' => '俄罗斯',
|
||||
'India' => '印度',
|
||||
'Brazil' => '巴西',
|
||||
'Netherlands' => '荷兰',
|
||||
'Singapore' => '新加坡',
|
||||
'Hong Kong' => '香港',
|
||||
'Saudi Arabia' => '沙特阿拉伯',
|
||||
'Turkey' => '土耳其',
|
||||
'Italy' => '意大利',
|
||||
'Spain' => '西班牙',
|
||||
'Thailand' => '泰国',
|
||||
'Malaysia' => '马来西亚',
|
||||
'Indonesia' => '印度尼西亚',
|
||||
'South Africa' => '南非',
|
||||
'Mexico' => '墨西哥',
|
||||
'Israel' => '以色列',
|
||||
'Sweden' => '瑞典',
|
||||
'Switzerland' => '瑞士',
|
||||
'Norway' => '挪威',
|
||||
'Denmark' => '丹麦',
|
||||
'Belgium' => '比利时',
|
||||
'Finland' => '芬兰',
|
||||
'Poland' => '波兰',
|
||||
'Austria' => '奥地利',
|
||||
'Greece' => '希腊',
|
||||
'Portugal' => '葡萄牙',
|
||||
'Ireland' => '爱尔兰',
|
||||
'New Zealand' => '新西兰',
|
||||
'United Arab Emirates' => '阿拉伯联合酋长国',
|
||||
'Argentina' => '阿根廷',
|
||||
'Chile' => '智利',
|
||||
'Colombia' => '哥伦比亚',
|
||||
'Philippines' => '菲律宾',
|
||||
'Vietnam' => '越南',
|
||||
'Pakistan' => '巴基斯坦',
|
||||
'Egypt' => '埃及',
|
||||
'Nigeria' => '尼日利亚',
|
||||
'Kenya' => '肯尼亚',
|
||||
'Morocco' => '摩洛哥',
|
||||
'Google' => '谷歌',
|
||||
'Amazon' => '亚马逊',
|
||||
'Microsoft' => '微软',
|
||||
'Facebook' => '脸书',
|
||||
'Apple' => '苹果',
|
||||
'IBM' => 'IBM',
|
||||
'Alibaba' => '阿里巴巴',
|
||||
'Tencent' => '腾讯',
|
||||
'Baidu' => '百度',
|
||||
'Verizon' => '威瑞森',
|
||||
'AT&T' => '美国电话电报公司',
|
||||
'T-Mobile' => 'T-移动',
|
||||
'Vodafone' => '沃达丰',
|
||||
'China Telecom' => '中国电信',
|
||||
'China Unicom' => '中国联通',
|
||||
'China Mobile' => '中国移动',
|
||||
'Chunghwa Telecom' => '中华电信',
|
||||
'Amazon Web Services (AWS)' => '亚马逊网络服务 (AWS)',
|
||||
'Google Cloud Platform (GCP)' => '谷歌云平台 (GCP)',
|
||||
'Microsoft Azure' => '微软Azure',
|
||||
'Oracle Cloud' => '甲骨文云',
|
||||
'Alibaba Cloud' => '阿里云',
|
||||
'Tencent Cloud' => '腾讯云',
|
||||
'DigitalOcean' => '数字海洋',
|
||||
'Linode' => '林诺德',
|
||||
'OVHcloud' => 'OVH 云',
|
||||
'Hetzner' => '赫兹纳',
|
||||
'Vultr' => '沃尔特',
|
||||
'OVH' => 'OVH',
|
||||
'DreamHost' => '梦想主机',
|
||||
'InMotion Hosting' => '动态主机',
|
||||
'HostGator' => '主机鳄鱼',
|
||||
'Bluehost' => '蓝主机',
|
||||
'A2 Hosting' => 'A2主机',
|
||||
'SiteGround' => '站点地',
|
||||
'Liquid Web' => '液态网络',
|
||||
'Kamatera' => '卡玛特拉',
|
||||
'IONOS' => 'IONOS',
|
||||
'InterServer' => '互联服务器',
|
||||
'Hostwinds' => '主机之风',
|
||||
'ScalaHosting' => '斯卡拉主机',
|
||||
'GreenGeeks' => '绿色极客'
|
||||
];
|
||||
$lang = $_GET['lang'] ?? 'en';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo htmlspecialchars($lang); ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="on">
|
||||
<link rel="dns-prefetch" href="//cdn.jsdelivr.net">
|
||||
<link rel="dns-prefetch" href="//whois.pconline.com.cn">
|
||||
<link rel="dns-prefetch" href="//forge.speedtest.cn">
|
||||
<link rel="dns-prefetch" href="//api-ipv4.ip.sb">
|
||||
<link rel="dns-prefetch" href="//api.ipify.org">
|
||||
<link rel="dns-prefetch" href="//api.ttt.sh">
|
||||
<link rel="dns-prefetch" href="//qqwry.api.skk.moe">
|
||||
<link rel="dns-prefetch" href="//d.skk.moe">
|
||||
<link rel="preconnect" href="https://forge.speedtest.cn">
|
||||
<link rel="preconnect" href="https://whois.pconline.com.cn">
|
||||
<link rel="preconnect" href="https://api-ipv4.ip.sb">
|
||||
<link rel="preconnect" href="https://api.ipify.org">
|
||||
<link rel="preconnect" href="https://api.ttt.sh">
|
||||
<link rel="preconnect" href="https://qqwry.api.skk.moe">
|
||||
<link rel="preconnect" href="https://d.skk.moe">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cbi-section {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #E6E6FA;
|
||||
color: var(--bs-btn-color);
|
||||
text-align: left;
|
||||
border-radius: 8px;
|
||||
height: 65px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 0 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.img-con {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.img-con img {
|
||||
width: 65px;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
color: #2dce89;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-style: italic;
|
||||
color: #fb6340;
|
||||
font-weight: 520;
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
letter-spacing: 0.02em;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.site-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: auto;
|
||||
height: 48px;
|
||||
max-height: 48px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-icon:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.ping-status {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
#ping-result {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#flag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.status-icons {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0 8px;
|
||||
height: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.img-con img {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.ping-status {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php if (in_array($lang, ['zh-cn', 'en', 'auto'])): ?>
|
||||
<fieldset class="cbi-section">
|
||||
<div class="status">
|
||||
<div class="site-status">
|
||||
<div class="img-con">
|
||||
<img src="./assets/neko/img/loading.svg" id="flag" class="pure-img" title="国旗" onclick="IP.getIpipnetIP()">
|
||||
</div>
|
||||
<div class="block">
|
||||
<p id="d-ip" class="ip-address">Checking...</p>
|
||||
<p id="ipip" class="info"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ping-status">
|
||||
<p id="ping-result"></p>
|
||||
</div>
|
||||
<div class="status-icons">
|
||||
<div class="site-icon">
|
||||
<img src="./assets/neko/img/site_icon_01.png" id="baidu-normal" class="status-icon" style="display: none;" onclick="pingHost('baidu', 'Baidu')">
|
||||
<img src="./assets/neko/img/site_icon1_01.png" id="baidu-gray" class="status-icon" onclick="pingHost('baidu', 'Baidu')">
|
||||
</div>
|
||||
<div class="site-icon">
|
||||
<img src="./assets/neko/img/site_icon_02.png" id="taobao-normal" class="status-icon" style="display: none;" onclick="pingHost('taobao', '淘宝')">
|
||||
<img src="./assets/neko/img/site_icon1_02.png" id="taobao-gray" class="status-icon" onclick="pingHost('taobao', '淘宝')">
|
||||
</div>
|
||||
<div class="site-icon">
|
||||
<img src="./assets/neko/img/site_icon_03.png" id="google-normal" class="status-icon" style="display: none;" onclick="pingHost('google', 'Google')">
|
||||
<img src="./assets/neko/img/site_icon1_03.png" id="google-gray" class="status-icon" onclick="pingHost('google', 'Google')">
|
||||
</div>
|
||||
<div class="site-icon">
|
||||
<img src="./assets/neko/img/site_icon_04.png" id="youtube-normal" class="status-icon" style="display: none;" onclick="pingHost('youtube', 'YouTube')">
|
||||
<img src="./assets/neko/img/site_icon1_04.png" id="youtube-gray" class="status-icon" onclick="pingHost('youtube', 'YouTube')">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
<script src="./assets/neko/js/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
const _IMG = './assets/neko/';
|
||||
const translate = <?php echo json_encode($translate, JSON_UNESCAPED_UNICODE); ?>;
|
||||
let cachedIP = null;
|
||||
let cachedInfo = null;
|
||||
let random = parseInt(Math.random() * 100000000);
|
||||
|
||||
const checkSiteStatus = {
|
||||
sites: {
|
||||
baidu: 'https://www.baidu.com',
|
||||
taobao: 'https://www.taobao.com',
|
||||
google: 'https://www.google.com',
|
||||
youtube: 'https://www.youtube.com'
|
||||
},
|
||||
|
||||
check: async function() {
|
||||
for (let [site, url] of Object.entries(this.sites)) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
document.getElementById(`${site}-normal`).style.display = 'inline';
|
||||
document.getElementById(`${site}-gray`).style.display = 'none';
|
||||
} catch (error) {
|
||||
document.getElementById(`${site}-normal`).style.display = 'none';
|
||||
document.getElementById(`${site}-gray`).style.display = 'inline';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function pingHost(site, siteName) {
|
||||
const url = checkSiteStatus.sites[site];
|
||||
const resultElement = document.getElementById('ping-result');
|
||||
|
||||
try {
|
||||
resultElement.innerHTML = `<span style="font-size: 20px">正在测试 ${siteName} 的连接延迟...`;
|
||||
resultElement.style.color = '#666';
|
||||
const startTime = performance.now();
|
||||
await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
const endTime = performance.now();
|
||||
const pingTime = Math.round(endTime - startTime);
|
||||
resultElement.innerHTML = `<span style="font-size: 20px">${siteName} 连接延迟: ${pingTime}ms</span>`;
|
||||
resultElement.style.color = pingTime > 200 ? '#ff6b6b' : '#20c997';
|
||||
} catch (error) {
|
||||
resultElement.innerHTML = `${siteName} 连接超时`;
|
||||
resultElement.style.color = '#ff6b6b';
|
||||
}
|
||||
}
|
||||
|
||||
let IP = {
|
||||
isRefreshing: false,
|
||||
fetchIP: async () => {
|
||||
try {
|
||||
const [ipifyResp, ipsbResp] = await Promise.all([
|
||||
IP.get('https://api.ipify.org?format=json', 'json'),
|
||||
IP.get('https://api-ipv4.ip.sb/geoip', 'json')
|
||||
]);
|
||||
|
||||
const ipData = ipifyResp.data.ip || ipsbResp.data.ip;
|
||||
cachedIP = ipData;
|
||||
document.getElementById('d-ip').innerHTML = ipData;
|
||||
return ipData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching IP:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
get: (url, type) =>
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store'
|
||||
}).then((resp) => {
|
||||
if (type === 'text')
|
||||
return Promise.all([resp.ok, resp.status, resp.text(), resp.headers]);
|
||||
else
|
||||
return Promise.all([resp.ok, resp.status, resp.json(), resp.headers]);
|
||||
}).then(([ok, status, data, headers]) => {
|
||||
if (ok) {
|
||||
return { ok, status, data, headers };
|
||||
} else {
|
||||
throw new Error(JSON.stringify(data.error));
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Error fetching data:", error);
|
||||
throw error;
|
||||
}),
|
||||
|
||||
Ipip: async (ip, elID) => {
|
||||
try {
|
||||
const resp = await IP.get(`https://api.ip.sb/geoip/${ip}`, 'json');
|
||||
cachedIP = ip;
|
||||
cachedInfo = resp.data;
|
||||
IP.updateUI(resp.data, elID);
|
||||
} catch (error) {
|
||||
console.error("Error in Ipip function:", error);
|
||||
}
|
||||
},
|
||||
|
||||
updateUI: (data, elID) => {
|
||||
let country = translate[data.country] || data.country;
|
||||
let isp = translate[data.isp] || data.isp;
|
||||
let asnOrganization = translate[data.asn_organization] || data.asn_organization;
|
||||
|
||||
if (data.country === 'Taiwan') {
|
||||
country = (navigator.language === 'en') ? 'China Taiwan' : '中国台湾';
|
||||
}
|
||||
const countryAbbr = data.country_code.toLowerCase();
|
||||
|
||||
document.getElementById(elID).innerHTML = `${country} ${isp} ${asnOrganization}`;
|
||||
$("#flag").attr("src", _IMG + "flags/" + countryAbbr + ".png");
|
||||
document.getElementById(elID).style.color = '#FF00FF';
|
||||
},
|
||||
|
||||
getIpipnetIP: async () => {
|
||||
if(IP.isRefreshing) return;
|
||||
|
||||
try {
|
||||
IP.isRefreshing = true;
|
||||
document.getElementById('d-ip').innerHTML = "Checking...";
|
||||
document.getElementById('ipip').innerHTML = "Loading...";
|
||||
$("#flag").attr("src", _IMG + "img/loading.svg");
|
||||
|
||||
const ip = await IP.fetchIP();
|
||||
await IP.Ipip(ip, 'ipip');
|
||||
} catch (error) {
|
||||
console.error("Error in getIpipnetIP function:", error);
|
||||
} finally {
|
||||
IP.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IP.getIpipnetIP();
|
||||
checkSiteStatus.check();
|
||||
setInterval(() => checkSiteStatus.check(), 30000);
|
||||
setInterval(IP.getIpipnetIP, 180000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user