Template
Add Hysteria 2 protocol with admin domain SSL via Let's Encrypt.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
{% for s in servers %}
|
||||
{% set server_idx = loop.index0 %}
|
||||
{% for key, info in (s.protocols or {}).items() %}
|
||||
{% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt'] %}
|
||||
{% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt','hysteria'] %}
|
||||
<option value="{{ key }}|{{ server_idx }}">{{ s.name or s.host }} — {{ key }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -117,10 +117,10 @@
|
||||
document.getElementById('configText').textContent = result.config;
|
||||
document.getElementById('vpnLinkText').textContent = currentVpnLink;
|
||||
|
||||
// Telemt (MTProxy) doesn't have a "VPN Link" (vpn://) format in Amnezia
|
||||
// Telemt / Hysteria use share links, not vpn:// Amnezia format
|
||||
const vpnTab = document.querySelectorAll('.config-tab')[1];
|
||||
const vpnPanel = document.getElementById('panel-vpn');
|
||||
if (proto === 'telemt') {
|
||||
if (proto === 'telemt' || String(proto || '').split('__')[0] === 'hysteria') {
|
||||
vpnTab.style.display = 'none';
|
||||
} else {
|
||||
vpnTab.style.display = '';
|
||||
|
||||
+71
-8
@@ -294,6 +294,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hysteria Card -->
|
||||
<div class="card card-hover protocol-card protocol-hysteria" id="proto-hysteria">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||
<div class="protocol-icon">🌀</div>
|
||||
<div class="flex gap-sm" id="hysteria-ctrl" style="display:none!important;"></div>
|
||||
</div>
|
||||
<div class="protocol-name">Hysteria 2</div>
|
||||
<div class="protocol-desc">
|
||||
{{ _('hysteria_desc') }}
|
||||
</div>
|
||||
<div class="protocol-status" id="hysteria-status">
|
||||
<span class="badge badge-warn"><span class="badge-dot"></span> {{ _('not_checked') }}</span>
|
||||
</div>
|
||||
<div id="hysteria-info" class="hidden">
|
||||
<div class="protocol-info" id="hysteria-info-grid"></div>
|
||||
</div>
|
||||
<div class="flex gap-sm" id="hysteria-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="openInstallModal('hysteria')" id="hysteria-install-btn"
|
||||
style="flex:1">
|
||||
{{ _('install') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WireGuard Card -->
|
||||
<div class="card card-hover protocol-card protocol-wireguard" id="proto-wireguard">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||
@@ -655,6 +679,20 @@
|
||||
<div class="form-hint">{{ _('nginx_install_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div id="hysteriaOptions"
|
||||
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('hysteria_domain') }}</label>
|
||||
<input class="form-input" type="text" id="installHysteriaDomain" placeholder="vpn.example.com" oninput="updateHysteriaDnsHint()">
|
||||
<div class="form-hint" id="hysteriaDnsHint"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('hysteria_email') }}</label>
|
||||
<input class="form-input" type="email" id="installHysteriaEmail" placeholder="admin@example.com">
|
||||
</div>
|
||||
<div class="form-hint">{{ _('hysteria_install_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('installModal')">{{ _('cancel') }}</button>
|
||||
<button class="btn btn-primary" onclick="installProtocol()" id="installBtn">
|
||||
@@ -934,6 +972,7 @@
|
||||
{ proto: 'awg_legacy', category: 'protocols', icon: '📡', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
||||
{ proto: 'xray', category: 'protocols', icon: '⚡', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
||||
{ proto: 'telemt', category: 'protocols', icon: '✈', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
||||
{ proto: 'hysteria', category: 'protocols', icon: '🌀', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
||||
{ proto: 'wireguard', category: 'protocols', icon: '🔒', title: 'WireGuard', descKey: 'wireguard_desc' },
|
||||
{ proto: 'dns', category: 'services', icon: '🔍', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
||||
{ proto: 'adguard', category: 'services', icon: '🛡️', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
||||
@@ -969,7 +1008,7 @@
|
||||
}
|
||||
|
||||
function isMultiInstanceBase(proto) {
|
||||
return ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'socks5'].includes(protoBase(proto));
|
||||
return ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'socks5'].includes(protoBase(proto));
|
||||
}
|
||||
|
||||
function nextSuggestedPort(base, fallback) {
|
||||
@@ -1174,6 +1213,7 @@
|
||||
case 'awg_legacy': title = 'AmneziaWG Legacy'; break;
|
||||
case 'xray': title = 'Xray'; break;
|
||||
case 'telemt': title = 'Telemt'; break;
|
||||
case 'hysteria': title = 'Hysteria 2'; break;
|
||||
case 'wireguard': title = 'WireGuard'; break;
|
||||
case 'dns': title = 'AmneziaDNS'; break;
|
||||
case 'socks5': title = 'SOCKS5 Proxy'; break;
|
||||
@@ -1233,7 +1273,7 @@
|
||||
|
||||
for (const [proto, info] of orderedProtocolEntries(data.protocols)) {
|
||||
updateProtocolCard(proto, info);
|
||||
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard'].includes(protoBase(proto));
|
||||
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'wireguard'].includes(protoBase(proto));
|
||||
if (info.container_running && isVPN) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = proto;
|
||||
@@ -1462,6 +1502,9 @@
|
||||
grid = buildServiceInfoGrid(proto, info);
|
||||
} else if (info.port) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
||||
if (protoBase(proto) === 'hysteria' && info.domain) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('hysteria_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||
}
|
||||
}
|
||||
if (!isService && info.clients_count !== undefined) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('connections')}</span><span class="protocol-info-value">${info.clients_count}</span></div>`;
|
||||
@@ -1691,6 +1734,14 @@
|
||||
hint.innerHTML = `${_('nginx_dns_hint')} <code>A ${domain} ${SERVER_HOST}</code>`;
|
||||
}
|
||||
|
||||
function updateHysteriaDnsHint() {
|
||||
const input = document.getElementById('installHysteriaDomain');
|
||||
const hint = document.getElementById('hysteriaDnsHint');
|
||||
if (!input || !hint) return;
|
||||
const domain = (input.value || '').trim() || 'vpn.example.com';
|
||||
hint.innerHTML = `${_('hysteria_dns_hint')} <code>A ${domain} ${SERVER_HOST}</code>`;
|
||||
}
|
||||
|
||||
function openInstallModal(proto, installAnother = false) {
|
||||
const base = protoBase(proto);
|
||||
currentInstallProto = installAnother ? base : proto;
|
||||
@@ -1705,11 +1756,13 @@
|
||||
const socks5Opts = document.getElementById('socks5Options');
|
||||
const adguardOpts = document.getElementById('adguardOptions');
|
||||
const nginxOpts = document.getElementById('nginxOptions');
|
||||
const hysteriaOpts = document.getElementById('hysteriaOptions');
|
||||
|
||||
telemtOpts.style.display = 'none';
|
||||
socks5Opts.style.display = 'none';
|
||||
adguardOpts.style.display = 'none';
|
||||
nginxOpts.style.display = 'none';
|
||||
hysteriaOpts.style.display = 'none';
|
||||
|
||||
if (base === 'dns') {
|
||||
portLabel.textContent = _('port') + ' (Internal)';
|
||||
@@ -1746,6 +1799,13 @@
|
||||
portHint.textContent = _('nginx_port_hint');
|
||||
nginxOpts.style.display = 'block';
|
||||
updateNginxDnsHint();
|
||||
} else if (base === 'hysteria') {
|
||||
portLabel.textContent = _('port') + ' (UDP)';
|
||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
|
||||
portInput.disabled = false;
|
||||
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('hysteria_port_hint');
|
||||
hysteriaOpts.style.display = 'block';
|
||||
updateHysteriaDnsHint();
|
||||
} else {
|
||||
portLabel.textContent = _('port') + ' (UDP)';
|
||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 55424) : '55424';
|
||||
@@ -1812,6 +1872,9 @@
|
||||
} else if (protoBase(currentInstallProto) === 'nginx') {
|
||||
params.nginx_domain = document.getElementById('installNginxDomain').value.trim();
|
||||
params.nginx_email = document.getElementById('installNginxEmail').value.trim();
|
||||
} else if (protoBase(currentInstallProto) === 'hysteria') {
|
||||
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
|
||||
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
|
||||
}
|
||||
const result = await apiCall(`/api/servers/${SERVER_ID}/install`, 'POST', params);
|
||||
clearInterval(progressInterval);
|
||||
@@ -2025,7 +2088,7 @@
|
||||
const sent = userData.dataSent || '';
|
||||
const initial = name.charAt(0).toUpperCase();
|
||||
const enabled = (client.enabled !== undefined) ? client.enabled : (userData.enabled !== false);
|
||||
const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt';
|
||||
const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria';
|
||||
const assignedUser = client.assigned_user || '';
|
||||
|
||||
let metaHtml = '';
|
||||
@@ -2063,7 +2126,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="client-actions">
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt'})" title="${_('config')}">📄</button>
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria'})" title="${_('config')}">📄</button>
|
||||
${proto === 'telemt' ? `<button class="btn btn-secondary btn-sm btn-icon" onclick="editConnection('${escapeJs(client.clientId)}')" title="${_('edit')}">✏️</button>` : ''}
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleConnection('${escapeJs(client.clientId)}', ${!enabled})" title="${toggleTitle}">${toggleIcon}</button>
|
||||
<button class="btn btn-danger btn-sm btn-icon" onclick="removeConnection('${escapeJs(client.clientId)}')" title="${_('delete')}">🗑</button>
|
||||
@@ -2115,8 +2178,8 @@
|
||||
document.getElementById('configModalTitle').textContent = `${_('config')} — ${connName}`;
|
||||
document.getElementById('configText').textContent = result.config;
|
||||
document.getElementById('vpnLinkText').textContent = currentVpnLink;
|
||||
document.getElementById('panel-vpn').style.display = (proto === 'telemt' ? 'none' : '');
|
||||
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' ? 'none' : '');
|
||||
document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
|
||||
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
|
||||
switchConfigTab('conf');
|
||||
openModal('configModal');
|
||||
generateQR(result.config);
|
||||
@@ -2194,9 +2257,9 @@
|
||||
const proto = document.getElementById('connProtoSelect').value;
|
||||
|
||||
// Restore tabs visibility first
|
||||
document.getElementById('panel-vpn').style.display = (proto === 'telemt' ? 'none' : '');
|
||||
document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
|
||||
document.getElementById('panel-qr').style.display = '';
|
||||
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' ? 'none' : '');
|
||||
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
|
||||
document.querySelectorAll('.config-tab')[2].style.display = '';
|
||||
|
||||
// Client was created via native Amnezia app — private key is not stored server-side
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
{% for s in servers %}
|
||||
{% set server_idx = loop.index0 %}
|
||||
{% for key, info in (s.protocols or {}).items() %}
|
||||
{% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt'] %}
|
||||
{% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt','hysteria'] %}
|
||||
<option value="{{ key }}|{{ server_idx }}"
|
||||
{% if settings.guest.create_protocol == key and settings.guest.create_server_id == server_idx %}selected{% endif %}>
|
||||
{{ s.name or s.host }} — {{ key }}
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
const select = document.getElementById(selectId);
|
||||
const group = groupId ? document.getElementById(groupId) : null;
|
||||
select.innerHTML = '';
|
||||
const vpnBases = new Set(['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard']);
|
||||
const vpnBases = new Set(['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'wireguard']);
|
||||
let count = 0;
|
||||
|
||||
const server = (serverId !== '' && serversData[serverId]) ? serversData[serverId] : null;
|
||||
@@ -608,7 +608,7 @@
|
||||
if (!vpnBases.has(base)) continue;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = key;
|
||||
opt.textContent = key === 'awg' ? 'AmneziaWG' : (key === 'awg2' ? 'AmneziaWG 2.0' : (key === 'awg_legacy' ? 'AWG Legacy' : (key === 'xray' ? 'Xray' : (key === 'wireguard' ? 'WireGuard' : (key === 'telemt' ? 'Telemt' : key.toUpperCase())))));
|
||||
opt.textContent = key === 'awg' ? 'AmneziaWG' : (key === 'awg2' ? 'AmneziaWG 2.0' : (key === 'awg_legacy' ? 'AWG Legacy' : (key === 'xray' ? 'Xray' : (key === 'wireguard' ? 'WireGuard' : (key === 'telemt' ? 'Telemt' : (key === 'hysteria' ? 'Hysteria 2' : key.toUpperCase()))))));
|
||||
select.appendChild(opt);
|
||||
count++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user