Template
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53bffbd4fc | ||
|
|
53638cf122 | ||
|
|
1f1234d217 |
@@ -277,6 +277,9 @@ GitHub Actions workflows in `.github/workflows/`:
|
|||||||
|
|
||||||
## 📋 Fix / changelog (this fork)
|
## 📋 Fix / changelog (this fork)
|
||||||
|
|
||||||
|
### v2.6.5
|
||||||
|
* **Mieru install fix** — wait for `/var/run/mita.sock`, seed a bootstrap user (empty `users` caused `mita start` RPC EOF), retry apply/start with systemd restart.
|
||||||
|
|
||||||
### v2.6.4
|
### v2.6.4
|
||||||
* **Mieru (mita v3.28.0)** — optional per-server install from [enfein/mieru](https://github.com/enfein/mieru): native Debian/RPM package, no Docker. Marketplace + server card, TCP port at install, user connections with `mierus://` share links. Pinned release **v3.28.0** (GitHub has no v2.8.0 tag).
|
* **Mieru (mita v3.28.0)** — optional per-server install from [enfein/mieru](https://github.com/enfein/mieru): native Debian/RPM package, no Docker. Marketplace + server card, TCP port at install, user connections with `mierus://` share links. Pinned release **v3.28.0** (GitHub has no v2.8.0 tag).
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ else:
|
|||||||
application_path = os.path.dirname(__file__)
|
application_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||||
CURRENT_VERSION = "v2.6.4"
|
CURRENT_VERSION = "v2.6.5"
|
||||||
RELEASES_REPO_URL = repo_url()
|
RELEASES_REPO_URL = repo_url()
|
||||||
RELEASES_API_LATEST = api_latest_url()
|
RELEASES_API_LATEST = api_latest_url()
|
||||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||||
|
|||||||
+201
-54
@@ -14,12 +14,14 @@ import re
|
|||||||
import secrets
|
import secrets
|
||||||
import shlex
|
import shlex
|
||||||
import string
|
import string
|
||||||
|
import time
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIERU_RELEASE = '3.28.0'
|
MIERU_RELEASE = '3.28.0'
|
||||||
GITHUB_RELEASE = f'https://github.com/enfein/mieru/releases/download/v{MIERU_RELEASE}'
|
GITHUB_RELEASE = f'https://github.com/enfein/mieru/releases/download/v{MIERU_RELEASE}'
|
||||||
|
MITA_SOCK = '/var/run/mita.sock'
|
||||||
|
|
||||||
|
|
||||||
def _q(value):
|
def _q(value):
|
||||||
@@ -144,7 +146,8 @@ class MieruManager:
|
|||||||
b64 = base64.b64encode((content or '').encode('utf-8')).decode('ascii')
|
b64 = base64.b64encode((content or '').encode('utf-8')).decode('ascii')
|
||||||
script = (
|
script = (
|
||||||
f"mkdir -p $(dirname {_q(path)}) && "
|
f"mkdir -p $(dirname {_q(path)}) && "
|
||||||
f"echo {_q(b64)} | base64 -d > {_q(path)}"
|
f"echo {_q(b64)} | base64 -d > {_q(path)} && "
|
||||||
|
f"chmod 644 {_q(path)}"
|
||||||
)
|
)
|
||||||
out, err, code = self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
|
out, err, code = self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
@@ -176,14 +179,84 @@ class MieruManager:
|
|||||||
def _write_clients(self, clients):
|
def _write_clients(self, clients):
|
||||||
self._write_file(self.clients_path, json.dumps(clients, indent=2))
|
self._write_file(self.clients_path, json.dumps(clients, indent=2))
|
||||||
|
|
||||||
|
def _ensure_daemon(self, log=None):
|
||||||
|
"""Ensure mita systemd unit is up and RPC socket answers."""
|
||||||
|
self.ssh.run_sudo_command(
|
||||||
|
f"systemctl enable {self.SERVICE_NAME} 2>/dev/null || true",
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
self.ssh.run_sudo_command(
|
||||||
|
f"systemctl start {self.SERVICE_NAME} 2>/dev/null || "
|
||||||
|
f"systemctl restart {self.SERVICE_NAME} 2>/dev/null || true",
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
# Official package expects the operating user in group `mita`.
|
||||||
|
user_out, _, _ = self.ssh.run_command('id -un 2>/dev/null || echo root')
|
||||||
|
op_user = (user_out or 'root').strip() or 'root'
|
||||||
|
if op_user != 'root':
|
||||||
|
self.ssh.run_sudo_command(
|
||||||
|
f"usermod -a -G mita {_q(op_user)} 2>/dev/null || true",
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if not self._wait_for_rpc(timeout=45):
|
||||||
|
# Stale socket / crashed daemon — hard restart once.
|
||||||
|
self.ssh.run_sudo_command(
|
||||||
|
f"systemctl stop {self.SERVICE_NAME} 2>/dev/null || true; "
|
||||||
|
f"rm -f {_q(MITA_SOCK)} /var/run/mita/*.sock 2>/dev/null || true; "
|
||||||
|
f"systemctl start {self.SERVICE_NAME}",
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if not self._wait_for_rpc(timeout=45):
|
||||||
|
journal, _, _ = self.ssh.run_sudo_command(
|
||||||
|
f"journalctl -u {self.SERVICE_NAME} -n 40 --no-pager 2>&1",
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
'mita systemd daemon is not ready (RPC socket missing). '
|
||||||
|
f'journal: {(journal or "").strip()[-500:]}'
|
||||||
|
)
|
||||||
|
if log is not None:
|
||||||
|
log.append('mita daemon is active')
|
||||||
|
|
||||||
|
def _wait_for_rpc(self, timeout=30):
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
while time.time() < deadline:
|
||||||
|
sock_out, _, sock_code = self.ssh.run_sudo_command(
|
||||||
|
f"test -S {_q(MITA_SOCK)} && echo ok"
|
||||||
|
)
|
||||||
|
if sock_code == 0 and 'ok' in (sock_out or ''):
|
||||||
|
status_out, _, status_code = self.ssh.run_sudo_command(
|
||||||
|
'mita status 2>&1',
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
text = (status_out or '').upper()
|
||||||
|
if status_code == 0 and ('IDLE' in text or 'RUNNING' in text):
|
||||||
|
return True
|
||||||
|
# Socket exists but CLI still races — brief pause.
|
||||||
|
time.sleep(1.5)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _mita_cli(self, args, timeout=60):
|
||||||
|
"""Run mita CLI as root so group/socket ACL is not an issue."""
|
||||||
|
cmd = f"mita {' '.join(args)} 2>&1"
|
||||||
|
return self.ssh.run_sudo_command(cmd, timeout=timeout)
|
||||||
|
|
||||||
def _build_server_config(self, port, clients):
|
def _build_server_config(self, port, clients):
|
||||||
users = []
|
users = []
|
||||||
for c in clients:
|
for c in clients:
|
||||||
if c.get('enabled', True):
|
if not c.get('enabled', True):
|
||||||
users.append({
|
continue
|
||||||
'name': c.get('username') or c.get('name') or c.get('id'),
|
username = (c.get('username') or c.get('name') or c.get('id') or '').strip()
|
||||||
'password': c.get('password') or '',
|
password = (c.get('password') or '').strip()
|
||||||
})
|
if not username or not password:
|
||||||
|
continue
|
||||||
|
users.append({'name': username, 'password': password})
|
||||||
|
# mita rejects / crashes on empty users during `mita start` (RPC EOF).
|
||||||
|
if not users:
|
||||||
|
users = [{
|
||||||
|
'name': f'panel_{_rand_token(6)}',
|
||||||
|
'password': _rand_token(20),
|
||||||
|
}]
|
||||||
return {
|
return {
|
||||||
'portBindings': [{'port': int(port), 'protocol': 'TCP'}],
|
'portBindings': [{'port': int(port), 'protocol': 'TCP'}],
|
||||||
'users': users,
|
'users': users,
|
||||||
@@ -192,20 +265,75 @@ class MieruManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _apply_config(self, config, reload_only=False):
|
def _apply_config(self, config, reload_only=False):
|
||||||
|
self._ensure_daemon()
|
||||||
self._write_file(self.config_path, json.dumps(config, indent=2))
|
self._write_file(self.config_path, json.dumps(config, indent=2))
|
||||||
out, err, code = self.ssh.run_sudo_command(
|
out, err, code = self._mita_cli(
|
||||||
f"mita apply config {_q(self.config_path)} 2>&1",
|
['apply', 'config', _q(self.config_path)],
|
||||||
timeout=60,
|
timeout=60,
|
||||||
)
|
)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
raise RuntimeError((err or out or 'mita apply config failed').strip())
|
# Recover from transient EOF / unavailable RPC.
|
||||||
|
if self._is_rpc_error(out, err):
|
||||||
|
self._ensure_daemon()
|
||||||
|
out, err, code = self._mita_cli(
|
||||||
|
['apply', 'config', _q(self.config_path)],
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError((err or out or 'mita apply config failed').strip())
|
||||||
|
|
||||||
if reload_only:
|
if reload_only:
|
||||||
self.ssh.run_sudo_command('mita reload 2>/dev/null || true', timeout=30)
|
# users/loggingLevel can hot-reload; fall back to full restart.
|
||||||
else:
|
reload_out, reload_err, reload_code = self._mita_cli(['reload'], timeout=30)
|
||||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
if reload_code == 0:
|
||||||
out2, err2, code2 = self.ssh.run_sudo_command('mita start 2>&1', timeout=60)
|
return
|
||||||
if code2 != 0:
|
logger.info('mita reload failed, falling back to stop/start: %s',
|
||||||
raise RuntimeError((err2 or out2 or 'mita start failed').strip())
|
(reload_err or reload_out or '').strip())
|
||||||
|
|
||||||
|
self._restart_proxy()
|
||||||
|
|
||||||
|
def _is_rpc_error(self, *parts):
|
||||||
|
text = ' '.join(str(p or '') for p in parts).lower()
|
||||||
|
return any(token in text for token in (
|
||||||
|
'rpc error',
|
||||||
|
'unavailable',
|
||||||
|
'error reading from server',
|
||||||
|
'eof',
|
||||||
|
'no such file or directory',
|
||||||
|
'mita.sock',
|
||||||
|
'connection refused',
|
||||||
|
))
|
||||||
|
|
||||||
|
def _restart_proxy(self):
|
||||||
|
self._mita_cli(['stop'], timeout=30)
|
||||||
|
time.sleep(1)
|
||||||
|
last_err = ''
|
||||||
|
for attempt in range(1, 4):
|
||||||
|
out, err, code = self._mita_cli(['start'], timeout=60)
|
||||||
|
if code == 0:
|
||||||
|
# Confirm RUNNING (daemon may report success then die).
|
||||||
|
time.sleep(1)
|
||||||
|
if self._proxy_running():
|
||||||
|
return
|
||||||
|
last_err = (out or err or 'mita start returned ok but status is not RUNNING').strip()
|
||||||
|
else:
|
||||||
|
last_err = (err or out or 'mita start failed').strip()
|
||||||
|
if self._is_rpc_error(last_err) or attempt < 3:
|
||||||
|
self.ssh.run_sudo_command(
|
||||||
|
f"systemctl restart {self.SERVICE_NAME} 2>/dev/null || true",
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
self._wait_for_rpc(timeout=30)
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
journal, _, _ = self.ssh.run_sudo_command(
|
||||||
|
f"journalctl -u {self.SERVICE_NAME} -n 30 --no-pager 2>&1",
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
f'{last_err}. journal: {(journal or "").strip()[-400:]}'
|
||||||
|
)
|
||||||
|
|
||||||
def _sync_server(self, reload_only=True):
|
def _sync_server(self, reload_only=True):
|
||||||
meta = self._read_metadata()
|
meta = self._read_metadata()
|
||||||
@@ -261,10 +389,7 @@ fi
|
|||||||
if code != 0:
|
if code != 0:
|
||||||
raise RuntimeError(f'Failed to install mita package: {err or out}')
|
raise RuntimeError(f'Failed to install mita package: {err or out}')
|
||||||
log.append('Installed mita package')
|
log.append('Installed mita package')
|
||||||
self.ssh.run_sudo_command(
|
self._ensure_daemon(log)
|
||||||
f"systemctl enable --now {self.SERVICE_NAME} 2>/dev/null || true",
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_share_uri(self, host, port, username, password, name=''):
|
def _build_share_uri(self, host, port, username, password, name=''):
|
||||||
user = quote(username or '', safe='')
|
user = quote(username or '', safe='')
|
||||||
@@ -305,42 +430,51 @@ fi
|
|||||||
return {'status': 'error', 'message': 'Port must be between 1025 and 65535'}
|
return {'status': 'error', 'message': 'Port must be between 1025 and 65535'}
|
||||||
|
|
||||||
log = []
|
log = []
|
||||||
if not self._mita_installed():
|
|
||||||
self._install_package(log)
|
|
||||||
else:
|
|
||||||
log.append(f'mita already installed, configuring panel (v{MIERU_RELEASE})')
|
|
||||||
|
|
||||||
self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)}")
|
|
||||||
meta = {'port': port, 'release': MIERU_RELEASE}
|
|
||||||
self._write_metadata(meta)
|
|
||||||
self._write_clients([])
|
|
||||||
log.append(f'Prepared {self.base_dir}')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = self._build_server_config(port, [])
|
if not self._mita_installed():
|
||||||
|
self._install_package(log)
|
||||||
|
else:
|
||||||
|
log.append(f'mita already installed, configuring panel (v{MIERU_RELEASE})')
|
||||||
|
self._ensure_daemon(log)
|
||||||
|
|
||||||
|
self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)}")
|
||||||
|
meta = {'port': port, 'release': MIERU_RELEASE}
|
||||||
|
self._write_metadata(meta)
|
||||||
|
# Keep clients empty in panel DB, but seed a real mita user so start works.
|
||||||
|
self._write_clients([])
|
||||||
|
bootstrap = {
|
||||||
|
'id': secrets.token_hex(8),
|
||||||
|
'name': 'panel-bootstrap',
|
||||||
|
'username': f'panel_{_rand_token(6)}',
|
||||||
|
'password': _rand_token(20),
|
||||||
|
'enabled': True,
|
||||||
|
'bootstrap': True,
|
||||||
|
}
|
||||||
|
self._write_clients([bootstrap])
|
||||||
|
log.append(f'Prepared {self.base_dir}')
|
||||||
|
|
||||||
|
config = self._build_server_config(port, [bootstrap])
|
||||||
self._apply_config(config, reload_only=False)
|
self._apply_config(config, reload_only=False)
|
||||||
|
self._open_firewall_port(port)
|
||||||
|
log.append(f'Started mita proxy on TCP {port}')
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'message': f'Mieru v{MIERU_RELEASE} installed',
|
||||||
|
'log': log,
|
||||||
|
'port': str(port),
|
||||||
|
'release': MIERU_RELEASE,
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'status': 'error', 'message': str(e), 'log': log}
|
return {'status': 'error', 'message': str(e), 'log': log}
|
||||||
|
|
||||||
self._open_firewall_port(port)
|
|
||||||
log.append(f'Started mita proxy on TCP {port}')
|
|
||||||
return {
|
|
||||||
'status': 'success',
|
|
||||||
'message': f'Mieru v{MIERU_RELEASE} installed',
|
|
||||||
'log': log,
|
|
||||||
'port': str(port),
|
|
||||||
'release': MIERU_RELEASE,
|
|
||||||
}
|
|
||||||
|
|
||||||
def remove_container(self, protocol_type=None):
|
def remove_container(self, protocol_type=None):
|
||||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
||||||
self.ssh.run_sudo_command(f"rm -rf {_q(self.base_dir)}")
|
self.ssh.run_sudo_command(f"rm -rf {_q(self.base_dir)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def start_service(self):
|
def start_service(self):
|
||||||
out, err, code = self.ssh.run_sudo_command('mita start 2>&1', timeout=60)
|
self._ensure_daemon()
|
||||||
if code != 0:
|
self._restart_proxy()
|
||||||
raise RuntimeError((err or out or 'mita start failed').strip())
|
|
||||||
|
|
||||||
def stop_service(self):
|
def stop_service(self):
|
||||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
||||||
@@ -352,15 +486,16 @@ fi
|
|||||||
return self._read_file(self.config_path)
|
return self._read_file(self.config_path)
|
||||||
|
|
||||||
def save_server_config(self, protocol_type=None, config_text=''):
|
def save_server_config(self, protocol_type=None, config_text=''):
|
||||||
self._write_file(self.config_path, config_text or '')
|
raw = (config_text or '').strip()
|
||||||
out, err, code = self.ssh.run_sudo_command(
|
if not raw:
|
||||||
f"mita apply config {_q(self.config_path)} 2>&1",
|
raise RuntimeError('Config is empty')
|
||||||
timeout=60,
|
try:
|
||||||
)
|
parsed = json.loads(raw)
|
||||||
if code != 0:
|
except Exception as e:
|
||||||
raise RuntimeError((err or out or 'mita apply config failed').strip())
|
raise RuntimeError(f'Invalid JSON config: {e}') from e
|
||||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
if not isinstance(parsed, dict):
|
||||||
self.ssh.run_sudo_command('mita start 2>&1', timeout=60)
|
raise RuntimeError('Config must be a JSON object')
|
||||||
|
self._apply_config(parsed, reload_only=False)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ===================== CLIENTS =====================
|
# ===================== CLIENTS =====================
|
||||||
@@ -369,6 +504,8 @@ fi
|
|||||||
clients = self._read_clients()
|
clients = self._read_clients()
|
||||||
result = []
|
result = []
|
||||||
for c in clients:
|
for c in clients:
|
||||||
|
if c.get('bootstrap'):
|
||||||
|
continue
|
||||||
cname = c.get('name') or c.get('id')
|
cname = c.get('name') or c.get('id')
|
||||||
result.append({
|
result.append({
|
||||||
'clientId': c.get('id'),
|
'clientId': c.get('id'),
|
||||||
@@ -413,7 +550,7 @@ fi
|
|||||||
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
||||||
clients = self._read_clients()
|
clients = self._read_clients()
|
||||||
client = next((c for c in clients if c.get('id') == client_id), None)
|
client = next((c for c in clients if c.get('id') == client_id), None)
|
||||||
if not client:
|
if not client or client.get('bootstrap'):
|
||||||
return ''
|
return ''
|
||||||
if not client.get('enabled', True):
|
if not client.get('enabled', True):
|
||||||
return ''
|
return ''
|
||||||
@@ -424,6 +561,16 @@ fi
|
|||||||
|
|
||||||
def remove_client(self, protocol_type, client_id):
|
def remove_client(self, protocol_type, client_id):
|
||||||
clients = [c for c in self._read_clients() if c.get('id') != client_id]
|
clients = [c for c in self._read_clients() if c.get('id') != client_id]
|
||||||
|
# Keep at least bootstrap so mita never has empty users.
|
||||||
|
if not any(not c.get('bootstrap') for c in clients) and not any(c.get('bootstrap') for c in clients):
|
||||||
|
clients.append({
|
||||||
|
'id': secrets.token_hex(8),
|
||||||
|
'name': 'panel-bootstrap',
|
||||||
|
'username': f'panel_{_rand_token(6)}',
|
||||||
|
'password': _rand_token(20),
|
||||||
|
'enabled': True,
|
||||||
|
'bootstrap': True,
|
||||||
|
})
|
||||||
self._write_clients(clients)
|
self._write_clients(clients)
|
||||||
self._sync_server(reload_only=True)
|
self._sync_server(reload_only=True)
|
||||||
return True
|
return True
|
||||||
|
|||||||
+26
-26
@@ -314,6 +314,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mieru Card -->
|
||||||
|
<div class="card card-hover protocol-card protocol-mieru" id="proto-mieru">
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
|
<div class="protocol-icon">{{ icon('zap') }}</div>
|
||||||
|
<div class="flex gap-sm" id="mieru-ctrl" style="display:none!important;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="protocol-name">Mieru <span
|
||||||
|
style="font-size:0.65rem; background:var(--accent, #6366f1); color:#fff; padding:2px 6px; border-radius:8px; vertical-align:middle;">v3.28.0</span></div>
|
||||||
|
<div class="protocol-desc">
|
||||||
|
{{ _('mieru_desc') }}
|
||||||
|
</div>
|
||||||
|
<div class="protocol-status" id="mieru-status">
|
||||||
|
<span class="badge badge-warn"><span class="badge-dot"></span> {{ _('not_checked') }}</span>
|
||||||
|
</div>
|
||||||
|
<div id="mieru-info" class="hidden">
|
||||||
|
<div class="protocol-info" id="mieru-info-grid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-sm" id="mieru-actions">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="openInstallModal('mieru')" id="mieru-install-btn"
|
||||||
|
style="flex:1">
|
||||||
|
{{ _('install') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hysteria Card -->
|
<!-- Hysteria Card -->
|
||||||
<div class="card card-hover protocol-card protocol-hysteria" id="proto-hysteria">
|
<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 style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
@@ -363,31 +388,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mieru Card -->
|
|
||||||
<div class="card card-hover protocol-card protocol-mieru" id="proto-mieru">
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
|
||||||
<div class="protocol-icon">{{ icon('zap') }}</div>
|
|
||||||
<div class="flex gap-sm" id="mieru-ctrl" style="display:none!important;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="protocol-name">Mieru <span
|
|
||||||
style="font-size:0.65rem; background:var(--accent, #6366f1); color:#fff; padding:2px 6px; border-radius:8px; vertical-align:middle;">v3.28.0</span></div>
|
|
||||||
<div class="protocol-desc">
|
|
||||||
{{ _('mieru_desc') }}
|
|
||||||
</div>
|
|
||||||
<div class="protocol-status" id="mieru-status">
|
|
||||||
<span class="badge badge-warn"><span class="badge-dot"></span> {{ _('not_checked') }}</span>
|
|
||||||
</div>
|
|
||||||
<div id="mieru-info" class="hidden">
|
|
||||||
<div class="protocol-info" id="mieru-info-grid"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-sm" id="mieru-actions">
|
|
||||||
<button class="btn btn-primary btn-sm" onclick="openInstallModal('mieru')" id="mieru-install-btn"
|
|
||||||
style="flex:1">
|
|
||||||
{{ _('install') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- WireGuard Card -->
|
<!-- WireGuard Card -->
|
||||||
<div class="card card-hover protocol-card protocol-wireguard" id="proto-wireguard">
|
<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);">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||||
@@ -1199,9 +1199,9 @@
|
|||||||
{ proto: 'awg_legacy', category: 'protocols', icon: 'radio', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
{ proto: 'awg_legacy', category: 'protocols', icon: 'radio', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
||||||
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
||||||
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
||||||
|
{ proto: 'mieru', category: 'protocols', icon: 'zap', title: 'Mieru', descKey: 'mieru_desc', badge: 'v3.28.0' },
|
||||||
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
||||||
{ proto: 'naiveproxy', category: 'protocols', icon: 'link', title: 'NaiveProxy', descKey: 'naiveproxy_desc', badge: 'STABLE' },
|
{ proto: 'naiveproxy', category: 'protocols', icon: 'link', title: 'NaiveProxy', descKey: 'naiveproxy_desc', badge: 'STABLE' },
|
||||||
{ proto: 'mieru', category: 'protocols', icon: 'zap', title: 'Mieru', descKey: 'mieru_desc', badge: 'v3.28.0' },
|
|
||||||
{ proto: 'wireguard', category: 'protocols', icon: 'lock', title: 'WireGuard', descKey: 'wireguard_desc' },
|
{ proto: 'wireguard', category: 'protocols', icon: 'lock', title: 'WireGuard', descKey: 'wireguard_desc' },
|
||||||
{ proto: 'dns', category: 'services', icon: 'search', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
{ proto: 'dns', category: 'services', icon: 'search', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
||||||
{ proto: 'adguard', category: 'services', icon: 'shield-check', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
{ proto: 'adguard', category: 'services', icon: 'shield-check', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
||||||
|
|||||||
@@ -384,6 +384,12 @@
|
|||||||
"naiveproxy_dns_hint": "قبل از نصب این رکورد DNS را بسازید:",
|
"naiveproxy_dns_hint": "قبل از نصب این رکورد DNS را بسازید:",
|
||||||
"naiveproxy_ports_warning": "هشدار: برای کار پایدار پورتهای TCP آزاد ۸۰ و ۴۴۳ لازم است (Let\u0027s Encrypt روی ۸۰، پروکسی HTTPS روی ۴۴۳).",
|
"naiveproxy_ports_warning": "هشدار: برای کار پایدار پورتهای TCP آزاد ۸۰ و ۴۴۳ لازم است (Let\u0027s Encrypt روی ۸۰، پروکسی HTTPS روی ۴۴۳).",
|
||||||
"naiveproxy_client_hint": "نسخه پایدار. از v2rayN استفاده نکنید. در Karing تأیید شده. سایر کلاینتها آزمایش نشدهاند.",
|
"naiveproxy_client_hint": "نسخه پایدار. از v2rayN استفاده نکنید. در Karing تأیید شده. سایر کلاینتها آزمایش نشدهاند.",
|
||||||
|
"mieru_desc": "Mieru (mita v3.28.0) — پروکسی TCP بومی enfein/mieru. بدون Docker. کلاینت: mieru یا Clash.Meta/mihomo.",
|
||||||
|
"mieru_version": "نسخه",
|
||||||
|
"mieru_port_hint": "پورت TCP برای mita (۱۰۲۵–۶۵۵۳۵). در فایروال سرور باز کنید.",
|
||||||
|
"mieru_install_hint": "بسته رسمی mita را از GitHub v3.28.0 نصب میکند (Debian/RPM). لینوکس با systemd لازم است.",
|
||||||
|
"mieru_ports_warning": "هشدار: پورت TCP انتخابی باید آزاد و در فایروال مجاز باشد.",
|
||||||
|
"mieru_client_hint": "لینکها با فرمت mierus://. در کلاینت mieru یا Clash.Meta (type: mieru) وارد کنید.",
|
||||||
"server_ssl_domain": "دامنه SSL (پیشفرض)",
|
"server_ssl_domain": "دامنه SSL (پیشفرض)",
|
||||||
"server_ssl_email": "ایمیل SSL (پیشفرض)",
|
"server_ssl_email": "ایمیل SSL (پیشفرض)",
|
||||||
"server_ssl_hint": "هنگام نصب Hysteria / NGINX روی این سرور استفاده میشود (Let\u0027s Encrypt).",
|
"server_ssl_hint": "هنگام نصب Hysteria / NGINX روی این سرور استفاده میشود (Let\u0027s Encrypt).",
|
||||||
|
|||||||
@@ -384,6 +384,12 @@
|
|||||||
"naiveproxy_dns_hint": "Créez cet enregistrement DNS avant l\u0027installation :",
|
"naiveproxy_dns_hint": "Créez cet enregistrement DNS avant l\u0027installation :",
|
||||||
"naiveproxy_ports_warning": "Attention : les ports TCP 80 et 443 doivent être libres pour un fonctionnement stable (Let\u0027s Encrypt sur 80, proxy HTTPS sur 443).",
|
"naiveproxy_ports_warning": "Attention : les ports TCP 80 et 443 doivent être libres pour un fonctionnement stable (Let\u0027s Encrypt sur 80, proxy HTTPS sur 443).",
|
||||||
"naiveproxy_client_hint": "Version stable. N\u0027utilisez PAS v2rayN. Fonctionne avec Karing. Autres clients non testés.",
|
"naiveproxy_client_hint": "Version stable. N\u0027utilisez PAS v2rayN. Fonctionne avec Karing. Autres clients non testés.",
|
||||||
|
"mieru_desc": "Mieru (mita v3.28.0) — proxy TCP natif enfein/mieru. Pas besoin de Docker. Client : mieru ou Clash.Meta/mihomo.",
|
||||||
|
"mieru_version": "Version",
|
||||||
|
"mieru_port_hint": "Port TCP pour mita (1025–65535). Ouvrez-le dans le pare-feu du serveur.",
|
||||||
|
"mieru_install_hint": "Installe le paquet officiel mita depuis GitHub v3.28.0 (Debian/RPM). Linux avec systemd requis.",
|
||||||
|
"mieru_ports_warning": "Attention : le port TCP choisi doit être libre et autorisé dans le pare-feu.",
|
||||||
|
"mieru_client_hint": "Liens au format mierus://. Importez dans le client mieru ou Clash.Meta (type: mieru).",
|
||||||
"server_ssl_domain": "Domaine SSL (par défaut)",
|
"server_ssl_domain": "Domaine SSL (par défaut)",
|
||||||
"server_ssl_email": "Email SSL (par défaut)",
|
"server_ssl_email": "Email SSL (par défaut)",
|
||||||
"server_ssl_hint": "Utilisé lors de l\u0027installation de Hysteria / NGINX sur ce serveur (Let\u0027s Encrypt).",
|
"server_ssl_hint": "Utilisé lors de l\u0027installation de Hysteria / NGINX sur ce serveur (Let\u0027s Encrypt).",
|
||||||
|
|||||||
@@ -384,6 +384,12 @@
|
|||||||
"naiveproxy_dns_hint": "安装前请创建此 DNS 记录:",
|
"naiveproxy_dns_hint": "安装前请创建此 DNS 记录:",
|
||||||
"naiveproxy_ports_warning": "注意:稳定运行需要空闲的 TCP 端口 80 和 443(Let\u0027s Encrypt 使用 80,HTTPS 代理使用 443)。",
|
"naiveproxy_ports_warning": "注意:稳定运行需要空闲的 TCP 端口 80 和 443(Let\u0027s Encrypt 使用 80,HTTPS 代理使用 443)。",
|
||||||
"naiveproxy_client_hint": "稳定版。请勿使用 v2rayN。已确认 Karing 可用。其他客户端未测试。",
|
"naiveproxy_client_hint": "稳定版。请勿使用 v2rayN。已确认 Karing 可用。其他客户端未测试。",
|
||||||
|
"mieru_desc": "Mieru(mita v3.28.0)— enfein/mieru 原生 TCP 代理,无需 Docker。客户端:mieru 或 Clash.Meta/mihomo。",
|
||||||
|
"mieru_version": "版本",
|
||||||
|
"mieru_port_hint": "mita 的 TCP 端口(1025–65535)。请在服务器防火墙中放行。",
|
||||||
|
"mieru_install_hint": "从 GitHub v3.28.0 安装官方 mita 包(Debian/RPM)。需要带 systemd 的 Linux。",
|
||||||
|
"mieru_ports_warning": "注意:所选 TCP 端口必须空闲,并在防火墙中允许。",
|
||||||
|
"mieru_client_hint": "分享链接为 mierus:// 格式。可导入 mieru 客户端或 Clash.Meta(type: mieru)。",
|
||||||
"server_ssl_domain": "SSL 域名(默认)",
|
"server_ssl_domain": "SSL 域名(默认)",
|
||||||
"server_ssl_email": "SSL 邮箱(默认)",
|
"server_ssl_email": "SSL 邮箱(默认)",
|
||||||
"server_ssl_hint": "安装 Hysteria / NGINX 时自动填入(Let\u0027s Encrypt)。",
|
"server_ssl_hint": "安装 Hysteria / NGINX 时自动填入(Let\u0027s Encrypt)。",
|
||||||
|
|||||||
Reference in New Issue
Block a user