dsh-mobile 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/CONTRIBUTORS.md +7 -3
- package/README.en.md +27 -14
- package/README.md +27 -15
- package/SECURITY.md +7 -5
- package/docs/ATTACH_EXISTING_FRPS.en.md +44 -0
- package/docs/ATTACH_EXISTING_FRPS.md +98 -0
- package/docs/README.md +3 -0
- package/docs/SELF_HOSTED_FRP.en.md +3 -1
- package/docs/SELF_HOSTED_FRP.md +7 -1
- package/lib/cli.js +52 -15
- package/lib/cli.js.map +1 -1
- package/lib/client.js +1339 -162
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +393 -10
- package/lib/index.mjs +2178 -593
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js +1 -1
- package/lib/mobile-compat.js.map +1 -1
- package/package.json +25 -21
package/lib/client.js
CHANGED
|
@@ -562,7 +562,7 @@ window.__ModuleLoader__.load({
|
|
|
562
562
|
selfHostedConnections: "Self-hosted connection",
|
|
563
563
|
advanced: "Advanced",
|
|
564
564
|
frpName: "Self-hosted FRP",
|
|
565
|
-
frpDescription: "A restricted FRP channel for this DSH gateway only. You manage the VPS and public
|
|
565
|
+
frpDescription: "A restricted FRP channel for this DSH gateway only. You manage the VPS and public address.",
|
|
566
566
|
prepareFrp: "Configure self-hosted FRP",
|
|
567
567
|
frpStep1Title: "1 · Connection details",
|
|
568
568
|
frpStep1Text: "Enter the VPS address, frps control port, shared token, and public HTTPS origin.",
|
|
@@ -649,7 +649,7 @@ window.__ModuleLoader__.load({
|
|
|
649
649
|
selfHostedConnections: "Connessione autogestita",
|
|
650
650
|
advanced: "Avanzato",
|
|
651
651
|
frpName: "FRP autogestito",
|
|
652
|
-
frpDescription: "Canale FRP limitato al solo gateway DSH. VPS e
|
|
652
|
+
frpDescription: "Canale FRP limitato al solo gateway DSH. VPS e indirizzo pubblico restano sotto il tuo controllo.",
|
|
653
653
|
prepareFrp: "Configura FRP autogestito",
|
|
654
654
|
frpStep1Title: "1 · Dati di connessione",
|
|
655
655
|
frpStep1Text: "Inserisci indirizzo VPS, porta di controllo frps, token condiviso e origine HTTPS pubblica.",
|
|
@@ -985,7 +985,12 @@ window.__ModuleLoader__.load({
|
|
|
985
985
|
phone: "The computer cannot determine whether the router isolates the phone.",
|
|
986
986
|
phoneAction: "Confirm the phone and computer use the same network, then disable guest-network or AP isolation.",
|
|
987
987
|
reportTitle: "DSH Mobile diagnostic report",
|
|
988
|
-
generated: "Generated"
|
|
988
|
+
generated: "Generated",
|
|
989
|
+
certificateOk: "The FRP entry certificate is valid.",
|
|
990
|
+
certificateExpiring: "The FRP entry certificate expires soon.",
|
|
991
|
+
certificateExpired: "The FRP entry certificate has expired.",
|
|
992
|
+
certificateUnknown: "The FRP entry certificate could not be read.",
|
|
993
|
+
certificateAction: "Reconnect the FRP channel to re-issue the certificate, or follow the attach runbook on the VPS."
|
|
989
994
|
},
|
|
990
995
|
it: {
|
|
991
996
|
versions: "Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.",
|
|
@@ -1008,7 +1013,12 @@ window.__ModuleLoader__.load({
|
|
|
1008
1013
|
phone: "Il computer non può stabilire se il router isola il telefono.",
|
|
1009
1014
|
phoneAction: "Verifica che telefono e computer usino la stessa rete, poi disattiva rete ospiti o isolamento AP.",
|
|
1010
1015
|
reportTitle: "Report diagnostico DSH Mobile",
|
|
1011
|
-
generated: "Generato"
|
|
1016
|
+
generated: "Generato",
|
|
1017
|
+
certificateOk: "Il certificato di ingresso FRP è valido.",
|
|
1018
|
+
certificateExpiring: "Il certificato di ingresso FRP scade a breve.",
|
|
1019
|
+
certificateExpired: "Il certificato di ingresso FRP è scaduto.",
|
|
1020
|
+
certificateUnknown: "Impossibile leggere il certificato di ingresso FRP.",
|
|
1021
|
+
certificateAction: "Riconnetti il canale FRP per rigenerare il certificato, oppure segui il runbook attach sul VPS."
|
|
1012
1022
|
},
|
|
1013
1023
|
zh: {
|
|
1014
1024
|
versions: "已显示插件、DSH 和 Android App 最低版本。",
|
|
@@ -1031,7 +1041,12 @@ window.__ModuleLoader__.load({
|
|
|
1031
1041
|
phone: "电脑无法判断路由器是否隔离了手机。",
|
|
1032
1042
|
phoneAction: "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。",
|
|
1033
1043
|
reportTitle: "DSH Mobile 诊断报告",
|
|
1034
|
-
generated: "生成时间"
|
|
1044
|
+
generated: "生成时间",
|
|
1045
|
+
certificateOk: "FRP 入口证书有效。",
|
|
1046
|
+
certificateExpiring: "FRP 入口证书即将过期。",
|
|
1047
|
+
certificateExpired: "FRP 入口证书已过期。",
|
|
1048
|
+
certificateUnknown: "无法读取 FRP 入口证书。",
|
|
1049
|
+
certificateAction: "重新连接 FRP 通道即可重新签发证书,或按接入清单在 VPS 侧处理。"
|
|
1035
1050
|
}
|
|
1036
1051
|
};
|
|
1037
1052
|
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
@@ -1379,7 +1394,9 @@ window.__ModuleLoader__.load({
|
|
|
1379
1394
|
"remote-off": ["Remote access through {provider} is currently off.", ""],
|
|
1380
1395
|
"remote-origin-ready": ["The private HTTP backend is listening; public HTTPS is not verified.", "Verify your reverse proxy, public certificate, external Host including port, and WebSocket access from your phone. Keep the HTTP backend private."],
|
|
1381
1396
|
"remote-ready": ["{provider} endpoint {endpointSuffix} is reachable in about {latencyMs} ms.", ""],
|
|
1397
|
+
"remote-ready-via-proxy": ["The computer reached {provider} endpoint {endpointSuffix} through its configured proxy in about {latencyMs} ms; phone reachability is not verified.", "Test the connection from the phone’s network."],
|
|
1382
1398
|
"remote-rate-limited": ["{provider} endpoint {endpointSuffix} is reachable, but this check observed rate limiting.", "Try again later; older sessions load on demand to reduce traffic."],
|
|
1399
|
+
"remote-rate-limited-via-proxy": ["The computer reached {provider} endpoint {endpointSuffix} through its proxy, but the service rate-limited this check; phone reachability is not verified.", "Try again later and test from the phone’s network."],
|
|
1383
1400
|
"remote-fake-ip": ["The Tailscale address is intercepted by the current VPN or DNS proxy, so TLS was not established.", "Switch VPN node or proxy mode; if it still fails, use cpolar."],
|
|
1384
1401
|
"remote-unreachable": ["{provider} reports ready, but endpoint {endpointSuffix} is not reachable.", "Reconnect, then check the provider status if it still fails."],
|
|
1385
1402
|
"remote-needs-login": ["Tailscale is waiting for login to finish.", "Return to Remote access and continue login."],
|
|
@@ -1401,7 +1418,9 @@ window.__ModuleLoader__.load({
|
|
|
1401
1418
|
"remote-off": ["L’accesso remoto tramite {provider} è disattivato.", ""],
|
|
1402
1419
|
"remote-origin-ready": ["Il backend HTTP privato è in ascolto; HTTPS pubblico non è verificato.", "Verifica dal telefono reverse proxy, certificato pubblico, Host esterno inclusa la porta e accesso WebSocket. Mantieni privato il backend HTTP."],
|
|
1403
1420
|
"remote-ready": ["L’endpoint {provider} {endpointSuffix} è raggiungibile in circa {latencyMs} ms.", ""],
|
|
1421
|
+
"remote-ready-via-proxy": ["Il computer ha raggiunto l’endpoint {provider} {endpointSuffix} tramite il proxy configurato in circa {latencyMs} ms; la raggiungibilità dal telefono non è verificata.", "Prova la connessione dalla rete del telefono."],
|
|
1404
1422
|
"remote-rate-limited": ["L’endpoint {provider} {endpointSuffix} è raggiungibile, ma il controllo ha rilevato una limitazione temporanea.", "Riprova più tardi; le sessioni precedenti vengono caricate su richiesta per ridurre il traffico."],
|
|
1423
|
+
"remote-rate-limited-via-proxy": ["Il computer ha raggiunto l’endpoint {provider} {endpointSuffix} tramite proxy, ma il servizio ha limitato questo controllo; la raggiungibilità dal telefono non è verificata.", "Riprova più tardi e verifica dalla rete del telefono."],
|
|
1405
1424
|
"remote-fake-ip": ["L’indirizzo Tailscale è intercettato dalla VPN o dal proxy DNS corrente e TLS non è stato stabilito.", "Cambia nodo VPN o modalità proxy; se il problema continua, usa cpolar."],
|
|
1406
1425
|
"remote-unreachable": ["{provider} risulta pronto, ma l’endpoint {endpointSuffix} non è raggiungibile.", "Riconnettiti; se il problema continua, controlla lo stato del provider."],
|
|
1407
1426
|
"remote-needs-login": ["Tailscale attende il completamento dell’accesso.", "Torna ad Accesso remoto e continua l’accesso."],
|
|
@@ -1423,7 +1442,9 @@ window.__ModuleLoader__.load({
|
|
|
1423
1442
|
"remote-off": ["{provider} 远程访问当前未启用。", ""],
|
|
1424
1443
|
"remote-origin-ready": ["私有 HTTP 后端已监听;尚未验证公网 HTTPS。", "请用手机核对反向代理、公网证书、含端口的外部 Host 及 WebSocket 访问。HTTP 后端必须留在私网。"],
|
|
1425
1444
|
"remote-ready": ["{provider} 端点 {endpointSuffix} 可达,往返约 {latencyMs} ms。", ""],
|
|
1445
|
+
"remote-ready-via-proxy": ["电脑经已配置代理访问 {provider} 端点 {endpointSuffix},约 {latencyMs} ms;尚未验证手机网络是否可达。", "请用手机当前网络实际连接测试。"],
|
|
1426
1446
|
"remote-rate-limited": ["{provider} 端点 {endpointSuffix} 可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。"],
|
|
1447
|
+
"remote-rate-limited-via-proxy": ["电脑经代理访问 {provider} 端点 {endpointSuffix} 时遇到限流;尚未验证手机网络是否可达。", "稍后重试,并用手机当前网络实际连接测试。"],
|
|
1427
1448
|
"remote-fake-ip": ["Tailscale 地址被当前 VPN 或 DNS 代理接管,TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。"],
|
|
1428
1449
|
"remote-unreachable": ["{provider} 显示已就绪,但端点 {endpointSuffix} 暂不可达。", "点击重新连接;仍失败时检查提供方状态。"],
|
|
1429
1450
|
"remote-needs-login": ["Tailscale 正在等待完成登录。", "返回远程访问并继续登录。"],
|
|
@@ -1432,18 +1453,171 @@ window.__ModuleLoader__.load({
|
|
|
1432
1453
|
"phone-network-unknown": ["电脑无法判断路由器是否隔离了手机。", "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。"]
|
|
1433
1454
|
}
|
|
1434
1455
|
};
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1456
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
1457
|
+
frpMode: "Provisioning mode",
|
|
1458
|
+
frpModeDeploy: "Deploy frps here (managed)",
|
|
1459
|
+
frpModeAttach: "Use my existing frps (no SSH, no changes)",
|
|
1460
|
+
frpModeHint: "Attach manages frpc and, if selected, local ingress certificates. It never installs, edits, or restarts your frps.",
|
|
1461
|
+
frpEntryTls: "Entry certificate",
|
|
1462
|
+
frpEntryTlsPublic: "Public-CA certificate (Caddy, recommended)",
|
|
1463
|
+
frpEntryTlsSelfSigned: "Self-signed passthrough (no public certificate at all)",
|
|
1464
|
+
frpVhostHttpPort: "frps vhostHTTPPort",
|
|
1465
|
+
frpVhostHttpPortHint: "The loopback plaintext vhost port of your own frps (upstream default 7080).",
|
|
1466
|
+
frpPublicPort: "Public entry port",
|
|
1467
|
+
frpPublicPortHint: "Raw TCP proxy port used as the public HTTPS entry (default 33080, never 3080/3443/3444).",
|
|
1468
|
+
frpAttachPlan: "Copy attach runbook",
|
|
1469
|
+
frpAttachPlanCopied: "Attach runbook copied: local frpc.toml plus the VPS checklist.",
|
|
1470
|
+
frpAttachPlanFailed: "Could not build the attach runbook: {error}",
|
|
1471
|
+
frpAttachCertOk: "Ingress certificate valid for {days} more days; the plugin checks whether its leaf needs renewal.",
|
|
1472
|
+
frpAttachCertExpiring: "Ingress certificate expires in {days} days; the plugin will try to renew the leaf under the existing CA.",
|
|
1473
|
+
frpAttachCertExpired: "Ingress certificate has expired. Reconnect and check the certificate error.",
|
|
1474
|
+
frpAttachCertUnknown: "Ingress certificate is missing or unreadable. Check the certificate error before reconnecting.",
|
|
1475
|
+
frpAttachCaOk: "Ingress CA valid for {days} more days.",
|
|
1476
|
+
frpAttachCaExpiring: "Ingress CA expires in {days} days; replacing it will require pairing every phone again.",
|
|
1477
|
+
frpAttachSelfCheck: "Run self-check",
|
|
1478
|
+
frpAttachFrpsReachable: "frps control TCP port reachable",
|
|
1479
|
+
frpAttachFrpsUnreachable: "frps control TCP port unreachable",
|
|
1480
|
+
frpAttachEntryReachable: "public entry TCP port reachable (identity not checked)",
|
|
1481
|
+
frpAttachEntryUnreachable: "public entry TCP port unreachable",
|
|
1482
|
+
frpAttachModeRequiresVhostPort: "Attach mode needs the real vhostHTTPPort of your frps; the plugin will not guess 7080.",
|
|
1483
|
+
frpAttachCertUnknownError: "The self-signed ingress certificate is missing or unreadable; inspect the local certificate files before reconnecting.",
|
|
1484
|
+
frpEntryTlsInvalid: "This entry certificate mode cannot be provisioned with the selected mode.",
|
|
1485
|
+
frpSelfSignedRequiresPublicIpv4: "Self-signed TCP ingress requires a public IPv4 address. Use a public-CA HTTPS entry for a domain.",
|
|
1486
|
+
frpIngressCaExpired: "The local ingress CA has expired. Remove the FRP component from this panel, configure it again, and re-pair every phone.",
|
|
1487
|
+
frpIngressCaInvalid: "The local ingress CA is missing or damaged. Inspect the files; if rebuilding it, use this panel’s FRP cleanup and re-pair every phone.",
|
|
1488
|
+
frpIngressCaChanged: "The ingress CA differs from the saved fingerprint, so the channel stopped. Inspect the files; if rebuilding it, clean up FRP and re-pair every phone.",
|
|
1489
|
+
frpIngressRenewalFailed: "The ingress certificate could not be renewed. Check the FRP certificate files and reconnect; do not replace the CA silently.",
|
|
1490
|
+
frpAttachStepWriteSnippet: "Create the Caddy snippet",
|
|
1491
|
+
frpAttachStepAddImport: "Add the import line and reload Caddy",
|
|
1492
|
+
frpAttachStepIssueCert: "Issue the public-IP certificate",
|
|
1493
|
+
frpAttachStepCertTimer: "Confirm the renewal timer",
|
|
1494
|
+
frpAttachStepVerifyHttps: "Verify the public HTTPS entry",
|
|
1495
|
+
frpAttachStepOpenPort: "Open the public entry port",
|
|
1496
|
+
frpAttachStepVerifyFrps: "Confirm your frps is ready",
|
|
1497
|
+
frpAttachStepVerifyEntry: "Verify the tunnel end to end",
|
|
1498
|
+
frpAttachToken: "Copy frpc.toml with token",
|
|
1499
|
+
frpAttachTokenCopied: "The plaintext frpc.toml is copied. It contains the shared token and stays in the clipboard until you clear it — do not paste it anywhere public.",
|
|
1500
|
+
frpAttachTokenFailed: "Could not copy the frpc.toml with its token: {error}",
|
|
1501
|
+
frpAttachTokenReenter: "Re-enter the shared token first. The plugin never returns a saved token to this page.",
|
|
1502
|
+
frpStep2TitleAttach: "2 · Check your VPS",
|
|
1503
|
+
frpStep2TextAttach: "Check your existing frps and copy a VPS checklist; the plugin does not change the server.",
|
|
1504
|
+
frpStep4TextAttach: "Save the local client configuration and verify that the public endpoint reaches this computer.",
|
|
1505
|
+
frpAppRequirementSelfSigned: "Self-signed ingress requires an Android app that can pin a remote CA. Older apps without this feature cannot use this entry.",
|
|
1506
|
+
frpConfigurationMissingAttach: "Enter your existing frps details, install frpc locally, then save and verify the connection.",
|
|
1507
|
+
purgeFrpConfirmSelfSigned: "Stop FRP and delete its local frpc, token, configuration, certificates, private keys, identity record, staging files, and logs? Every phone paired through this self-signed entry must be paired again. Your VPS is unchanged."
|
|
1508
|
+
});
|
|
1509
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
1510
|
+
frpMode: "Modalità di provisioning",
|
|
1511
|
+
frpModeDeploy: "Installa frps qui (gestito)",
|
|
1512
|
+
frpModeAttach: "Usa il mio frps esistente (niente SSH, nessuna modifica)",
|
|
1513
|
+
frpModeHint: "La modalità attach gestisce frpc e, se scelti, i certificati locali. Non installa, modifica o riavvia il tuo frps.",
|
|
1514
|
+
frpEntryTls: "Certificato di ingresso",
|
|
1515
|
+
frpEntryTlsPublic: "Certificato da CA pubblica (Caddy, consigliato)",
|
|
1516
|
+
frpEntryTlsSelfSigned: "Passthrough autofirmato (nessun certificato pubblico)",
|
|
1517
|
+
frpVhostHttpPort: "vhostHTTPPort di frps",
|
|
1518
|
+
frpVhostHttpPortHint: "Porta vhost in chiaro su loopback del tuo frps (predefinita 7080).",
|
|
1519
|
+
frpPublicPort: "Porta pubblica di ingresso",
|
|
1520
|
+
frpPublicPortHint: "Porta del proxy TCP grezzo usata come ingresso HTTPS pubblico (predefinita 33080, mai 3080/3443/3444).",
|
|
1521
|
+
frpAttachPlan: "Copia il runbook attach",
|
|
1522
|
+
frpAttachPlanCopied: "Runbook attach copiato: frpc.toml locale più la checklist VPS.",
|
|
1523
|
+
frpAttachPlanFailed: "Impossibile creare il runbook attach: {error}",
|
|
1524
|
+
frpAttachCertOk: "Certificato di ingresso valido per altri {days} giorni; il plugin verifica se la foglia va rinnovata.",
|
|
1525
|
+
frpAttachCertExpiring: "Il certificato di ingresso scade tra {days} giorni; il plugin proverà a rinnovare la foglia con la CA esistente.",
|
|
1526
|
+
frpAttachCertExpired: "Il certificato di ingresso è scaduto. Riconnettiti e controlla l’errore del certificato.",
|
|
1527
|
+
frpAttachCertUnknown: "Certificato di ingresso mancante o illeggibile. Controlla l’errore prima di riconnetterti.",
|
|
1528
|
+
frpAttachCaOk: "CA di ingresso valida per altri {days} giorni.",
|
|
1529
|
+
frpAttachCaExpiring: "La CA di ingresso scade tra {days} giorni; sostituirla richiederà una nuova associazione di ogni telefono.",
|
|
1530
|
+
frpAttachSelfCheck: "Esegui autoverifica",
|
|
1531
|
+
frpAttachFrpsReachable: "porta TCP di controllo frps raggiungibile",
|
|
1532
|
+
frpAttachFrpsUnreachable: "porta TCP di controllo frps non raggiungibile",
|
|
1533
|
+
frpAttachEntryReachable: "porta TCP pubblica raggiungibile (identità non verificata)",
|
|
1534
|
+
frpAttachEntryUnreachable: "porta TCP pubblica non raggiungibile",
|
|
1535
|
+
frpAttachModeRequiresVhostPort: "La modalità attach richiede il vhostHTTPPort reale del tuo frps; il plugin non presume 7080.",
|
|
1536
|
+
frpAttachCertUnknownError: "Il certificato di ingresso autofirmato manca o è illeggibile; controlla i file locali prima di riconnetterti.",
|
|
1537
|
+
frpEntryTlsInvalid: "Questa modalità di certificato non è compatibile con la modalità selezionata.",
|
|
1538
|
+
frpSelfSignedRequiresPublicIpv4: "L’ingresso TCP autofirmato richiede un IPv4 pubblico. Per un dominio usa un ingresso HTTPS con CA pubblica.",
|
|
1539
|
+
frpIngressCaExpired: "La CA locale è scaduta. Rimuovi FRP da questo pannello, riconfiguralo e associa di nuovo ogni telefono.",
|
|
1540
|
+
frpIngressCaInvalid: "La CA locale manca o è danneggiata. Controlla i file; se la ricrei, usa la pulizia FRP del pannello e associa di nuovo ogni telefono.",
|
|
1541
|
+
frpIngressCaChanged: "La CA non corrisponde all’impronta salvata, quindi il canale è stato fermato. Controlla i file; se la ricrei, pulisci FRP e associa di nuovo i telefoni.",
|
|
1542
|
+
frpIngressRenewalFailed: "Impossibile rinnovare il certificato di ingresso. Controlla i file e riconnettiti senza sostituire la CA automaticamente.",
|
|
1543
|
+
frpAttachStepWriteSnippet: "Crea lo snippet Caddy",
|
|
1544
|
+
frpAttachStepAddImport: "Aggiungi la riga import e ricarica Caddy",
|
|
1545
|
+
frpAttachStepIssueCert: "Emetti il certificato per l’IP pubblico",
|
|
1546
|
+
frpAttachStepCertTimer: "Verifica il timer di rinnovo",
|
|
1547
|
+
frpAttachStepVerifyHttps: "Verifica l’ingresso HTTPS pubblico",
|
|
1548
|
+
frpAttachStepOpenPort: "Apri la porta pubblica di ingresso",
|
|
1549
|
+
frpAttachStepVerifyFrps: "Verifica che il tuo frps sia pronto",
|
|
1550
|
+
frpAttachStepVerifyEntry: "Verifica il tunnel end-to-end",
|
|
1551
|
+
frpAttachToken: "Copia frpc.toml con token",
|
|
1552
|
+
frpAttachTokenCopied: "frpc.toml in chiaro copiato. Contiene il token condiviso e resta negli appunti finché non li svuoti: non incollarlo in luoghi pubblici.",
|
|
1553
|
+
frpAttachTokenFailed: "Impossibile copiare frpc.toml con il token: {error}",
|
|
1554
|
+
frpAttachTokenReenter: "Inserisci di nuovo il token condiviso. Il plugin non restituisce mai un token salvato a questa pagina.",
|
|
1555
|
+
frpStep2TitleAttach: "2 · Controlla il VPS",
|
|
1556
|
+
frpStep2TextAttach: "Controlla il tuo frps e copia la checklist VPS; il plugin non modifica il server.",
|
|
1557
|
+
frpStep4TextAttach: "Salva la configurazione locale e verifica che l’ingresso pubblico raggiunga questo computer.",
|
|
1558
|
+
frpAppRequirementSelfSigned: "L’ingresso autofirmato richiede un’app Android capace di fissare una CA remota. Le vecchie versioni senza questa funzione non possono usarlo.",
|
|
1559
|
+
frpConfigurationMissingAttach: "Inserisci i dati del tuo frps, installa frpc localmente, poi salva e verifica la connessione.",
|
|
1560
|
+
purgeFrpConfirmSelfSigned: "Arrestare FRP e cancellare frpc, token, configurazione, certificati, chiavi private, identità, file temporanei e log locali? Ogni telefono associato a questo ingresso autofirmato dovrà essere associato di nuovo. Il VPS non cambia."
|
|
1561
|
+
});
|
|
1562
|
+
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
1563
|
+
frpMode: "置备方式",
|
|
1564
|
+
frpModeDeploy: "由插件部署 frps(托管)",
|
|
1565
|
+
frpModeAttach: "接入我已有的 frps(零 SSH,不改动)",
|
|
1566
|
+
frpModeHint: "接入模式只管理本机 frpc 与按需生成的入口证书,不会安装、修改或重启你已有的 frps。",
|
|
1567
|
+
frpEntryTls: "入口证书档",
|
|
1568
|
+
frpEntryTlsPublic: "公开 CA 证书(Caddy,推荐)",
|
|
1569
|
+
frpEntryTlsSelfSigned: "自签穿透(完全不用公开证书)",
|
|
1570
|
+
frpVhostHttpPort: "frps 的 vhostHTTPPort",
|
|
1571
|
+
frpVhostHttpPortHint: "你自己 frps 的回环明文 vhost 端口(上游默认 7080)。",
|
|
1572
|
+
frpPublicPort: "公网入口端口",
|
|
1573
|
+
frpPublicPortHint: "作为公网 HTTPS 入口的 TCP 透传端口(默认 33080,不得为 3080/3443/3444)。",
|
|
1574
|
+
frpAttachPlan: "复制接入清单",
|
|
1575
|
+
frpAttachPlanCopied: "接入清单已复制:本机 frpc.toml 与 VPS 侧待办。",
|
|
1576
|
+
frpAttachPlanFailed: "生成接入清单失败:{error}",
|
|
1577
|
+
frpAttachCertOk: "入口证书还有 {days} 天有效期;插件会检查是否需要续签叶子证书。",
|
|
1578
|
+
frpAttachCertExpiring: "入口证书 {days} 天后到期;插件会尝试沿用现有 CA 续签叶子证书。",
|
|
1579
|
+
frpAttachCertExpired: "入口证书已过期,请重新连接并查看具体证书错误。",
|
|
1580
|
+
frpAttachCertUnknown: "入口证书缺失或不可读,请先查看具体证书错误。",
|
|
1581
|
+
frpAttachCaOk: "入口 CA 还有 {days} 天有效期。",
|
|
1582
|
+
frpAttachCaExpiring: "入口 CA {days} 天后到期;更换 CA 后所有手机需重新配对。",
|
|
1583
|
+
frpAttachSelfCheck: "运行自检",
|
|
1584
|
+
frpAttachFrpsReachable: "frps 控制 TCP 端口可达",
|
|
1585
|
+
frpAttachFrpsUnreachable: "frps 控制 TCP 端口不可达",
|
|
1586
|
+
frpAttachEntryReachable: "公网入口 TCP 端口可达(未验证身份)",
|
|
1587
|
+
frpAttachEntryUnreachable: "公网入口 TCP 端口不可达",
|
|
1588
|
+
frpAttachModeRequiresVhostPort: "接入模式必须填写你 frps 真实的 vhostHTTPPort,插件不会替你假定 7080。",
|
|
1589
|
+
frpAttachCertUnknownError: "自签入口证书缺失或不可读,请检查本机证书文件后再连接。",
|
|
1590
|
+
frpEntryTlsInvalid: "当前置备方式无法使用该入口证书档。",
|
|
1591
|
+
frpSelfSignedRequiresPublicIpv4: "自签 TCP 入口需要公网 IPv4 地址;域名请使用受公共 CA 信任的 HTTPS 入口。",
|
|
1592
|
+
frpIngressCaExpired: "本机入口 CA 已过期。请在此面板彻底清理 FRP 并重新配置,所有手机需重新配对。",
|
|
1593
|
+
frpIngressCaInvalid: "本机入口 CA 缺失或损坏。请检查证书文件;如需重建,在此面板清理 FRP 后重新配对。",
|
|
1594
|
+
frpIngressCaChanged: "入口 CA 与保存的指纹不符,通道已停止。请核查证书文件;如需重建,清理 FRP 后重新配对。",
|
|
1595
|
+
frpIngressRenewalFailed: "入口证书续签失败。请检查 FRP 证书文件并重新连接,不要静默更换 CA。",
|
|
1596
|
+
frpAttachStepWriteSnippet: "新增 Caddy 片段",
|
|
1597
|
+
frpAttachStepAddImport: "加入 import 行并重载 Caddy",
|
|
1598
|
+
frpAttachStepIssueCert: "为公网 IP 签发证书",
|
|
1599
|
+
frpAttachStepCertTimer: "确认续期定时器",
|
|
1600
|
+
frpAttachStepVerifyHttps: "验证公网 HTTPS 入口",
|
|
1601
|
+
frpAttachStepOpenPort: "放行公网入口端口",
|
|
1602
|
+
frpAttachStepVerifyFrps: "确认既有 frps 已就绪",
|
|
1603
|
+
frpAttachStepVerifyEntry: "端到端验证隧道",
|
|
1604
|
+
frpAttachToken: "复制含 Token 的 frpc.toml",
|
|
1605
|
+
frpAttachTokenCopied: "含 Token 的 frpc.toml(明文)已复制:它会留在系统剪贴板中,请勿粘贴到公开位置。",
|
|
1606
|
+
frpAttachTokenFailed: "复制含 Token 的 frpc.toml 失败:{error}",
|
|
1607
|
+
frpAttachTokenReenter: "请先重新输入共享 Token。插件不会把已保存的 Token 返回页面。",
|
|
1608
|
+
frpStep2TitleAttach: "2 · 核对既有服务器",
|
|
1609
|
+
frpStep2TextAttach: "核对既有 frps 配置并复制 VPS 操作清单;插件不会改动服务器。",
|
|
1610
|
+
frpStep4TextAttach: "保存本机 frpc 配置,并验证公开入口确实连接到这台电脑。",
|
|
1611
|
+
frpAppRequirementSelfSigned: "自签入口需要支持远程 CA 固定的 Android App;不具备此功能的旧版 App 无法使用。",
|
|
1612
|
+
frpConfigurationMissingAttach: "填写既有 frps 信息,在本机安装 frpc,然后保存并验证连接。",
|
|
1613
|
+
purgeFrpConfirmSelfSigned: "要停止 FRP 并删除本机 frpc、Token、配置、证书、私钥、身份记录、临时文件和日志吗?通过此自签入口配对的手机都需重新配对;VPS 不会改变。"
|
|
1614
|
+
});
|
|
1439
1615
|
/** Caddy snippet owned entirely by DSH Mobile; the main Caddyfile only imports it. */
|
|
1440
1616
|
const FRP_CADDY_SNIPPET_PATH = "/etc/caddy/dsh-mobile-dsh.caddy";
|
|
1441
1617
|
/** First line of the owned snippet; also the legacy whole-file marker. */
|
|
1442
1618
|
const FRP_CADDY_SNIPPET_MARKER = "# Managed by DSH Mobile - snippet, safe to delete";
|
|
1443
1619
|
/** Exact line the main Caddyfile must contain (uncommented) for the site to load. */
|
|
1444
1620
|
const FRP_CADDY_IMPORT_LINE = `import ${FRP_CADDY_SNIPPET_PATH}`;
|
|
1445
|
-
/** Directory holding the public-IPv4 certificates installed by certbot. */
|
|
1446
|
-
const FRP_CADDY_IP_CERT_DIR = "/var/lib/caddy/dsh-mobile-certs";
|
|
1447
1621
|
function publicIpv4Address(value) {
|
|
1448
1622
|
const parts = value.split(".");
|
|
1449
1623
|
return parts.length === 4 && parts.every((part) => /^(?:0|[1-9][0-9]{0,2})$/u.test(part) && Number(part) <= 255);
|
|
@@ -1461,8 +1635,31 @@ window.__ModuleLoader__.load({
|
|
|
1461
1635
|
if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || !publicIpv4Address(url.hostname) && !publicDnsHostname(url.hostname)) throw new Error("frp_template_input_invalid");
|
|
1462
1636
|
return url.hostname;
|
|
1463
1637
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1638
|
+
function resolveVhostHttpPort(options) {
|
|
1639
|
+
const port = options.vhostHttpPort ?? 7080;
|
|
1640
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error("frp_template_input_invalid");
|
|
1641
|
+
return port;
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* A self-signed entry deliberately never reaches Caddy: the tunnel is a raw TCP
|
|
1645
|
+
* passthrough and the DSH gateway is the TLS endpoint. Refuse loudly instead of
|
|
1646
|
+
* emitting a Caddy site that would need the gateway's private CA on the VPS.
|
|
1647
|
+
*/
|
|
1648
|
+
function rejectSelfSignedCaddySite(options) {
|
|
1649
|
+
if (options.entryTls === "self-signed") throw new Error("frp_entry_tls_invalid");
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Build the Caddy site for one public host (without markers or import wiring).
|
|
1653
|
+
*
|
|
1654
|
+
* The second parameter accepts either a bare certificate directory (the legacy
|
|
1655
|
+
* signature) or a full {@link CaddySiteOptions} object, so existing callers keep
|
|
1656
|
+
* working unchanged.
|
|
1657
|
+
*/
|
|
1658
|
+
function createCaddySite(publicHost, options = {}) {
|
|
1659
|
+
const resolved = typeof options === "string" ? { certDir: options } : options;
|
|
1660
|
+
rejectSelfSignedCaddySite(resolved);
|
|
1661
|
+
const certDir = resolved.certDir ?? "/var/lib/caddy/dsh-mobile-certs";
|
|
1662
|
+
const vhostHttpPort = resolveVhostHttpPort(resolved);
|
|
1466
1663
|
if (publicIpv4Address(publicHost)) return [
|
|
1467
1664
|
"{",
|
|
1468
1665
|
` default_sni ${publicHost}`,
|
|
@@ -1474,14 +1671,14 @@ window.__ModuleLoader__.load({
|
|
|
1474
1671
|
"",
|
|
1475
1672
|
`https://${publicHost} {`,
|
|
1476
1673
|
` tls ${certDir}/fullchain.pem ${certDir}/privkey.pem`,
|
|
1477
|
-
` reverse_proxy 127.0.0.1:${String(
|
|
1674
|
+
` reverse_proxy 127.0.0.1:${String(vhostHttpPort)}`,
|
|
1478
1675
|
"}",
|
|
1479
1676
|
""
|
|
1480
1677
|
].join("\n");
|
|
1481
1678
|
if (!publicDnsHostname(publicHost)) throw new Error("frp_template_input_invalid");
|
|
1482
1679
|
return [
|
|
1483
1680
|
`${publicHost} {`,
|
|
1484
|
-
` reverse_proxy 127.0.0.1:${String(
|
|
1681
|
+
` reverse_proxy 127.0.0.1:${String(vhostHttpPort)}`,
|
|
1485
1682
|
"}",
|
|
1486
1683
|
""
|
|
1487
1684
|
].join("\n");
|
|
@@ -1504,15 +1701,24 @@ window.__ModuleLoader__.load({
|
|
|
1504
1701
|
"#"
|
|
1505
1702
|
].join("\n");
|
|
1506
1703
|
}
|
|
1507
|
-
/**
|
|
1508
|
-
|
|
1704
|
+
/**
|
|
1705
|
+
* Build the only supported frps config and Caddy snippet from validated user inputs.
|
|
1706
|
+
*
|
|
1707
|
+
* Default `options` reproduce the upstream artefact byte for byte; `attach` mode
|
|
1708
|
+
* passes the user's real vhost port, and `self-signed` is rejected because that
|
|
1709
|
+
* mode publishes a raw TCP proxy instead of an HTTP vhost.
|
|
1710
|
+
*/
|
|
1711
|
+
function createRestrictedFrpServerTemplate(serverPort, token, publicOrigin, options = {}) {
|
|
1509
1712
|
if (!Number.isSafeInteger(serverPort) || serverPort < 1 || serverPort > 65535 || token.length < 16 || token.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(token)) throw new Error("frp_template_input_invalid");
|
|
1713
|
+
rejectSelfSignedCaddySite(options);
|
|
1714
|
+
const vhostHttpPort = resolveVhostHttpPort(options);
|
|
1715
|
+
const certDir = options.certDir ?? "/var/lib/caddy/dsh-mobile-certs";
|
|
1510
1716
|
const publicHost = parsePublicOrigin(publicOrigin);
|
|
1511
1717
|
const lines = [
|
|
1512
1718
|
"# frps.toml — save as /etc/dsh-mobile/frps.toml, then start the frps service.",
|
|
1513
1719
|
`bindPort = ${String(serverPort)}`,
|
|
1514
1720
|
"proxyBindAddr = \"127.0.0.1\"",
|
|
1515
|
-
`vhostHTTPPort = ${String(
|
|
1721
|
+
`vhostHTTPPort = ${String(vhostHttpPort)}`,
|
|
1516
1722
|
"auth.method = \"token\"",
|
|
1517
1723
|
`auth.token = ${JSON.stringify(token)}`,
|
|
1518
1724
|
"",
|
|
@@ -1523,12 +1729,528 @@ window.__ModuleLoader__.load({
|
|
|
1523
1729
|
"# finally run: caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy",
|
|
1524
1730
|
"# Uninstall later removes only this snippet file and the import line; your own Caddy content is kept.",
|
|
1525
1731
|
`${FRP_CADDY_SNIPPET_MARKER}`,
|
|
1526
|
-
createCaddySite(publicHost
|
|
1732
|
+
createCaddySite(publicHost, {
|
|
1733
|
+
certDir,
|
|
1734
|
+
vhostHttpPort
|
|
1735
|
+
}).trimEnd(),
|
|
1527
1736
|
""
|
|
1528
1737
|
];
|
|
1529
1738
|
if (publicIpv4Address(publicHost)) lines.push(manualIpCertificateGuide(publicHost), "");
|
|
1530
1739
|
return lines.join("\n");
|
|
1531
1740
|
}
|
|
1741
|
+
//#endregion
|
|
1742
|
+
//#region src/ip.ts
|
|
1743
|
+
/**
|
|
1744
|
+
* Browser-safe IP literal classification.
|
|
1745
|
+
*
|
|
1746
|
+
* `node:net` cannot be imported from the browser Client face, so the pure
|
|
1747
|
+
* predicate lives here instead. Semantics are deliberately identical to
|
|
1748
|
+
* `net.isIP`: strict dotted-quad IPv4 (no leading zeros, no signs, no
|
|
1749
|
+
* whitespace), IPv6 with at most one `::`, at most four hex digits per group,
|
|
1750
|
+
* an optional dotted-quad tail in the final position, and an optional
|
|
1751
|
+
* non-empty zone id after a single `%`.
|
|
1752
|
+
*
|
|
1753
|
+
* This module is dependency-free and side-effect-free on purpose: it is the
|
|
1754
|
+
* single classification source shared by every face of the plugin — the Node
|
|
1755
|
+
* side (`config.ts`, `cloudflared-tunnel.ts`, `origin-proxy-config.ts`,
|
|
1756
|
+
* `vps-deploy.ts`, `gateway.ts`, `network.ts`, `frp-config.ts`) and the browser
|
|
1757
|
+
* Client face that cannot reach `node:net` — so both classify a given literal
|
|
1758
|
+
* identically. Callers rely on the strict reading above and never post-process
|
|
1759
|
+
* the literal afterwards, so changing the accept/reject boundary here changes
|
|
1760
|
+
* it everywhere, which is the intended contract.
|
|
1761
|
+
*/
|
|
1762
|
+
/** Strict dotted-quad IPv4: POSIX `inet_pton` rules, so `01.1.1.1` is rejected. */
|
|
1763
|
+
function isStrictIpv4(value) {
|
|
1764
|
+
const parts = value.split(".");
|
|
1765
|
+
if (parts.length !== 4) return false;
|
|
1766
|
+
for (const part of parts) {
|
|
1767
|
+
if (!/^(?:0|[1-9][0-9]{0,2})$/u.test(part)) return false;
|
|
1768
|
+
if (Number(part) > 255) return false;
|
|
1769
|
+
}
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Count the 16-bit groups one side of an IPv6 address occupies.
|
|
1774
|
+
*
|
|
1775
|
+
* A dotted-quad tail counts as two groups and is accepted only where the caller
|
|
1776
|
+
* allows it — that is, only in the final label of the whole address.
|
|
1777
|
+
*
|
|
1778
|
+
* @returns the group count, or `-1` when any label is malformed.
|
|
1779
|
+
*/
|
|
1780
|
+
function countIpv6Groups(labels, allowDottedQuad) {
|
|
1781
|
+
let groups = 0;
|
|
1782
|
+
for (let index = 0; index < labels.length; index += 1) {
|
|
1783
|
+
const label = labels[index];
|
|
1784
|
+
if (label.includes(".")) {
|
|
1785
|
+
if (index !== labels.length - 1 || !allowDottedQuad || !isStrictIpv4(label)) return -1;
|
|
1786
|
+
groups += 2;
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (!/^[\da-f]{1,4}$/iu.test(label)) return -1;
|
|
1790
|
+
groups += 1;
|
|
1791
|
+
}
|
|
1792
|
+
return groups;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Classify an IP literal.
|
|
1796
|
+
*
|
|
1797
|
+
* @param value - candidate address; non-strings are never an IP.
|
|
1798
|
+
* @returns `4` for IPv4, `6` for IPv6, `0` when the value is neither.
|
|
1799
|
+
*/
|
|
1800
|
+
function isIP(value) {
|
|
1801
|
+
if (typeof value !== "string") return 0;
|
|
1802
|
+
if (isStrictIpv4(value)) return 4;
|
|
1803
|
+
let text = value;
|
|
1804
|
+
const zone = text.indexOf("%");
|
|
1805
|
+
if (zone !== -1) {
|
|
1806
|
+
const suffix = text.slice(zone + 1);
|
|
1807
|
+
if (suffix.length === 0 || suffix.includes("%")) return 0;
|
|
1808
|
+
text = text.slice(0, zone);
|
|
1809
|
+
}
|
|
1810
|
+
if (text.length === 0) return 0;
|
|
1811
|
+
const halves = text.split("::");
|
|
1812
|
+
if (halves.length > 2) return 0;
|
|
1813
|
+
if (halves.length === 1) return countIpv6Groups(text.split(":"), true) === 8 ? 6 : 0;
|
|
1814
|
+
const leftText = halves[0];
|
|
1815
|
+
const rightText = halves[1];
|
|
1816
|
+
const left = leftText === "" ? [] : leftText.split(":");
|
|
1817
|
+
const right = rightText === "" ? [] : rightText.split(":");
|
|
1818
|
+
const leftGroups = countIpv6Groups(left, false);
|
|
1819
|
+
const rightGroups = countIpv6Groups(right, right.length > 0);
|
|
1820
|
+
if (leftGroups < 0 || rightGroups < 0) return 0;
|
|
1821
|
+
return leftGroups + rightGroups >= 8 ? 0 : 6;
|
|
1822
|
+
}
|
|
1823
|
+
//#endregion
|
|
1824
|
+
//#region src/network.ts
|
|
1825
|
+
/**
|
|
1826
|
+
* IPv4 ranges that are never a public VPS endpoint (IANA special-purpose,
|
|
1827
|
+
* private, shared, loopback, link-local, documentation, benchmark, multicast,
|
|
1828
|
+
* and reserved space). Kept in sync with Android `RemoteHostPolicy`.
|
|
1829
|
+
*/
|
|
1830
|
+
const NON_ROUTABLE_IPV4_RANGES = Object.freeze([
|
|
1831
|
+
[0n, 8],
|
|
1832
|
+
[167772160n, 8],
|
|
1833
|
+
[1681915904n, 10],
|
|
1834
|
+
[2130706432n, 8],
|
|
1835
|
+
[2851995648n, 16],
|
|
1836
|
+
[2886729728n, 12],
|
|
1837
|
+
[3221225472n, 24],
|
|
1838
|
+
[3221225984n, 24],
|
|
1839
|
+
[3223307264n, 24],
|
|
1840
|
+
[3224682752n, 24],
|
|
1841
|
+
[3227017984n, 24],
|
|
1842
|
+
[3232235520n, 16],
|
|
1843
|
+
[3232706560n, 24],
|
|
1844
|
+
[3323068416n, 15],
|
|
1845
|
+
[3325256704n, 24],
|
|
1846
|
+
[3405803776n, 24],
|
|
1847
|
+
[3758096384n, 4],
|
|
1848
|
+
[4026531840n, 4]
|
|
1849
|
+
]);
|
|
1850
|
+
function parseStrictIpv4Octets(address) {
|
|
1851
|
+
const parts = address.split(".");
|
|
1852
|
+
if (parts.length !== 4) return void 0;
|
|
1853
|
+
const octets = [];
|
|
1854
|
+
for (const part of parts) {
|
|
1855
|
+
if (!/^(?:0|[1-9][0-9]{0,2})$/u.test(part)) return void 0;
|
|
1856
|
+
const octet = Number(part);
|
|
1857
|
+
if (octet > 255) return void 0;
|
|
1858
|
+
octets.push(octet);
|
|
1859
|
+
}
|
|
1860
|
+
return octets;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Whether a dotted-quad IPv4 literal is globally routable and therefore usable
|
|
1864
|
+
* as a public VPS / remote HTTPS endpoint. Rejects documentation addresses such
|
|
1865
|
+
* as 203.0.113.10 alongside private, shared, and reserved space.
|
|
1866
|
+
*/
|
|
1867
|
+
function isGloballyRoutableIpv4(address) {
|
|
1868
|
+
const octets = parseStrictIpv4Octets(address);
|
|
1869
|
+
if (octets === void 0) return false;
|
|
1870
|
+
const value = BigInt(octets[0]) << 24n | BigInt(octets[1]) << 16n | BigInt(octets[2]) << 8n | BigInt(octets[3]);
|
|
1871
|
+
return !NON_ROUTABLE_IPV4_RANGES.some(([network, prefix]) => {
|
|
1872
|
+
if (prefix === 0) return true;
|
|
1873
|
+
const hostBits = BigInt(32 - prefix);
|
|
1874
|
+
const mask = (1n << 32n) - 1n ^ (1n << hostBits) - 1n;
|
|
1875
|
+
return (value & mask) === network;
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
/** Ports owned by other DSH Mobile listeners; the public entry may never reuse them. */
|
|
1879
|
+
const FRP_RESERVED_PORTS = Object.freeze([
|
|
1880
|
+
3080,
|
|
1881
|
+
3443,
|
|
1882
|
+
3444
|
|
1883
|
+
]);
|
|
1884
|
+
/** Every key `settings.json` may contain. Unknown keys stay rejected. */
|
|
1885
|
+
const FRP_SETTINGS_KEYS = /* @__PURE__ */ new Set([
|
|
1886
|
+
"version",
|
|
1887
|
+
"serverAddress",
|
|
1888
|
+
"serverPort",
|
|
1889
|
+
"token",
|
|
1890
|
+
"publicOrigin",
|
|
1891
|
+
"mode",
|
|
1892
|
+
"entryTls",
|
|
1893
|
+
"vhostHttpPort",
|
|
1894
|
+
"publicPort"
|
|
1895
|
+
]);
|
|
1896
|
+
function hostname(value) {
|
|
1897
|
+
if (value.length > 253 || !value.includes(".")) return false;
|
|
1898
|
+
return value.split(".").every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label));
|
|
1899
|
+
}
|
|
1900
|
+
/** Validate the FRP server hostname or IP address. */
|
|
1901
|
+
function validateFrpServerAddress(value) {
|
|
1902
|
+
if (typeof value !== "string" || value !== value.trim() || value.length === 0 || value.length > 253 || /[\s\u0000-\u001f\u007f/\\@?#]/u.test(value)) throw new Error("frp_server_address_invalid");
|
|
1903
|
+
const normalized = value.toLowerCase().replace(/\.$/u, "");
|
|
1904
|
+
if (isIP(normalized) === 0 && !hostname(normalized)) throw new Error("frp_server_address_invalid");
|
|
1905
|
+
return normalized;
|
|
1906
|
+
}
|
|
1907
|
+
/** Validate the FRP control port. */
|
|
1908
|
+
function validateFrpServerPort(value) {
|
|
1909
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > 65535) throw new Error("frp_server_port_invalid");
|
|
1910
|
+
return Number(value);
|
|
1911
|
+
}
|
|
1912
|
+
/** Validate a high-entropy FRP token before durable storage. */
|
|
1913
|
+
function validateFrpToken(value) {
|
|
1914
|
+
if (typeof value !== "string" || value.length < 16 || value.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(value)) throw new Error("frp_token_invalid");
|
|
1915
|
+
return value;
|
|
1916
|
+
}
|
|
1917
|
+
/** Validate the public HTTPS origin used by Caddy and Android pairing. */
|
|
1918
|
+
function validateFrpPublicOrigin(value) {
|
|
1919
|
+
if (typeof value !== "string" || value.length > 512) throw new Error("frp_public_origin_invalid");
|
|
1920
|
+
let url;
|
|
1921
|
+
try {
|
|
1922
|
+
url = new URL(value);
|
|
1923
|
+
} catch {
|
|
1924
|
+
throw new Error("frp_public_origin_invalid");
|
|
1925
|
+
}
|
|
1926
|
+
const publicHost = url.hostname;
|
|
1927
|
+
if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || isIP(publicHost) !== 4 && !hostname(publicHost)) throw new Error("frp_public_origin_invalid");
|
|
1928
|
+
if (isIP(publicHost) === 4 && !isGloballyRoutableIpv4(publicHost)) throw new Error("frp_public_origin_invalid");
|
|
1929
|
+
return url.origin;
|
|
1930
|
+
}
|
|
1931
|
+
/** Validate the provisioning mode. */
|
|
1932
|
+
function validateFrpMode(value) {
|
|
1933
|
+
if (value !== "deploy" && value !== "attach") throw new Error("frp_settings_invalid");
|
|
1934
|
+
return value;
|
|
1935
|
+
}
|
|
1936
|
+
/** Validate the entry TLS mode. */
|
|
1937
|
+
function validateFrpEntryTls(value) {
|
|
1938
|
+
if (value !== "public-ip-cert" && value !== "self-signed") throw new Error("frp_entry_tls_invalid");
|
|
1939
|
+
return value;
|
|
1940
|
+
}
|
|
1941
|
+
function validatePort(value, rejectReserved) {
|
|
1942
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > 65535) throw new Error("frp_settings_invalid");
|
|
1943
|
+
const port = Number(value);
|
|
1944
|
+
if (rejectReserved && FRP_RESERVED_PORTS.includes(port)) throw new Error("frp_settings_invalid");
|
|
1945
|
+
return port;
|
|
1946
|
+
}
|
|
1947
|
+
/** Validate the loopback vhost port of the target frps. */
|
|
1948
|
+
function validateFrpVhostHttpPort(value) {
|
|
1949
|
+
return validatePort(value, false);
|
|
1950
|
+
}
|
|
1951
|
+
/** Validate the public entry port of the self-signed TCP proxy. */
|
|
1952
|
+
function validateFrpPublicPort(value) {
|
|
1953
|
+
return validatePort(value, true);
|
|
1954
|
+
}
|
|
1955
|
+
/** Effective provisioning mode; an absent field keeps the upstream behaviour. */
|
|
1956
|
+
function resolveFrpMode(settings) {
|
|
1957
|
+
return settings.mode ?? "deploy";
|
|
1958
|
+
}
|
|
1959
|
+
/** Effective entry TLS mode; an absent field keeps the upstream behaviour. */
|
|
1960
|
+
function resolveFrpEntryTls(settings) {
|
|
1961
|
+
return settings.entryTls ?? "public-ip-cert";
|
|
1962
|
+
}
|
|
1963
|
+
/** Effective loopback vhost port; an absent field keeps the upstream behaviour. */
|
|
1964
|
+
function resolveFrpVhostHttpPort(settings) {
|
|
1965
|
+
return settings.vhostHttpPort ?? 7080;
|
|
1966
|
+
}
|
|
1967
|
+
/** Effective public entry port of the self-signed passthrough. */
|
|
1968
|
+
function resolveFrpPublicPort(settings) {
|
|
1969
|
+
return settings.publicPort ?? 33080;
|
|
1970
|
+
}
|
|
1971
|
+
/** True when the gateway terminates TLS itself behind a raw frps TCP proxy. */
|
|
1972
|
+
function isFrpSelfSignedIngress(settings) {
|
|
1973
|
+
return resolveFrpEntryTls(settings) === "self-signed";
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Parse FRP settings at the loopback request and filesystem boundaries.
|
|
1977
|
+
*
|
|
1978
|
+
* Optional keys are kept only when supplied, so a legacy `settings.json` still
|
|
1979
|
+
* round-trips byte for byte and every default stays the upstream behaviour.
|
|
1980
|
+
* Two cross-field rules are enforced here because neither can be recovered later:
|
|
1981
|
+
* the self-signed entry is a TCP passthrough and therefore attach-only, and
|
|
1982
|
+
* attach mode with a public-CA entry must state the user's real vhost port —
|
|
1983
|
+
* silently assuming 7080 would disable the plaintext-exposure gate.
|
|
1984
|
+
*/
|
|
1985
|
+
function parseFrpSettings(value) {
|
|
1986
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("frp_settings_invalid");
|
|
1987
|
+
const record = value;
|
|
1988
|
+
if (Reflect.ownKeys(record).some((key) => !FRP_SETTINGS_KEYS.has(String(key)))) throw new Error("frp_settings_invalid");
|
|
1989
|
+
if (record.version !== void 0 && record.version !== 1) throw new Error("frp_settings_invalid");
|
|
1990
|
+
const mode = record.mode === void 0 ? void 0 : validateFrpMode(record.mode);
|
|
1991
|
+
const entryTls = record.entryTls === void 0 ? void 0 : validateFrpEntryTls(record.entryTls);
|
|
1992
|
+
const vhostHttpPort = record.vhostHttpPort === void 0 ? void 0 : validateFrpVhostHttpPort(record.vhostHttpPort);
|
|
1993
|
+
const publicPort = record.publicPort === void 0 ? void 0 : validateFrpPublicPort(record.publicPort);
|
|
1994
|
+
const resolvedMode = mode ?? "deploy";
|
|
1995
|
+
const resolvedEntryTls = entryTls ?? "public-ip-cert";
|
|
1996
|
+
if (resolvedEntryTls === "self-signed" && resolvedMode !== "attach") throw new Error("frp_entry_tls_invalid");
|
|
1997
|
+
if (resolvedMode === "attach" && resolvedEntryTls === "public-ip-cert" && vhostHttpPort === void 0) throw new Error("frp_attach_mode_requires_vhost_port");
|
|
1998
|
+
const publicOrigin = validateFrpPublicOrigin(record.publicOrigin);
|
|
1999
|
+
if (resolvedEntryTls === "self-signed" && isIP(new URL(publicOrigin).hostname) !== 4) throw new Error("frp_self_signed_requires_public_ipv4");
|
|
2000
|
+
return Object.freeze({
|
|
2001
|
+
version: 1,
|
|
2002
|
+
serverAddress: validateFrpServerAddress(record.serverAddress),
|
|
2003
|
+
serverPort: validateFrpServerPort(record.serverPort),
|
|
2004
|
+
token: validateFrpToken(record.token),
|
|
2005
|
+
publicOrigin,
|
|
2006
|
+
...mode === void 0 ? {} : { mode },
|
|
2007
|
+
...entryTls === void 0 ? {} : { entryTls },
|
|
2008
|
+
...vhostHttpPort === void 0 ? {} : { vhostHttpPort },
|
|
2009
|
+
...publicPort === void 0 ? {} : { publicPort }
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
function tomlString(value) {
|
|
2013
|
+
return JSON.stringify(value);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Build the single-purpose frpc configuration for the current loopback gateway.
|
|
2017
|
+
*
|
|
2018
|
+
* The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
|
|
2019
|
+
* for byte as before. The self-signed entry cannot use a vhost at all: frps only
|
|
2020
|
+
* forwards raw TCP, and the gateway terminates TLS on that connection.
|
|
2021
|
+
*/
|
|
2022
|
+
function createFrpcToml(settings, localPort) {
|
|
2023
|
+
if (!Number.isSafeInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("frp_local_port_invalid");
|
|
2024
|
+
const header = [
|
|
2025
|
+
`serverAddr = ${tomlString(settings.serverAddress)}`,
|
|
2026
|
+
`serverPort = ${String(settings.serverPort)}`,
|
|
2027
|
+
"auth.method = \"token\"",
|
|
2028
|
+
`auth.token = ${tomlString(settings.token)}`,
|
|
2029
|
+
"transport.tls.enable = true",
|
|
2030
|
+
"",
|
|
2031
|
+
"[[proxies]]",
|
|
2032
|
+
"name = \"dsh-mobile\""
|
|
2033
|
+
];
|
|
2034
|
+
if (isFrpSelfSignedIngress(settings)) return [
|
|
2035
|
+
...header,
|
|
2036
|
+
"type = \"tcp\"",
|
|
2037
|
+
"localIP = \"127.0.0.1\"",
|
|
2038
|
+
`localPort = ${String(localPort)}`,
|
|
2039
|
+
`remotePort = ${String(resolveFrpPublicPort(settings))}`,
|
|
2040
|
+
"transport.useEncryption = true",
|
|
2041
|
+
"transport.useCompression = true",
|
|
2042
|
+
""
|
|
2043
|
+
].join("\n");
|
|
2044
|
+
const hostnameValue = new URL(settings.publicOrigin).hostname;
|
|
2045
|
+
return [
|
|
2046
|
+
...header,
|
|
2047
|
+
"type = \"http\"",
|
|
2048
|
+
"localIP = \"127.0.0.1\"",
|
|
2049
|
+
`localPort = ${String(localPort)}`,
|
|
2050
|
+
`customDomains = [${tomlString(hostnameValue)}]`,
|
|
2051
|
+
"transport.useEncryption = true",
|
|
2052
|
+
"transport.useCompression = true",
|
|
2053
|
+
""
|
|
2054
|
+
].join("\n");
|
|
2055
|
+
}
|
|
2056
|
+
//#endregion
|
|
2057
|
+
//#region src/frp-attach.ts
|
|
2058
|
+
/** Default proof path used to confirm the public entry answered from this computer. */
|
|
2059
|
+
const FRP_ATTACH_DISCOVERY_PATH = "/mobile-access/discovery";
|
|
2060
|
+
/** Declaration printed above the VPS half of an attach template. */
|
|
2061
|
+
const FRP_ATTACH_VPS_DECLARATION = "# 本模板不安装、不改动、不重启你的 frps;只新增 Caddy 片段与证书。frps 侧需你自行确认 vhostHTTPPort=<N> 与 proxyBindAddr=\"127.0.0.1\"。";
|
|
2062
|
+
/** Declaration printed above the local half of an attach template. */
|
|
2063
|
+
const FRP_ATTACH_LOCAL_DECLARATION = "# 本机只写入 frpc.toml 并启动 frpc:不安装、不改动本机 frps,也不改动 DSH 自身配置。";
|
|
2064
|
+
/** Declaration for the self-signed passthrough, which keeps its CA on this computer. */
|
|
2065
|
+
const FRP_ATTACH_SELF_SIGNED_DECLARATION = "# 自签穿透档不使用 Caddy 或公开证书:frps 只做 TCP 透传,由本机 DSH 网关终止 TLS;服务端证书会在运行时续签,CA 到期需重新配对。";
|
|
2066
|
+
/**
|
|
2067
|
+
* One-line notice printed above the masked frpc.toml preview.
|
|
2068
|
+
*
|
|
2069
|
+
* It states both facts a reader cannot verify from the text itself: the real
|
|
2070
|
+
* file is written with 0600 permissions, and an unmasked copy would survive in
|
|
2071
|
+
* the system clipboard.
|
|
2072
|
+
*/
|
|
2073
|
+
function frpAttachTokenMaskNotice(configFile) {
|
|
2074
|
+
return `# 预览已打码:真正的 frpc.toml 已由插件以 0600 权限写入 ${configFile};明文 token 不打码会随复制进入系统剪贴板,故此处只显示占位符。`;
|
|
2075
|
+
}
|
|
2076
|
+
/** One-line notice printed above an explicitly revealed frpc.toml preview. */
|
|
2077
|
+
const FRP_ATTACH_TOKEN_REVEAL_NOTICE = "# 明文 token 已按你的显式操作显示:复制后即进入系统剪贴板并长期留存,请勿粘贴到公开位置。";
|
|
2078
|
+
/** Matches the single `auth.token` assignment of a generated frpc.toml. */
|
|
2079
|
+
const FRPC_TOKEN_ASSIGNMENT = /^auth\.token\s*=\s*"(?:[^"\\]|\\.)*"/mu;
|
|
2080
|
+
/**
|
|
2081
|
+
* Replace the `auth.token` value of a rendered frpc.toml with a fixed placeholder.
|
|
2082
|
+
*
|
|
2083
|
+
* Only the preview and clipboard paths call this: the file written to disk keeps
|
|
2084
|
+
* the real token, because frpc has to read it. The mapping never fails open — a
|
|
2085
|
+
* token line that cannot be masked raises instead of leaking the secret.
|
|
2086
|
+
*/
|
|
2087
|
+
function redactFrpcTomlToken(frpcToml) {
|
|
2088
|
+
if (!/(^|\n)auth\.token/u.test(frpcToml)) return frpcToml;
|
|
2089
|
+
const masked = frpcToml.replace(FRPC_TOKEN_ASSIGNMENT, `auth.token = "***"`);
|
|
2090
|
+
if (!masked.includes(`auth.token = "***"`)) throw new Error("frp_token_mask_failed");
|
|
2091
|
+
return masked;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* The frpc.toml exactly as the panel displays or copies it.
|
|
2095
|
+
*
|
|
2096
|
+
* `options.revealToken` is the only way to obtain the plaintext variant; the
|
|
2097
|
+
* default is the masked text. `createFrpcToml` itself is untouched, so the file
|
|
2098
|
+
* the provider writes stays byte-for-byte identical to the upstream output.
|
|
2099
|
+
*/
|
|
2100
|
+
function createFrpAttachFrpcToml(settings, options = {}) {
|
|
2101
|
+
const frpcToml = createFrpcToml(settings, attachLocalPort(options));
|
|
2102
|
+
return options.revealToken === true ? frpcToml : redactFrpcTomlToken(frpcToml);
|
|
2103
|
+
}
|
|
2104
|
+
const DEFAULT_ATTACH_CONFIG_FILE = "<DSH Mobile 私有目录>/remote/frp/config/frpc.toml";
|
|
2105
|
+
function attachConfigFile(options) {
|
|
2106
|
+
const file = options.configFile;
|
|
2107
|
+
if (file === void 0 || file === "") return DEFAULT_ATTACH_CONFIG_FILE;
|
|
2108
|
+
return file;
|
|
2109
|
+
}
|
|
2110
|
+
function attachLocalPort(options) {
|
|
2111
|
+
const port = options.localPort ?? 3443;
|
|
2112
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error("frp_local_port_invalid");
|
|
2113
|
+
return port;
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Reject every attach request that could not be carried out safely before any
|
|
2117
|
+
* artefact is produced. Attach never installs frps, so it must know the user's
|
|
2118
|
+
* real vhost port (unless the self-signed passthrough removes the vhost entirely).
|
|
2119
|
+
*/
|
|
2120
|
+
function validateAttachSettings(settings, options = {}) {
|
|
2121
|
+
if (resolveFrpMode(settings) !== "attach") throw new Error("frp_settings_invalid");
|
|
2122
|
+
if (isFrpSelfSignedIngress(settings)) {
|
|
2123
|
+
if (settings.mode !== "attach") throw new Error("frp_entry_tls_invalid");
|
|
2124
|
+
resolveFrpPublicPort(settings);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
const vhostHttpPort = options.vhostHttpPort ?? settings.vhostHttpPort;
|
|
2128
|
+
if (vhostHttpPort === void 0) throw new Error("frp_attach_mode_requires_vhost_port");
|
|
2129
|
+
if (!Number.isSafeInteger(vhostHttpPort) || vhostHttpPort < 1 || vhostHttpPort > 65535) throw new Error("frp_settings_invalid");
|
|
2130
|
+
}
|
|
2131
|
+
/** Strip the leading `# ` of a guide block so it can be pasted as runnable shell. */
|
|
2132
|
+
function guideCommands(guide) {
|
|
2133
|
+
return guide.split("\n").map((line) => line.replace(/^#\s?/u, "").trimEnd()).filter((line) => line !== "");
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Derive every VPS-side value once so the copied runbook and the panel plan can
|
|
2137
|
+
* never drift apart.
|
|
2138
|
+
*/
|
|
2139
|
+
function frpAttachVpsParts(settings, options = {}) {
|
|
2140
|
+
validateAttachSettings(settings, options);
|
|
2141
|
+
const publicHost = new URL(settings.publicOrigin).hostname;
|
|
2142
|
+
if (isFrpSelfSignedIngress(settings)) return Object.freeze({
|
|
2143
|
+
publicHost,
|
|
2144
|
+
selfSigned: true,
|
|
2145
|
+
publicPort: resolveFrpPublicPort(settings),
|
|
2146
|
+
snippet: "",
|
|
2147
|
+
certGuide: Object.freeze([])
|
|
2148
|
+
});
|
|
2149
|
+
const vhostHttpPort = options.vhostHttpPort ?? resolveFrpVhostHttpPort(settings);
|
|
2150
|
+
const certDir = options.certDir;
|
|
2151
|
+
const site = createCaddySite(publicHost, {
|
|
2152
|
+
vhostHttpPort,
|
|
2153
|
+
...certDir === void 0 ? {} : { certDir }
|
|
2154
|
+
});
|
|
2155
|
+
return Object.freeze({
|
|
2156
|
+
publicHost,
|
|
2157
|
+
selfSigned: false,
|
|
2158
|
+
vhostHttpPort,
|
|
2159
|
+
snippet: `${FRP_CADDY_SNIPPET_MARKER}\n${site.trimEnd()}\n`,
|
|
2160
|
+
certGuide: Object.freeze(isIP(publicHost) === 4 ? guideCommands(manualIpCertificateGuide(publicHost)) : [])
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Build the attach artefact for an existing frps.
|
|
2165
|
+
*
|
|
2166
|
+
* The result is deliberately split in two: the VPS half contains only what the
|
|
2167
|
+
* user's own server needs (Caddy snippet + import + certificate guidance, or a
|
|
2168
|
+
* single firewall rule for the self-signed passthrough), and the local half
|
|
2169
|
+
* contains the generated frpc.toml plus its `frpc verify` self-check. Neither
|
|
2170
|
+
* half installs, rewrites, or restarts the user's frps.
|
|
2171
|
+
*/
|
|
2172
|
+
function createFrpAttachTemplateParts(settings, options = {}) {
|
|
2173
|
+
const parts = frpAttachVpsParts(settings, options);
|
|
2174
|
+
const configFile = attachConfigFile(options);
|
|
2175
|
+
const revealed = options.revealToken === true;
|
|
2176
|
+
const frpcToml = createFrpAttachFrpcToml(settings, options);
|
|
2177
|
+
const local = [
|
|
2178
|
+
"# ---- (b) 本机侧(在这台电脑上执行)----",
|
|
2179
|
+
FRP_ATTACH_LOCAL_DECLARATION,
|
|
2180
|
+
revealed ? FRP_ATTACH_TOKEN_REVEAL_NOTICE : frpAttachTokenMaskNotice(configFile),
|
|
2181
|
+
`# frpc.toml 全文(插件同样会以 0600 权限写入 ${configFile})`,
|
|
2182
|
+
frpcToml.trimEnd(),
|
|
2183
|
+
"",
|
|
2184
|
+
"# 启动前自检:",
|
|
2185
|
+
`frpc verify -c "${configFile}"`,
|
|
2186
|
+
""
|
|
2187
|
+
].join("\n");
|
|
2188
|
+
if (parts.selfSigned) {
|
|
2189
|
+
const publicPort = parts.publicPort ?? resolveFrpPublicPort(settings);
|
|
2190
|
+
const vps = [
|
|
2191
|
+
"# ---- (a) VPS 侧(在你的服务器上执行,共 3 项)----",
|
|
2192
|
+
FRP_ATTACH_SELF_SIGNED_DECLARATION,
|
|
2193
|
+
"#",
|
|
2194
|
+
"# 1) 放行公网入口端口(就是这台电脑网关的 HTTPS 入口,TCP 透传不解密):",
|
|
2195
|
+
`ufw allow ${String(publicPort)}/tcp`,
|
|
2196
|
+
`ufw status | grep ${String(publicPort)}`,
|
|
2197
|
+
"# 若使用 firewalld / 云厂商安全组,请放行同样的 TCP 端口。",
|
|
2198
|
+
"# 前提:现有 frps 的 TCP 代理已监听公网地址;若 proxyBindAddr=127.0.0.1,放行防火墙仍无法从公网访问。",
|
|
2199
|
+
"# 插件不会修改你现有的 frps;不满足此前提时请改选受信任证书入口。",
|
|
2200
|
+
"#",
|
|
2201
|
+
"# 2) 确认你既有的 frps 已就绪(无需修改它的配置):",
|
|
2202
|
+
"systemctl is-active frps || true",
|
|
2203
|
+
`ss -lnt | grep -E ':(${String(settings.serverPort)}|${String(publicPort)})\\b' || true`,
|
|
2204
|
+
"#",
|
|
2205
|
+
"# 3) frpc 启动后,用 -k 检查连通性;此命令不验证身份,App 配对后会固定网关 CA:",
|
|
2206
|
+
`curl -k -sS -o /dev/null -w '%{http_code}\\n' https://${parts.publicHost}:${String(publicPort)}${FRP_ATTACH_DISCOVERY_PATH}`,
|
|
2207
|
+
"# 期望输出 200。",
|
|
2208
|
+
""
|
|
2209
|
+
].join("\n");
|
|
2210
|
+
return Object.freeze({
|
|
2211
|
+
mode: "attach",
|
|
2212
|
+
vps,
|
|
2213
|
+
local,
|
|
2214
|
+
text: `${vps}${local}`
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
const vps = [
|
|
2218
|
+
"# ---- (a) VPS 侧(在你的服务器上执行)----",
|
|
2219
|
+
FRP_ATTACH_VPS_DECLARATION,
|
|
2220
|
+
"#",
|
|
2221
|
+
`# 1) 保存 Caddy 片段到 ${FRP_CADDY_SNIPPET_PATH}(只新增这一个文件):`,
|
|
2222
|
+
`cat > ${FRP_CADDY_SNIPPET_PATH} <<'DSH_MOBILE_CADDY_SNIPPET'`,
|
|
2223
|
+
parts.snippet.trimEnd(),
|
|
2224
|
+
"DSH_MOBILE_CADDY_SNIPPET",
|
|
2225
|
+
"#",
|
|
2226
|
+
"# 2) 确认 Caddyfile 顶部含下面这一行(全局块必须在前;没有则加到文件最上方):",
|
|
2227
|
+
`# ${FRP_CADDY_IMPORT_LINE}`,
|
|
2228
|
+
"caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy",
|
|
2229
|
+
"#",
|
|
2230
|
+
...parts.certGuide.length === 0 ? ["# 3) 域名 HTTPS:确认 DNS 指向 VPS,开放 80/443;Caddy 会自动申请和续期证书。"] : [
|
|
2231
|
+
"# 3) 公网 IP 的 HTTPS 证书(Caddy 不能为 IP 自动签发,故用 certbot 签发):",
|
|
2232
|
+
...parts.certGuide.map((line) => `# ${line}`),
|
|
2233
|
+
"#",
|
|
2234
|
+
"# 4) 确认证书续期任务,并在续期后将新证书安装到 Caddy:",
|
|
2235
|
+
"systemctl list-timers certbot.timer --all || true"
|
|
2236
|
+
],
|
|
2237
|
+
"#",
|
|
2238
|
+
"# 端到端验证(公网入口为 443,与 frps 的 vhost 端口无关):",
|
|
2239
|
+
`curl -sS -o /dev/null -w '%{http_code}\\n' https://${parts.publicHost}${FRP_ATTACH_DISCOVERY_PATH}`,
|
|
2240
|
+
"# 期望输出 200。",
|
|
2241
|
+
""
|
|
2242
|
+
].join("\n");
|
|
2243
|
+
return Object.freeze({
|
|
2244
|
+
mode: "attach",
|
|
2245
|
+
vps,
|
|
2246
|
+
local,
|
|
2247
|
+
text: `${vps}${local}`
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
/** Combined attach artefact, ready for the clipboard. */
|
|
2251
|
+
function createFrpAttachTemplate(settings, options = {}) {
|
|
2252
|
+
return createFrpAttachTemplateParts(settings, options).text;
|
|
2253
|
+
}
|
|
1532
2254
|
/** Wire bounds mirrored by the Android notification policy. */
|
|
1533
2255
|
const TASK_NOTIFY_LIMITS = Object.freeze({
|
|
1534
2256
|
title: 80,
|
|
@@ -1851,8 +2573,10 @@ window.__ModuleLoader__.load({
|
|
|
1851
2573
|
.dsh-mobile-settings_rowText { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; padding-right:48px; }
|
|
1852
2574
|
.dsh-mobile-settings_title { color:var(--dsw-alias-label-primary); font-size:14px; font-weight:400; line-height:22px; }
|
|
1853
2575
|
.dsh-mobile-settings_desc { color:var(--dsw-alias-label-tertiary); font-size:12px; font-weight:400; line-height:18px; }
|
|
1854
|
-
.dsh-mobile-settings_selector { display:inline-flex; align-items:center; justify-content:center; min-width:max-content; height:
|
|
2576
|
+
.dsh-mobile-settings_selector { display:inline-flex; align-items:center; justify-content:center; min-width:max-content; min-height:48px; padding:0 14px; border:0; border-radius:24px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; font-size:14px; line-height:22px; cursor:pointer; }
|
|
1855
2577
|
.dsh-mobile-settings_selector:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); }
|
|
2578
|
+
.dsh-mobile-settings_selector:active:not(:disabled) { background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)); }
|
|
2579
|
+
.dsh-mobile-settings_selector:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
|
|
1856
2580
|
.dsh-mobile-settings_selector:disabled { cursor:wait; opacity:.55; }
|
|
1857
2581
|
@media ${NATIVE_MOBILE_OVERLAY_QUERY} {
|
|
1858
2582
|
html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
|
|
@@ -1923,7 +2647,7 @@ window.__ModuleLoader__.load({
|
|
|
1923
2647
|
[data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
|
|
1924
2648
|
[data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
|
|
1925
2649
|
[data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_rowText"] { width:100% !important; padding-right:0 !important; }
|
|
1926
|
-
[data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_selector"] { box-sizing:border-box !important; align-self:flex-start !important; justify-content:space-between !important; min-width:0 !important; min-height:
|
|
2650
|
+
[data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_selector"] { box-sizing:border-box !important; align-self:flex-start !important; justify-content:space-between !important; min-width:0 !important; min-height:48px !important; max-width:100% !important; }
|
|
1927
2651
|
[data-dsh-mobile-settings-options] :is(input,select,textarea,button) { max-width:100%; }
|
|
1928
2652
|
[data-dsh-mobile-settings-options] :is(input,select,textarea) { box-sizing:border-box; width:100%; min-width:0; }
|
|
1929
2653
|
[data-dsh-mobile-settings-options] [class*="_head"] { min-width:0; flex-wrap:wrap; }
|
|
@@ -2904,6 +3628,90 @@ window.__ModuleLoader__.load({
|
|
|
2904
3628
|
onClick: invoke
|
|
2905
3629
|
}, state === "busy" ? copy.busy : state === "error" ? copy.retry : copy.action));
|
|
2906
3630
|
}
|
|
3631
|
+
function mobileTaskNotificationCopy(locale) {
|
|
3632
|
+
if (locale === "zh") return {
|
|
3633
|
+
title: "任务通知",
|
|
3634
|
+
description: "在 Android 系统中开启任务完成和待确认提醒。",
|
|
3635
|
+
action: "通知设置",
|
|
3636
|
+
busy: "正在打开…",
|
|
3637
|
+
success: "请在系统提示或设置中确认任务通知权限。",
|
|
3638
|
+
reopen: "重新打开",
|
|
3639
|
+
error: "无法打开通知设置,请重试。",
|
|
3640
|
+
retry: "重试"
|
|
3641
|
+
};
|
|
3642
|
+
if (locale === "it") return {
|
|
3643
|
+
title: "Notifiche delle attività",
|
|
3644
|
+
description: "Attiva gli avvisi Android per le attività completate o in attesa di conferma.",
|
|
3645
|
+
action: "Impostazioni notifiche",
|
|
3646
|
+
busy: "Apertura…",
|
|
3647
|
+
success: "Conferma il permesso nella richiesta o nelle impostazioni di sistema.",
|
|
3648
|
+
reopen: "Riapri",
|
|
3649
|
+
error: "Impossibile aprire le impostazioni delle notifiche. Riprova.",
|
|
3650
|
+
retry: "Riprova"
|
|
3651
|
+
};
|
|
3652
|
+
return {
|
|
3653
|
+
title: "Task notifications",
|
|
3654
|
+
description: "Enable Android reminders for completed tasks and pending confirmation.",
|
|
3655
|
+
action: "Notification settings",
|
|
3656
|
+
busy: "Opening…",
|
|
3657
|
+
success: "Confirm notification access in the system prompt or settings.",
|
|
3658
|
+
reopen: "Open again",
|
|
3659
|
+
error: "Could not open notification settings. Try again.",
|
|
3660
|
+
retry: "Retry"
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
/** Render the Android-only notification permission entry in DSH General settings. */
|
|
3664
|
+
function MobileTaskNotificationRow() {
|
|
3665
|
+
const [nativeReady, setNativeReady] = (0, react.useState)(false);
|
|
3666
|
+
const [state, setState] = (0, react.useState)("idle");
|
|
3667
|
+
(0, react.useEffect)(() => {
|
|
3668
|
+
let disposed = false;
|
|
3669
|
+
const checkNativeAction = () => {
|
|
3670
|
+
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
3671
|
+
if (bridge === void 0) {
|
|
3672
|
+
setNativeReady(false);
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
|
|
3676
|
+
if (!disposed) setNativeReady(capabilities.includes("notification.settings"));
|
|
3677
|
+
}, () => {
|
|
3678
|
+
if (!disposed) setNativeReady(false);
|
|
3679
|
+
});
|
|
3680
|
+
};
|
|
3681
|
+
checkNativeAction();
|
|
3682
|
+
window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
|
|
3683
|
+
return () => {
|
|
3684
|
+
disposed = true;
|
|
3685
|
+
window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
|
|
3686
|
+
};
|
|
3687
|
+
}, []);
|
|
3688
|
+
if (!nativeReady) return null;
|
|
3689
|
+
const locale = selectedMobileControlLocale();
|
|
3690
|
+
const copy = mobileTaskNotificationCopy(locale);
|
|
3691
|
+
const invoke = () => {
|
|
3692
|
+
const bridge = window.__DSH_MOBILE_NATIVE__;
|
|
3693
|
+
if (bridge === void 0 || state === "busy") return;
|
|
3694
|
+
setState("busy");
|
|
3695
|
+
bridge.invoke("notification.settings", {}).then(() => {
|
|
3696
|
+
setState("success");
|
|
3697
|
+
}, () => {
|
|
3698
|
+
setState("error");
|
|
3699
|
+
});
|
|
3700
|
+
};
|
|
3701
|
+
return (0, react.createElement)("div", {
|
|
3702
|
+
className: "dsh-mobile-settings_row",
|
|
3703
|
+
lang: locale
|
|
3704
|
+
}, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
|
|
3705
|
+
className: "dsh-mobile-settings_desc",
|
|
3706
|
+
"aria-live": "polite"
|
|
3707
|
+
}, state === "success" ? copy.success : state === "error" ? copy.error : copy.description)), (0, react.createElement)("button", {
|
|
3708
|
+
type: "button",
|
|
3709
|
+
className: "dsh-mobile-settings_selector",
|
|
3710
|
+
disabled: state === "busy",
|
|
3711
|
+
"aria-busy": state === "busy" ? "true" : void 0,
|
|
3712
|
+
onClick: invoke
|
|
3713
|
+
}, state === "busy" ? copy.busy : state === "success" ? copy.reopen : state === "error" ? copy.retry : copy.action));
|
|
3714
|
+
}
|
|
2907
3715
|
const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
|
|
2908
3716
|
const LONG_CONTROL_REQUEST_TIMEOUT_MS = 21e4;
|
|
2909
3717
|
const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
|
|
@@ -2973,6 +3781,50 @@ window.__ModuleLoader__.load({
|
|
|
2973
3781
|
function createFrpServerTemplateForClipboard(serverPort, token, publicOrigin) {
|
|
2974
3782
|
return createRestrictedFrpServerTemplate(serverPort, token, publicOrigin);
|
|
2975
3783
|
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Validate the panel form with the same parser the host uses, so a runbook can
|
|
3786
|
+
* never describe a topology the provider would later refuse to start.
|
|
3787
|
+
*/
|
|
3788
|
+
function attachClipboardSettings(form) {
|
|
3789
|
+
return parseFrpSettings({
|
|
3790
|
+
serverAddress: form.serverAddress,
|
|
3791
|
+
serverPort: form.serverPort,
|
|
3792
|
+
token: form.token,
|
|
3793
|
+
publicOrigin: form.publicOrigin,
|
|
3794
|
+
mode: "attach",
|
|
3795
|
+
entryTls: form.entryTls,
|
|
3796
|
+
...form.entryTls === "self-signed" || form.vhostHttpPort === void 0 ? {} : { vhostHttpPort: form.vhostHttpPort },
|
|
3797
|
+
...form.publicPort === void 0 ? {} : { publicPort: form.publicPort }
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Build the copy-only attach runbook locally, so the shared token never leaves
|
|
3802
|
+
* the browser: the panel only ever copies text out. The text is masked by
|
|
3803
|
+
* default; `options.revealToken` is the explicit opt-in.
|
|
3804
|
+
*/
|
|
3805
|
+
function createFrpAttachTemplateForClipboard(form, options = {}) {
|
|
3806
|
+
return createFrpAttachTemplate(attachClipboardSettings(form), options);
|
|
3807
|
+
}
|
|
3808
|
+
/**
|
|
3809
|
+
* The frpc.toml exactly as the plugin writes it (0600), for the panel's explicit
|
|
3810
|
+
* reveal action only. Every other path keeps the token masked.
|
|
3811
|
+
*/
|
|
3812
|
+
function createFrpAttachFrpcTomlForClipboard(form, options = {}) {
|
|
3813
|
+
return createFrpAttachFrpcToml(attachClipboardSettings(form), options);
|
|
3814
|
+
}
|
|
3815
|
+
/**
|
|
3816
|
+
* Attach-mode validation for the panel. The parser is the single source of
|
|
3817
|
+
* truth, so the panel reports exactly the codes the host would return instead of
|
|
3818
|
+
* duplicating the rules (and drifting from them).
|
|
3819
|
+
*/
|
|
3820
|
+
function frpAttachFormErrorCode(form) {
|
|
3821
|
+
try {
|
|
3822
|
+
createFrpAttachTemplateForClipboard(form);
|
|
3823
|
+
return;
|
|
3824
|
+
} catch (error) {
|
|
3825
|
+
return error instanceof Error ? error.message : "frp_settings_invalid";
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
2976
3828
|
const ORIGIN_ERROR_MESSAGE_KEYS = {
|
|
2977
3829
|
origin_public_origin_invalid: "originPublicOriginInvalid",
|
|
2978
3830
|
origin_listen_host_invalid: "originListenHostInvalid",
|
|
@@ -2988,74 +3840,141 @@ window.__ModuleLoader__.load({
|
|
|
2988
3840
|
origin_listen_address_unavailable: "originAddressUnavailable",
|
|
2989
3841
|
origin_gateway_start_failed: "originStartFailed"
|
|
2990
3842
|
};
|
|
3843
|
+
/** Provider failure codes shared by status, failed requests, and diagnostics. */
|
|
3844
|
+
const REMOTE_ERROR_MESSAGE_KEYS = {
|
|
3845
|
+
funnel_permission_required: "funnelPermission",
|
|
3846
|
+
funnel_https_required: "funnelHttps",
|
|
3847
|
+
funnel_start_failed: "funnelStart",
|
|
3848
|
+
funnel_start_timeout: "funnelTimeout",
|
|
3849
|
+
tailscale_dns_missing: "tailscaleDnsMissing",
|
|
3850
|
+
gateway_start_failed: "gatewayStartFailed",
|
|
3851
|
+
control_channel_failed: "controlChannelFailed",
|
|
3852
|
+
cpolar_component_missing: "cpolarMissing",
|
|
3853
|
+
cpolar_component_invalid: "cpolarInvalid",
|
|
3854
|
+
cpolar_config_missing: "cpolarConfigMissing",
|
|
3855
|
+
cpolar_config_invalid: "cpolarConfigInvalid",
|
|
3856
|
+
cpolar_port_unavailable: "cpolarPortUnavailable",
|
|
3857
|
+
cpolar_launch_failed: "cpolarLaunchFailed",
|
|
3858
|
+
cpolar_start_timeout: "cpolarTimeout",
|
|
3859
|
+
cpolar_stopped: "cpolarStopped",
|
|
3860
|
+
cpolar_exited: "cpolarExited",
|
|
3861
|
+
cpolar_invalid_output: "cpolarOutputInvalid",
|
|
3862
|
+
cpolar_invalid_origin: "cpolarOriginInvalid",
|
|
3863
|
+
cloudflared_component_missing: "cloudflaredMissing",
|
|
3864
|
+
cloudflared_component_invalid: "cloudflaredInvalid",
|
|
3865
|
+
cloudflared_port_unavailable: "cloudflaredPortUnavailable",
|
|
3866
|
+
cloudflared_launch_failed: "cloudflaredLaunchFailed",
|
|
3867
|
+
cloudflared_start_timeout: "cloudflaredTimeout",
|
|
3868
|
+
cloudflared_stopped: "cloudflaredStopped",
|
|
3869
|
+
cloudflared_exited: "cloudflaredExited",
|
|
3870
|
+
cloudflared_invalid_output: "cloudflaredOutputInvalid",
|
|
3871
|
+
cloudflared_invalid_origin: "cloudflaredOriginInvalid",
|
|
3872
|
+
cloudflared_tunnel_port_unavailable: "cloudflaredTunnelPortUnavailable",
|
|
3873
|
+
cloudflared_tunnel_hostname_invalid: "cloudflaredTunnelHostnameInvalid",
|
|
3874
|
+
cloudflared_tunnel_port_invalid: "cloudflaredTunnelPortInvalid",
|
|
3875
|
+
cloudflared_tunnel_port_reserved: "cloudflaredTunnelPortReserved",
|
|
3876
|
+
cloudflared_tunnel_token_invalid: "cloudflaredTunnelTokenInvalid",
|
|
3877
|
+
cloudflared_tunnel_settings_invalid: "cloudflaredTunnelSettingsInvalid",
|
|
3878
|
+
cloudflared_tunnel_config_missing: "cloudflaredTunnelConfigMissing",
|
|
3879
|
+
cloudflared_tunnel_config_invalid: "cloudflaredTunnelInvalid",
|
|
3880
|
+
cloudflared_tunnel_target_invalid: "cloudflaredTunnelTargetInvalid",
|
|
3881
|
+
cloudflared_component_unsupported: "cloudflaredComponentUnsupported",
|
|
3882
|
+
cloudflared_download_hash_mismatch: "cloudflaredDownloadHashMismatch",
|
|
3883
|
+
cloudflared_download_size_mismatch: "cloudflaredDownloadSizeMismatch",
|
|
3884
|
+
cloudflared_executable_hash_mismatch: "cloudflaredExecutableHashMismatch",
|
|
3885
|
+
cloudflared_download_redirect_missing: "cloudflaredDownloadRedirectMissing",
|
|
3886
|
+
cloudflared_download_redirect_invalid: "cloudflaredDownloadRedirectInvalid",
|
|
3887
|
+
cloudflared_download_redirect_rejected: "cloudflaredDownloadRedirectRejected",
|
|
3888
|
+
cloudflared_port_reservation_failed: "cloudflaredPortReservationFailed",
|
|
3889
|
+
frp_component_missing: "frpMissing",
|
|
3890
|
+
frp_component_invalid: "frpInvalid",
|
|
3891
|
+
frp_config_missing: "frpConfigMissing",
|
|
3892
|
+
frp_config_verify_failed: "frpConfigVerifyFailed",
|
|
3893
|
+
frp_vhost_publicly_reachable: "frpVhostPublic",
|
|
3894
|
+
frp_vhost_probe_failed: "frpVhostProbeFailed",
|
|
3895
|
+
frp_launch_failed: "frpLaunchFailed",
|
|
3896
|
+
frp_start_timeout: "frpTimeout",
|
|
3897
|
+
frp_discovery_mismatch: "frpDiscoveryMismatch",
|
|
3898
|
+
frp_discovery_invalid: "frpDiscoveryInvalid",
|
|
3899
|
+
frp_stopped: "frpStopped",
|
|
3900
|
+
frp_exited: "frpExited",
|
|
3901
|
+
frp_attach_mode_requires_vhost_port: "frpAttachModeRequiresVhostPort",
|
|
3902
|
+
frp_attach_cert_unknown: "frpAttachCertUnknownError",
|
|
3903
|
+
frp_entry_tls_invalid: "frpEntryTlsInvalid",
|
|
3904
|
+
frp_self_signed_requires_public_ipv4: "frpSelfSignedRequiresPublicIpv4",
|
|
3905
|
+
frp_ingress_ca_expired: "frpIngressCaExpired",
|
|
3906
|
+
frp_ingress_ca_invalid: "frpIngressCaInvalid",
|
|
3907
|
+
frp_ingress_ca_changed: "frpIngressCaChanged",
|
|
3908
|
+
frp_ingress_renewal_failed: "frpIngressRenewalFailed",
|
|
3909
|
+
...ORIGIN_ERROR_MESSAGE_KEYS
|
|
3910
|
+
};
|
|
3911
|
+
const DIAGNOSTIC_CONTROLLER_ACTION_OVERRIDES = {
|
|
3912
|
+
component_missing: "remoteUnavailableTailscale",
|
|
3913
|
+
sidecar_launch_failed: "remoteUnavailableTailscale",
|
|
3914
|
+
sidecar_stopped: "controlChannelFailed",
|
|
3915
|
+
sidecar_exited: "controlChannelFailed"
|
|
3916
|
+
};
|
|
3917
|
+
/** Only codes with a specific diagnostic action use the status message key. */
|
|
3918
|
+
const DIAGNOSTIC_CONTROLLER_ACTION_CODES = /* @__PURE__ */ new Set([
|
|
3919
|
+
...Object.keys(ORIGIN_ERROR_MESSAGE_KEYS),
|
|
3920
|
+
"funnel_permission_required",
|
|
3921
|
+
"funnel_https_required",
|
|
3922
|
+
"funnel_start_failed",
|
|
3923
|
+
"funnel_start_timeout",
|
|
3924
|
+
"tailscale_dns_missing",
|
|
3925
|
+
"control_channel_failed",
|
|
3926
|
+
"gateway_start_failed",
|
|
3927
|
+
"cpolar_component_missing",
|
|
3928
|
+
"cpolar_component_invalid",
|
|
3929
|
+
"cpolar_config_missing",
|
|
3930
|
+
"cpolar_config_invalid",
|
|
3931
|
+
"cpolar_start_timeout",
|
|
3932
|
+
"cpolar_stopped",
|
|
3933
|
+
"cpolar_exited",
|
|
3934
|
+
"cloudflared_component_missing",
|
|
3935
|
+
"cloudflared_component_invalid",
|
|
3936
|
+
"cloudflared_component_unsupported",
|
|
3937
|
+
"cloudflared_port_unavailable",
|
|
3938
|
+
"cloudflared_port_reservation_failed",
|
|
3939
|
+
"cloudflared_launch_failed",
|
|
3940
|
+
"cloudflared_start_timeout",
|
|
3941
|
+
"cloudflared_stopped",
|
|
3942
|
+
"cloudflared_exited",
|
|
3943
|
+
"cloudflared_invalid_origin",
|
|
3944
|
+
"cloudflared_tunnel_port_unavailable",
|
|
3945
|
+
"cloudflared_tunnel_config_invalid",
|
|
3946
|
+
"cloudflared_tunnel_target_invalid",
|
|
3947
|
+
"cloudflared_download_hash_mismatch",
|
|
3948
|
+
"cloudflared_download_size_mismatch",
|
|
3949
|
+
"cloudflared_executable_hash_mismatch",
|
|
3950
|
+
"frp_component_missing",
|
|
3951
|
+
"frp_component_invalid",
|
|
3952
|
+
"frp_config_missing",
|
|
3953
|
+
"frp_config_verify_failed",
|
|
3954
|
+
"frp_vhost_publicly_reachable",
|
|
3955
|
+
"frp_vhost_probe_failed",
|
|
3956
|
+
"frp_launch_failed",
|
|
3957
|
+
"frp_start_timeout",
|
|
3958
|
+
"frp_discovery_mismatch",
|
|
3959
|
+
"frp_discovery_invalid",
|
|
3960
|
+
"frp_stopped",
|
|
3961
|
+
"frp_exited",
|
|
3962
|
+
"frp_attach_mode_requires_vhost_port",
|
|
3963
|
+
"frp_attach_cert_unknown",
|
|
3964
|
+
"frp_entry_tls_invalid",
|
|
3965
|
+
"frp_self_signed_requires_public_ipv4",
|
|
3966
|
+
"frp_ingress_ca_expired",
|
|
3967
|
+
"frp_ingress_ca_invalid",
|
|
3968
|
+
"frp_ingress_ca_changed",
|
|
3969
|
+
"frp_ingress_renewal_failed"
|
|
3970
|
+
]);
|
|
3971
|
+
/** Resolve the next-step text for a remote-controller diagnostic. */
|
|
3972
|
+
function diagnosticControllerAction(code, locale, fallback) {
|
|
3973
|
+
if (typeof code !== "string") return fallback;
|
|
3974
|
+
const key = DIAGNOSTIC_CONTROLLER_ACTION_OVERRIDES[code] ?? (DIAGNOSTIC_CONTROLLER_ACTION_CODES.has(code) ? REMOTE_ERROR_MESSAGE_KEYS[code] : void 0);
|
|
3975
|
+
return key === void 0 ? fallback : controlTranslator(locale)(key);
|
|
3976
|
+
}
|
|
2991
3977
|
function installControl() {
|
|
2992
|
-
/**
|
|
2993
|
-
* Provider failure codes to message keys.
|
|
2994
|
-
*
|
|
2995
|
-
* Kept as keys rather than translated text so the same table can serve both the
|
|
2996
|
-
* status repaint and a failed request: a server rejection carries its code in
|
|
2997
|
-
* `error.message`, and showing that raw would print `Error: cloudflared_...` in
|
|
2998
|
-
* every language while the translated sentence sat unreachable.
|
|
2999
|
-
*/
|
|
3000
|
-
const REMOTE_ERROR_MESSAGE_KEYS = {
|
|
3001
|
-
funnel_permission_required: "funnelPermission",
|
|
3002
|
-
funnel_https_required: "funnelHttps",
|
|
3003
|
-
funnel_start_failed: "funnelStart",
|
|
3004
|
-
funnel_start_timeout: "funnelTimeout",
|
|
3005
|
-
tailscale_dns_missing: "tailscaleDnsMissing",
|
|
3006
|
-
gateway_start_failed: "gatewayStartFailed",
|
|
3007
|
-
control_channel_failed: "controlChannelFailed",
|
|
3008
|
-
cpolar_component_missing: "cpolarMissing",
|
|
3009
|
-
cpolar_component_invalid: "cpolarInvalid",
|
|
3010
|
-
cpolar_config_missing: "cpolarConfigMissing",
|
|
3011
|
-
cpolar_config_invalid: "cpolarConfigInvalid",
|
|
3012
|
-
cpolar_port_unavailable: "cpolarPortUnavailable",
|
|
3013
|
-
cpolar_launch_failed: "cpolarLaunchFailed",
|
|
3014
|
-
cpolar_start_timeout: "cpolarTimeout",
|
|
3015
|
-
cpolar_stopped: "cpolarStopped",
|
|
3016
|
-
cpolar_exited: "cpolarExited",
|
|
3017
|
-
cpolar_invalid_output: "cpolarOutputInvalid",
|
|
3018
|
-
cpolar_invalid_origin: "cpolarOriginInvalid",
|
|
3019
|
-
cloudflared_component_missing: "cloudflaredMissing",
|
|
3020
|
-
cloudflared_component_invalid: "cloudflaredInvalid",
|
|
3021
|
-
cloudflared_port_unavailable: "cloudflaredPortUnavailable",
|
|
3022
|
-
cloudflared_launch_failed: "cloudflaredLaunchFailed",
|
|
3023
|
-
cloudflared_start_timeout: "cloudflaredTimeout",
|
|
3024
|
-
cloudflared_stopped: "cloudflaredStopped",
|
|
3025
|
-
cloudflared_exited: "cloudflaredExited",
|
|
3026
|
-
cloudflared_invalid_output: "cloudflaredOutputInvalid",
|
|
3027
|
-
cloudflared_invalid_origin: "cloudflaredOriginInvalid",
|
|
3028
|
-
cloudflared_tunnel_port_unavailable: "cloudflaredTunnelPortUnavailable",
|
|
3029
|
-
cloudflared_tunnel_hostname_invalid: "cloudflaredTunnelHostnameInvalid",
|
|
3030
|
-
cloudflared_tunnel_port_invalid: "cloudflaredTunnelPortInvalid",
|
|
3031
|
-
cloudflared_tunnel_port_reserved: "cloudflaredTunnelPortReserved",
|
|
3032
|
-
cloudflared_tunnel_token_invalid: "cloudflaredTunnelTokenInvalid",
|
|
3033
|
-
cloudflared_tunnel_settings_invalid: "cloudflaredTunnelSettingsInvalid",
|
|
3034
|
-
cloudflared_tunnel_config_missing: "cloudflaredTunnelConfigMissing",
|
|
3035
|
-
cloudflared_tunnel_config_invalid: "cloudflaredTunnelInvalid",
|
|
3036
|
-
cloudflared_tunnel_target_invalid: "cloudflaredTunnelTargetInvalid",
|
|
3037
|
-
cloudflared_component_unsupported: "cloudflaredComponentUnsupported",
|
|
3038
|
-
cloudflared_download_hash_mismatch: "cloudflaredDownloadHashMismatch",
|
|
3039
|
-
cloudflared_download_size_mismatch: "cloudflaredDownloadSizeMismatch",
|
|
3040
|
-
cloudflared_executable_hash_mismatch: "cloudflaredExecutableHashMismatch",
|
|
3041
|
-
cloudflared_download_redirect_missing: "cloudflaredDownloadRedirectMissing",
|
|
3042
|
-
cloudflared_download_redirect_invalid: "cloudflaredDownloadRedirectInvalid",
|
|
3043
|
-
cloudflared_download_redirect_rejected: "cloudflaredDownloadRedirectRejected",
|
|
3044
|
-
cloudflared_port_reservation_failed: "cloudflaredPortReservationFailed",
|
|
3045
|
-
frp_component_missing: "frpMissing",
|
|
3046
|
-
frp_component_invalid: "frpInvalid",
|
|
3047
|
-
frp_config_missing: "frpConfigMissing",
|
|
3048
|
-
frp_config_verify_failed: "frpConfigVerifyFailed",
|
|
3049
|
-
frp_vhost_publicly_reachable: "frpVhostPublic",
|
|
3050
|
-
frp_vhost_probe_failed: "frpVhostProbeFailed",
|
|
3051
|
-
frp_launch_failed: "frpLaunchFailed",
|
|
3052
|
-
frp_start_timeout: "frpTimeout",
|
|
3053
|
-
frp_discovery_mismatch: "frpDiscoveryMismatch",
|
|
3054
|
-
frp_discovery_invalid: "frpDiscoveryInvalid",
|
|
3055
|
-
frp_stopped: "frpStopped",
|
|
3056
|
-
frp_exited: "frpExited",
|
|
3057
|
-
...ORIGIN_ERROR_MESSAGE_KEYS
|
|
3058
|
-
};
|
|
3059
3978
|
/** The table above, translated, for the status repaint. */
|
|
3060
3979
|
const translatedRemoteErrors = () => Object.fromEntries(Object.entries(REMOTE_ERROR_MESSAGE_KEYS).map(([code, key]) => [code, t(key)]));
|
|
3061
3980
|
/**
|
|
@@ -3515,11 +4434,58 @@ window.__ModuleLoader__.load({
|
|
|
3515
4434
|
frpOrigin.autocomplete = "off";
|
|
3516
4435
|
frpOrigin.spellcheck = false;
|
|
3517
4436
|
frpOrigin.placeholder = t("frpPublicOriginPlaceholder");
|
|
4437
|
+
const frpModeLabel = element("label", "dsh-mobile-control__field");
|
|
4438
|
+
frpModeLabel.textContent = t("frpMode");
|
|
4439
|
+
const frpMode = element("select");
|
|
4440
|
+
for (const [value, label] of [["deploy", t("frpModeDeploy")], ["attach", t("frpModeAttach")]]) {
|
|
4441
|
+
const option = element("option");
|
|
4442
|
+
option.value = value;
|
|
4443
|
+
option.textContent = label;
|
|
4444
|
+
frpMode.append(option);
|
|
4445
|
+
}
|
|
4446
|
+
const frpModeHint = element("p", "dsh-mobile-control__frp-hint");
|
|
4447
|
+
frpModeHint.textContent = t("frpModeHint");
|
|
4448
|
+
frpModeLabel.append(frpMode, frpModeHint);
|
|
4449
|
+
const frpEntryTlsLabel = element("label", "dsh-mobile-control__field");
|
|
4450
|
+
frpEntryTlsLabel.textContent = t("frpEntryTls");
|
|
4451
|
+
const frpEntryTls = element("select");
|
|
4452
|
+
for (const [value, label] of [["public-ip-cert", t("frpEntryTlsPublic")], ["self-signed", t("frpEntryTlsSelfSigned")]]) {
|
|
4453
|
+
const option = element("option");
|
|
4454
|
+
option.value = value;
|
|
4455
|
+
option.textContent = label;
|
|
4456
|
+
frpEntryTls.append(option);
|
|
4457
|
+
}
|
|
4458
|
+
frpEntryTlsLabel.append(frpEntryTls);
|
|
4459
|
+
const frpVhostLabel = element("label", "dsh-mobile-control__field");
|
|
4460
|
+
frpVhostLabel.textContent = t("frpVhostHttpPort");
|
|
4461
|
+
const frpVhostPort = element("input");
|
|
4462
|
+
frpVhostPort.type = "number";
|
|
4463
|
+
frpVhostPort.inputMode = "numeric";
|
|
4464
|
+
frpVhostPort.min = "1";
|
|
4465
|
+
frpVhostPort.max = "65535";
|
|
4466
|
+
frpVhostPort.placeholder = "7080";
|
|
4467
|
+
const frpVhostHint = element("p", "dsh-mobile-control__frp-hint");
|
|
4468
|
+
frpVhostHint.id = "dsh-mobile-frp-vhost-hint";
|
|
4469
|
+
frpVhostHint.textContent = t("frpVhostHttpPortHint");
|
|
4470
|
+
frpVhostPort.setAttribute("aria-describedby", frpVhostHint.id);
|
|
4471
|
+
frpVhostLabel.append(frpVhostPort, frpVhostHint);
|
|
4472
|
+
const frpPublicPortLabel = element("label", "dsh-mobile-control__field");
|
|
4473
|
+
frpPublicPortLabel.textContent = t("frpPublicPort");
|
|
4474
|
+
const frpPublicPort = element("input");
|
|
4475
|
+
frpPublicPort.type = "number";
|
|
4476
|
+
frpPublicPort.inputMode = "numeric";
|
|
4477
|
+
frpPublicPort.min = "1";
|
|
4478
|
+
frpPublicPort.max = "65535";
|
|
4479
|
+
frpPublicPort.value = "33080";
|
|
4480
|
+
frpPublicPort.placeholder = "33080";
|
|
4481
|
+
const frpPublicPortHint = element("p", "dsh-mobile-control__frp-hint");
|
|
4482
|
+
frpPublicPortHint.textContent = t("frpPublicPortHint");
|
|
4483
|
+
frpPublicPortLabel.append(frpPublicPort, frpPublicPortHint);
|
|
3518
4484
|
frpServerLabel.append(frpServer);
|
|
3519
4485
|
frpPortLabel.append(frpPort);
|
|
3520
4486
|
frpTokenLabel.append(frpToken);
|
|
3521
4487
|
frpOriginLabel.append(frpOrigin);
|
|
3522
|
-
frpFields.append(frpServerLabel, frpPortLabel, frpTokenLabel, frpOriginLabel);
|
|
4488
|
+
frpFields.append(frpServerLabel, frpPortLabel, frpTokenLabel, frpOriginLabel, frpModeLabel, frpEntryTlsLabel, frpVhostLabel, frpPublicPortLabel);
|
|
3523
4489
|
frpStep1.append(frpStep1Title, frpStep1Text, frpFields);
|
|
3524
4490
|
const frpStep2 = element("section", "dsh-mobile-control__frp-step");
|
|
3525
4491
|
const frpStep2Title = element("strong");
|
|
@@ -3529,6 +4495,21 @@ window.__ModuleLoader__.load({
|
|
|
3529
4495
|
const frpCopyTemplate = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
|
|
3530
4496
|
frpCopyTemplate.type = "button";
|
|
3531
4497
|
frpCopyTemplate.textContent = t("copyServerTemplate");
|
|
4498
|
+
const frpCopyAttachPlan = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
|
|
4499
|
+
frpCopyAttachPlan.type = "button";
|
|
4500
|
+
frpCopyAttachPlan.textContent = t("frpAttachPlan");
|
|
4501
|
+
frpCopyAttachPlan.hidden = true;
|
|
4502
|
+
const frpCopyAttachToken = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
|
|
4503
|
+
frpCopyAttachToken.type = "button";
|
|
4504
|
+
frpCopyAttachToken.textContent = t("frpAttachToken");
|
|
4505
|
+
frpCopyAttachToken.hidden = true;
|
|
4506
|
+
const frpSelfCheckButton = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
|
|
4507
|
+
frpSelfCheckButton.type = "button";
|
|
4508
|
+
frpSelfCheckButton.textContent = t("frpAttachSelfCheck");
|
|
4509
|
+
frpSelfCheckButton.hidden = true;
|
|
4510
|
+
const frpSelfCheckStatus = element("p", "dsh-mobile-control__component-status");
|
|
4511
|
+
frpSelfCheckStatus.textContent = "";
|
|
4512
|
+
frpSelfCheckStatus.hidden = true;
|
|
3532
4513
|
const vpsDeployText = element("p");
|
|
3533
4514
|
vpsDeployText.textContent = t("vpsDeployText");
|
|
3534
4515
|
const vpsChangesTitle = element("p");
|
|
@@ -3582,7 +4563,7 @@ window.__ModuleLoader__.load({
|
|
|
3582
4563
|
const vpsUninstall = element("button", "dsh-mobile-control__danger dsh-mobile-control__frp-action");
|
|
3583
4564
|
vpsUninstall.type = "button";
|
|
3584
4565
|
vpsUninstall.textContent = t("vpsUninstall");
|
|
3585
|
-
frpStep2.append(frpStep2Title, frpStep2Text, frpCopyTemplate, vpsDeployText, vpsChangesTitle, vpsChanges, vpsDeployFields, vpsDeploy, vpsDeployStatus, vpsCopyUninstall, vpsUninstall);
|
|
4566
|
+
frpStep2.append(frpStep2Title, frpStep2Text, frpCopyTemplate, frpCopyAttachPlan, frpCopyAttachToken, frpSelfCheckButton, frpSelfCheckStatus, vpsDeployText, vpsChangesTitle, vpsChanges, vpsDeployFields, vpsDeploy, vpsDeployStatus, vpsCopyUninstall, vpsUninstall);
|
|
3586
4567
|
const frpStep3 = element("section", "dsh-mobile-control__frp-step");
|
|
3587
4568
|
const frpStep3Title = element("strong");
|
|
3588
4569
|
frpStep3Title.textContent = t("frpStep3Title");
|
|
@@ -4099,6 +5080,14 @@ window.__ModuleLoader__.load({
|
|
|
4099
5080
|
let configuredFrpServer = "";
|
|
4100
5081
|
let configuredFrpPort = 7e3;
|
|
4101
5082
|
let configuredFrpOrigin = "";
|
|
5083
|
+
let configuredFrpVhostPort = 7080;
|
|
5084
|
+
let configuredFrpPublicPort = 33080;
|
|
5085
|
+
let frpConfiguredMode = "deploy";
|
|
5086
|
+
let frpConfiguredEntryTls = "public-ip-cert";
|
|
5087
|
+
let frpModeDraft = false;
|
|
5088
|
+
let frpEntryTlsDraft = false;
|
|
5089
|
+
let frpVhostDraft = false;
|
|
5090
|
+
let frpAttachVhostExplicit = false;
|
|
4102
5091
|
let providerInfoPinned = false;
|
|
4103
5092
|
let providerInfoHovered = false;
|
|
4104
5093
|
let previousAccessView = "lan";
|
|
@@ -4513,6 +5502,48 @@ window.__ModuleLoader__.load({
|
|
|
4513
5502
|
if (frpOrigin.value === "" && configuredFrpOrigin !== "") frpOrigin.value = configuredFrpOrigin;
|
|
4514
5503
|
frpStorage.textContent = frpStoragePath;
|
|
4515
5504
|
frpStorage.title = frpStoragePath;
|
|
5505
|
+
const configuredFrpMode = frpConfiguration.mode === "attach" ? "attach" : "deploy";
|
|
5506
|
+
const configuredFrpEntryTls = frpConfiguration.entryTls === "self-signed" ? "self-signed" : "public-ip-cert";
|
|
5507
|
+
if (frpConfigured) {
|
|
5508
|
+
frpConfiguredMode = configuredFrpMode;
|
|
5509
|
+
frpConfiguredEntryTls = configuredFrpEntryTls;
|
|
5510
|
+
}
|
|
5511
|
+
if (frpConfigured && !frpModeDraft) frpMode.value = configuredFrpMode;
|
|
5512
|
+
if (frpConfigured && !frpEntryTlsDraft) frpEntryTls.value = configuredFrpEntryTls;
|
|
5513
|
+
if (typeof frpConfiguration.vhostHttpPort === "number") configuredFrpVhostPort = frpConfiguration.vhostHttpPort;
|
|
5514
|
+
if (typeof frpConfiguration.publicPort === "number") configuredFrpPublicPort = frpConfiguration.publicPort;
|
|
5515
|
+
if (!frpVhostDraft) {
|
|
5516
|
+
if (frpMode.value === "attach" && configuredFrpMode === "attach" && configuredFrpEntryTls === "public-ip-cert") {
|
|
5517
|
+
frpVhostPort.value = String(configuredFrpVhostPort);
|
|
5518
|
+
frpAttachVhostExplicit = true;
|
|
5519
|
+
} else frpVhostPort.value = frpMode.value === "deploy" ? String(configuredFrpVhostPort) : "";
|
|
5520
|
+
}
|
|
5521
|
+
if (frpPublicPort.value === "" || frpPublicPort.value === "33080") frpPublicPort.value = String(configuredFrpPublicPort);
|
|
5522
|
+
const attachSelected = frpMode.value === "attach";
|
|
5523
|
+
const selfSignedSelected = attachSelected && frpEntryTls.value === "self-signed";
|
|
5524
|
+
frpStep2Title.textContent = t(attachSelected ? "frpStep2TitleAttach" : "frpStep2Title");
|
|
5525
|
+
frpStep2Text.textContent = t(attachSelected ? "frpStep2TextAttach" : "frpStep2Text");
|
|
5526
|
+
frpStep4Text.textContent = t(attachSelected ? "frpStep4TextAttach" : "frpStep4Text");
|
|
5527
|
+
frpAppRequirement.textContent = t(selfSignedSelected ? "frpAppRequirementSelfSigned" : "frpAppRequirement");
|
|
5528
|
+
frpCopyTemplate.hidden = attachSelected;
|
|
5529
|
+
frpCopyAttachPlan.hidden = !attachSelected;
|
|
5530
|
+
frpCopyAttachToken.hidden = !attachSelected;
|
|
5531
|
+
frpSelfCheckButton.hidden = !frpConfigured;
|
|
5532
|
+
frpVhostPort.disabled = selfSignedSelected;
|
|
5533
|
+
frpVhostLabel.hidden = selfSignedSelected;
|
|
5534
|
+
frpVhostPort.required = attachSelected && !selfSignedSelected;
|
|
5535
|
+
frpVhostHint.textContent = frpVhostPort.required ? t("frpAttachModeRequiresVhostPort") : t("frpVhostHttpPortHint");
|
|
5536
|
+
frpEntryTls.disabled = !attachSelected;
|
|
5537
|
+
frpEntryTlsLabel.hidden = !attachSelected;
|
|
5538
|
+
frpPublicPortLabel.hidden = !selfSignedSelected;
|
|
5539
|
+
vpsDeployText.hidden = attachSelected;
|
|
5540
|
+
vpsChangesTitle.hidden = attachSelected;
|
|
5541
|
+
vpsChanges.hidden = attachSelected;
|
|
5542
|
+
vpsDeployFields.hidden = attachSelected;
|
|
5543
|
+
vpsDeploy.hidden = attachSelected;
|
|
5544
|
+
vpsDeployStatus.hidden = attachSelected;
|
|
5545
|
+
vpsCopyUninstall.hidden = attachSelected;
|
|
5546
|
+
vpsUninstall.hidden = attachSelected;
|
|
4516
5547
|
frpInstall.hidden = frpInstalled || !frpSupported;
|
|
4517
5548
|
frpInstall.textContent = frpDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(frpDownloadBytes) }) : t("installFrpc");
|
|
4518
5549
|
frpInstall.disabled = remoteProviderBusy;
|
|
@@ -4522,7 +5553,7 @@ window.__ModuleLoader__.load({
|
|
|
4522
5553
|
vpsUninstall.disabled = vpsDeploy.disabled;
|
|
4523
5554
|
frpPurge.hidden = !frpInstalled && !frpConfigured;
|
|
4524
5555
|
frpComponentStatus.textContent = !frpSupported ? t("frpUnsupported") : frpInstalled ? t("frpComponentReady", { version: frpVersion }) : t("frpNotInstalled");
|
|
4525
|
-
frpConfigurationStatus.textContent = frpConfigured ? t("frpConfigurationReady") : t("frpConfigurationMissing");
|
|
5556
|
+
frpConfigurationStatus.textContent = frpConfigured ? t("frpConfigurationReady") : t(attachSelected ? "frpConfigurationMissingAttach" : "frpConfigurationMissing");
|
|
4526
5557
|
frpOverview.hidden = !frpConfigured;
|
|
4527
5558
|
frpOverviewEndpoint.textContent = configuredFrpOrigin === "" ? configuredFrpServer : `${configuredFrpOrigin} · ${configuredFrpServer}:${String(configuredFrpPort)}`;
|
|
4528
5559
|
frpToken.placeholder = frpConfigured ? locale === "zh" ? "已安全保存;留空保持不变" : locale === "it" ? "Salvato; lascia vuoto per mantenerlo" : "Saved securely; leave blank to keep it" : t("frpTokenPlaceholder");
|
|
@@ -4623,6 +5654,36 @@ window.__ModuleLoader__.load({
|
|
|
4623
5654
|
}
|
|
4624
5655
|
if (!needsFunnelSetup) remoteSetupPending = false;
|
|
4625
5656
|
};
|
|
5657
|
+
frpMode.addEventListener("change", () => {
|
|
5658
|
+
frpModeDraft = true;
|
|
5659
|
+
if (frpMode.value === "attach") {
|
|
5660
|
+
if (!frpAttachVhostExplicit) {
|
|
5661
|
+
frpVhostPort.value = "";
|
|
5662
|
+
frpVhostDraft = true;
|
|
5663
|
+
}
|
|
5664
|
+
} else {
|
|
5665
|
+
frpEntryTls.value = "public-ip-cert";
|
|
5666
|
+
frpEntryTlsDraft = true;
|
|
5667
|
+
if (frpVhostPort.value === "") frpVhostPort.value = String(configuredFrpVhostPort);
|
|
5668
|
+
frpVhostDraft = true;
|
|
5669
|
+
}
|
|
5670
|
+
frpVhostPort.removeAttribute("aria-invalid");
|
|
5671
|
+
renderRemote(latestRemoteState);
|
|
5672
|
+
});
|
|
5673
|
+
frpEntryTls.addEventListener("change", () => {
|
|
5674
|
+
frpEntryTlsDraft = true;
|
|
5675
|
+
if (frpMode.value === "attach" && frpEntryTls.value === "public-ip-cert" && !frpAttachVhostExplicit) {
|
|
5676
|
+
frpVhostPort.value = "";
|
|
5677
|
+
frpVhostDraft = true;
|
|
5678
|
+
}
|
|
5679
|
+
frpVhostPort.removeAttribute("aria-invalid");
|
|
5680
|
+
renderRemote(latestRemoteState);
|
|
5681
|
+
});
|
|
5682
|
+
frpVhostPort.addEventListener("input", () => {
|
|
5683
|
+
frpVhostDraft = true;
|
|
5684
|
+
if (frpMode.value === "attach") frpAttachVhostExplicit = frpVhostPort.value.trim() !== "";
|
|
5685
|
+
frpVhostPort.removeAttribute("aria-invalid");
|
|
5686
|
+
});
|
|
4626
5687
|
let remoteLoadInFlight = false;
|
|
4627
5688
|
const loadRemote = () => {
|
|
4628
5689
|
if (remoteLoadInFlight || originFormBusy) return;
|
|
@@ -4885,11 +5946,33 @@ window.__ModuleLoader__.load({
|
|
|
4885
5946
|
});
|
|
4886
5947
|
});
|
|
4887
5948
|
const validFrpServer = (value) => value === value.trim() && value.length > 0 && value.length <= 253 && !/[\s\u0000-\u001f\u007f/\\@?#]/u.test(value);
|
|
4888
|
-
const frpForm = () =>
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
5949
|
+
const frpForm = () => {
|
|
5950
|
+
const vhostHttpPort = Number(frpVhostPort.value);
|
|
5951
|
+
const publicPort = Number(frpPublicPort.value);
|
|
5952
|
+
const mode = frpMode.value === "attach" ? "attach" : "deploy";
|
|
5953
|
+
return {
|
|
5954
|
+
serverAddress: String(frpServer.value ?? "").trim(),
|
|
5955
|
+
serverPort: Number(frpPort.value),
|
|
5956
|
+
token: String(frpToken.value ?? ""),
|
|
5957
|
+
publicOrigin: String(frpOrigin.value ?? "").trim(),
|
|
5958
|
+
mode,
|
|
5959
|
+
entryTls: frpEntryTls.value === "self-signed" ? "self-signed" : "public-ip-cert",
|
|
5960
|
+
...Number.isSafeInteger(vhostHttpPort) && vhostHttpPort >= 1 ? { vhostHttpPort } : {},
|
|
5961
|
+
...Number.isSafeInteger(publicPort) && publicPort >= 1 ? { publicPort } : {}
|
|
5962
|
+
};
|
|
5963
|
+
};
|
|
5964
|
+
/**
|
|
5965
|
+
* Attach-mode artefacts are built locally, so the token never reaches the host
|
|
5966
|
+
* API and the runbook can be produced before anything is saved.
|
|
5967
|
+
*/
|
|
5968
|
+
const attachClipboardForm = (form) => ({
|
|
5969
|
+
serverAddress: form.serverAddress,
|
|
5970
|
+
serverPort: form.serverPort,
|
|
5971
|
+
token: form.token === "" && frpConfigured ? "0123456789abcdef" : form.token,
|
|
5972
|
+
publicOrigin: form.publicOrigin,
|
|
5973
|
+
entryTls: form.entryTls,
|
|
5974
|
+
...form.entryTls === "self-signed" || form.vhostHttpPort === void 0 ? {} : { vhostHttpPort: form.vhostHttpPort },
|
|
5975
|
+
...form.publicPort === void 0 ? {} : { publicPort: form.publicPort }
|
|
4893
5976
|
});
|
|
4894
5977
|
const validFrpForm = (form) => {
|
|
4895
5978
|
if (!validFrpServer(form.serverAddress)) return false;
|
|
@@ -4912,6 +5995,127 @@ window.__ModuleLoader__.load({
|
|
|
4912
5995
|
remoteStatus.textContent = t("templateCopyFailed");
|
|
4913
5996
|
});
|
|
4914
5997
|
});
|
|
5998
|
+
const frpAttachErrorText = (code) => t(REMOTE_ERROR_MESSAGE_KEYS[code] ?? "frpInputInvalid");
|
|
5999
|
+
const showFrpAttachError = (code) => {
|
|
6000
|
+
const message = frpAttachErrorText(code);
|
|
6001
|
+
if (code === "frp_attach_mode_requires_vhost_port") {
|
|
6002
|
+
frpVhostPort.setAttribute("aria-invalid", "true");
|
|
6003
|
+
frpVhostHint.textContent = message;
|
|
6004
|
+
frpVhostPort.focus();
|
|
6005
|
+
}
|
|
6006
|
+
remoteStatus.textContent = message;
|
|
6007
|
+
};
|
|
6008
|
+
frpCopyAttachPlan.addEventListener("click", () => {
|
|
6009
|
+
const form = frpForm();
|
|
6010
|
+
if (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
|
|
6011
|
+
remoteStatus.textContent = t("frpInputInvalid");
|
|
6012
|
+
return;
|
|
6013
|
+
}
|
|
6014
|
+
if (form.token !== "") {
|
|
6015
|
+
const attachForm = attachClipboardForm(form);
|
|
6016
|
+
const code = frpAttachFormErrorCode(attachForm);
|
|
6017
|
+
if (code !== void 0) {
|
|
6018
|
+
showFrpAttachError(code);
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
navigator.clipboard.writeText(createFrpAttachTemplateForClipboard(attachForm)).then(() => {
|
|
6022
|
+
remoteStatus.textContent = t("frpAttachPlanCopied");
|
|
6023
|
+
}, () => {
|
|
6024
|
+
remoteStatus.textContent = t("frpAttachPlanFailed", { error: t("templateCopyFailed") });
|
|
6025
|
+
});
|
|
6026
|
+
return;
|
|
6027
|
+
}
|
|
6028
|
+
if (!frpConfigured) {
|
|
6029
|
+
remoteStatus.textContent = t("frpInputInvalid");
|
|
6030
|
+
return;
|
|
6031
|
+
}
|
|
6032
|
+
remoteProviderBusy = true;
|
|
6033
|
+
frpCopyAttachPlan.disabled = true;
|
|
6034
|
+
controlRequestJson("/api/mobile-access/remote/frp/attach-plan", {
|
|
6035
|
+
method: "POST",
|
|
6036
|
+
body: JSON.stringify(form)
|
|
6037
|
+
}).then((data) => {
|
|
6038
|
+
const text = typeof data.frpAttachTemplate === "string" ? data.frpAttachTemplate : "";
|
|
6039
|
+
if (text === "") throw new Error("frp_settings_invalid");
|
|
6040
|
+
return navigator.clipboard.writeText(text).then(() => {
|
|
6041
|
+
remoteStatus.textContent = t("frpAttachPlanCopied");
|
|
6042
|
+
});
|
|
6043
|
+
}, (error) => {
|
|
6044
|
+
remoteStatus.textContent = frpAttachErrorText(String(error));
|
|
6045
|
+
}).catch((error) => {
|
|
6046
|
+
remoteStatus.textContent = t("frpAttachPlanFailed", { error: String(error) });
|
|
6047
|
+
}).finally(() => {
|
|
6048
|
+
remoteProviderBusy = false;
|
|
6049
|
+
frpCopyAttachPlan.disabled = false;
|
|
6050
|
+
});
|
|
6051
|
+
});
|
|
6052
|
+
/** Copy a token-bearing frpc.toml only from the token the user just typed. */
|
|
6053
|
+
frpCopyAttachToken.addEventListener("click", () => {
|
|
6054
|
+
const form = frpForm();
|
|
6055
|
+
if (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
|
|
6056
|
+
remoteStatus.textContent = t("frpInputInvalid");
|
|
6057
|
+
return;
|
|
6058
|
+
}
|
|
6059
|
+
if (form.token !== "") {
|
|
6060
|
+
const attachForm = attachClipboardForm(form);
|
|
6061
|
+
const code = frpAttachFormErrorCode(attachForm);
|
|
6062
|
+
if (code !== void 0) {
|
|
6063
|
+
showFrpAttachError(code);
|
|
6064
|
+
return;
|
|
6065
|
+
}
|
|
6066
|
+
navigator.clipboard.writeText(createFrpAttachFrpcTomlForClipboard(attachForm, { revealToken: true })).then(() => {
|
|
6067
|
+
remoteStatus.textContent = t("frpAttachTokenCopied");
|
|
6068
|
+
}, () => {
|
|
6069
|
+
remoteStatus.textContent = t("frpAttachTokenFailed", { error: t("templateCopyFailed") });
|
|
6070
|
+
});
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
frpToken.focus();
|
|
6074
|
+
remoteStatus.textContent = t("frpAttachTokenReenter");
|
|
6075
|
+
});
|
|
6076
|
+
const certificateStatusText = (status) => {
|
|
6077
|
+
if (status === void 0) return "";
|
|
6078
|
+
const state = typeof status.state === "string" ? status.state : "unknown";
|
|
6079
|
+
const days = typeof status.daysRemaining === "number" ? String(status.daysRemaining) : "0";
|
|
6080
|
+
if (state === "ok") return t("frpAttachCertOk", { days });
|
|
6081
|
+
if (state === "expiring") return t("frpAttachCertExpiring", { days });
|
|
6082
|
+
if (state === "expired") return t("frpAttachCertExpired");
|
|
6083
|
+
return t("frpAttachCertUnknown");
|
|
6084
|
+
};
|
|
6085
|
+
const caStatusText = (status) => {
|
|
6086
|
+
if (status === void 0) return "";
|
|
6087
|
+
const state = typeof status.state === "string" ? status.state : "unknown";
|
|
6088
|
+
const days = typeof status.daysRemaining === "number" ? String(status.daysRemaining) : "0";
|
|
6089
|
+
if (state === "ok") return t("frpAttachCaOk", { days });
|
|
6090
|
+
if (state === "expiring") return t("frpAttachCaExpiring", { days });
|
|
6091
|
+
if (state === "expired") return t("frpIngressCaExpired");
|
|
6092
|
+
return t("frpIngressCaInvalid");
|
|
6093
|
+
};
|
|
6094
|
+
frpSelfCheckButton.addEventListener("click", () => {
|
|
6095
|
+
if (remoteProviderBusy) return;
|
|
6096
|
+
remoteProviderBusy = true;
|
|
6097
|
+
frpSelfCheckButton.disabled = true;
|
|
6098
|
+
frpSelfCheckStatus.hidden = false;
|
|
6099
|
+
frpSelfCheckStatus.textContent = t("loadingStatus");
|
|
6100
|
+
controlRequestJson("/api/mobile-access/remote/frp/self-check").then((data) => {
|
|
6101
|
+
const check = data.frpSelfCheck !== null && typeof data.frpSelfCheck === "object" ? data.frpSelfCheck : {};
|
|
6102
|
+
const lines = [];
|
|
6103
|
+
const caCertificate = check.caCertificate !== null && typeof check.caCertificate === "object" ? check.caCertificate : void 0;
|
|
6104
|
+
const caText = caStatusText(caCertificate);
|
|
6105
|
+
if (caText !== "") lines.push(caText);
|
|
6106
|
+
const certificate = check.certificate !== null && typeof check.certificate === "object" ? check.certificate : void 0;
|
|
6107
|
+
const certificateText = certificateStatusText(certificate);
|
|
6108
|
+
if (certificateText !== "") lines.push(certificateText);
|
|
6109
|
+
lines.push(check.frpsReachable === true ? t("frpAttachFrpsReachable") : t("frpAttachFrpsUnreachable"));
|
|
6110
|
+
if (check.entryTls === "self-signed") lines.push(check.entryReachable === true ? t("frpAttachEntryReachable") : t("frpAttachEntryUnreachable"));
|
|
6111
|
+
frpSelfCheckStatus.textContent = lines.join(" · ");
|
|
6112
|
+
}, (error) => {
|
|
6113
|
+
frpSelfCheckStatus.textContent = t("requestFailed", { error: String(error) });
|
|
6114
|
+
}).finally(() => {
|
|
6115
|
+
remoteProviderBusy = false;
|
|
6116
|
+
frpSelfCheckButton.disabled = false;
|
|
6117
|
+
});
|
|
6118
|
+
});
|
|
4915
6119
|
const validVpsSshUser = (value) => /^[a-z_][a-z0-9_.-]*[$]?$/iu.test(value) && value.length <= 64;
|
|
4916
6120
|
const validVpsSshKey = (value) => value === "" || /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith("/");
|
|
4917
6121
|
const vpsFormStorageKey = "dsh-mobile.frp-vps-form.v1";
|
|
@@ -4947,7 +6151,11 @@ window.__ModuleLoader__.load({
|
|
|
4947
6151
|
serverAddress: form.serverAddress === "" ? configuredFrpServer : form.serverAddress,
|
|
4948
6152
|
serverPort: Number.isSafeInteger(form.serverPort) && form.serverPort >= 1 ? form.serverPort : configuredFrpPort,
|
|
4949
6153
|
token: form.token,
|
|
4950
|
-
publicOrigin: form.publicOrigin === "" ? configuredFrpOrigin : form.publicOrigin
|
|
6154
|
+
publicOrigin: form.publicOrigin === "" ? configuredFrpOrigin : form.publicOrigin,
|
|
6155
|
+
mode: form.mode,
|
|
6156
|
+
entryTls: form.entryTls,
|
|
6157
|
+
...form.vhostHttpPort === void 0 ? {} : { vhostHttpPort: form.vhostHttpPort },
|
|
6158
|
+
...form.publicPort === void 0 ? {} : { publicPort: form.publicPort }
|
|
4951
6159
|
};
|
|
4952
6160
|
};
|
|
4953
6161
|
const validVpsFrpForm = () => {
|
|
@@ -5187,10 +6395,14 @@ window.__ModuleLoader__.load({
|
|
|
5187
6395
|
frpConfigure.addEventListener("click", () => {
|
|
5188
6396
|
if (remoteProviderBusy || !frpInstalled) return;
|
|
5189
6397
|
const form = frpForm();
|
|
5190
|
-
const unchanged = frpConfigured && form.token === "" && form.serverAddress === configuredFrpServer && form.serverPort === configuredFrpPort && form.publicOrigin === configuredFrpOrigin;
|
|
5191
|
-
if (!unchanged
|
|
5192
|
-
|
|
5193
|
-
|
|
6398
|
+
const unchanged = frpConfigured && form.token === "" && form.serverAddress === configuredFrpServer && form.serverPort === configuredFrpPort && form.publicOrigin === configuredFrpOrigin && form.mode === (frpConfiguredMode === "attach" ? "attach" : "deploy") && form.entryTls === (frpConfiguredEntryTls === "self-signed" ? "self-signed" : "public-ip-cert") && (form.vhostHttpPort ?? configuredFrpVhostPort) === configuredFrpVhostPort && (form.publicPort ?? configuredFrpPublicPort) === configuredFrpPublicPort;
|
|
6399
|
+
if (!unchanged) {
|
|
6400
|
+
const code = form.mode === "attach" ? frpAttachFormErrorCode(attachClipboardForm(form)) : validFrpForm(form) ? void 0 : "frp_settings_invalid";
|
|
6401
|
+
if (code !== void 0) {
|
|
6402
|
+
if (form.mode === "attach") showFrpAttachError(code);
|
|
6403
|
+
else remoteStatus.textContent = t("frpInputInvalid");
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
5194
6406
|
}
|
|
5195
6407
|
remoteProviderBusy = true;
|
|
5196
6408
|
frpConfigure.disabled = true;
|
|
@@ -5217,7 +6429,7 @@ window.__ModuleLoader__.load({
|
|
|
5217
6429
|
});
|
|
5218
6430
|
});
|
|
5219
6431
|
frpPurge.addEventListener("click", () => {
|
|
5220
|
-
if (remoteProviderBusy || !window.confirm(t("purgeFrpConfirm"))) return;
|
|
6432
|
+
if (remoteProviderBusy || !window.confirm(t(frpConfiguredMode === "attach" && frpConfiguredEntryTls === "self-signed" ? "purgeFrpConfirmSelfSigned" : "purgeFrpConfirm"))) return;
|
|
5221
6433
|
remoteProviderBusy = true;
|
|
5222
6434
|
frpPurge.disabled = true;
|
|
5223
6435
|
remoteStatus.textContent = t("purgingFrp");
|
|
@@ -5518,9 +6730,10 @@ window.__ModuleLoader__.load({
|
|
|
5518
6730
|
const localizedEntryCopy = (entry) => {
|
|
5519
6731
|
const serverCopy = diagnosticServerCopy(entry);
|
|
5520
6732
|
const reason = typeof entry.reason === "string" ? entry.reason : "";
|
|
5521
|
-
const templates = DIAGNOSTIC_REASON_MESSAGES[locale][reason];
|
|
5522
|
-
if (templates === void 0) return serverCopy;
|
|
5523
6733
|
const facts = entry.facts !== null && typeof entry.facts === "object" ? entry.facts : {};
|
|
6734
|
+
const copyReason = facts.viaProxy === true && (reason === "remote-ready" || reason === "remote-rate-limited") ? `${reason}-via-proxy` : reason;
|
|
6735
|
+
const templates = DIAGNOSTIC_REASON_MESSAGES[locale][copyReason];
|
|
6736
|
+
if (templates === void 0) return serverCopy;
|
|
5524
6737
|
const providerNames = {
|
|
5525
6738
|
tailscale: "Tailscale Funnel",
|
|
5526
6739
|
cpolar: "cpolar",
|
|
@@ -5548,58 +6761,7 @@ window.__ModuleLoader__.load({
|
|
|
5548
6761
|
versions.minimumAndroidApp
|
|
5549
6762
|
].every((value) => typeof value === "string")) detail += ` plugin ${String(versions.plugin)}, DSH ${String(versions.dsh)}, Android ${String(versions.minimumAndroidApp)}.`;
|
|
5550
6763
|
}
|
|
5551
|
-
if (reason === "remote-controller-error")
|
|
5552
|
-
const actionKey = {
|
|
5553
|
-
...ORIGIN_ERROR_MESSAGE_KEYS,
|
|
5554
|
-
component_missing: "remoteUnavailableTailscale",
|
|
5555
|
-
funnel_permission_required: "funnelPermission",
|
|
5556
|
-
funnel_https_required: "funnelHttps",
|
|
5557
|
-
funnel_start_failed: "funnelStart",
|
|
5558
|
-
funnel_start_timeout: "funnelTimeout",
|
|
5559
|
-
tailscale_dns_missing: "tailscaleDnsMissing",
|
|
5560
|
-
sidecar_launch_failed: "remoteUnavailableTailscale",
|
|
5561
|
-
sidecar_stopped: "controlChannelFailed",
|
|
5562
|
-
sidecar_exited: "controlChannelFailed",
|
|
5563
|
-
control_channel_failed: "controlChannelFailed",
|
|
5564
|
-
cpolar_component_missing: "cpolarMissing",
|
|
5565
|
-
cpolar_component_invalid: "cpolarInvalid",
|
|
5566
|
-
cpolar_config_missing: "cpolarConfigMissing",
|
|
5567
|
-
cpolar_config_invalid: "cpolarConfigInvalid",
|
|
5568
|
-
cpolar_start_timeout: "cpolarTimeout",
|
|
5569
|
-
cpolar_stopped: "cpolarStopped",
|
|
5570
|
-
cpolar_exited: "cpolarExited",
|
|
5571
|
-
cloudflared_component_missing: "cloudflaredMissing",
|
|
5572
|
-
cloudflared_component_invalid: "cloudflaredInvalid",
|
|
5573
|
-
cloudflared_component_unsupported: "cloudflaredComponentUnsupported",
|
|
5574
|
-
cloudflared_port_unavailable: "cloudflaredPortUnavailable",
|
|
5575
|
-
cloudflared_port_reservation_failed: "cloudflaredPortReservationFailed",
|
|
5576
|
-
cloudflared_launch_failed: "cloudflaredLaunchFailed",
|
|
5577
|
-
cloudflared_start_timeout: "cloudflaredTimeout",
|
|
5578
|
-
cloudflared_stopped: "cloudflaredStopped",
|
|
5579
|
-
cloudflared_exited: "cloudflaredExited",
|
|
5580
|
-
cloudflared_invalid_origin: "cloudflaredOriginInvalid",
|
|
5581
|
-
cloudflared_tunnel_port_unavailable: "cloudflaredTunnelPortUnavailable",
|
|
5582
|
-
cloudflared_tunnel_config_invalid: "cloudflaredTunnelInvalid",
|
|
5583
|
-
cloudflared_tunnel_target_invalid: "cloudflaredTunnelTargetInvalid",
|
|
5584
|
-
cloudflared_download_hash_mismatch: "cloudflaredDownloadHashMismatch",
|
|
5585
|
-
cloudflared_download_size_mismatch: "cloudflaredDownloadSizeMismatch",
|
|
5586
|
-
cloudflared_executable_hash_mismatch: "cloudflaredExecutableHashMismatch",
|
|
5587
|
-
frp_component_missing: "frpMissing",
|
|
5588
|
-
frp_component_invalid: "frpInvalid",
|
|
5589
|
-
frp_config_missing: "frpConfigMissing",
|
|
5590
|
-
frp_config_verify_failed: "frpConfigVerifyFailed",
|
|
5591
|
-
frp_vhost_publicly_reachable: "frpVhostPublic",
|
|
5592
|
-
frp_vhost_probe_failed: "frpVhostProbeFailed",
|
|
5593
|
-
frp_launch_failed: "frpLaunchFailed",
|
|
5594
|
-
frp_start_timeout: "frpTimeout",
|
|
5595
|
-
frp_discovery_mismatch: "frpDiscoveryMismatch",
|
|
5596
|
-
frp_discovery_invalid: "frpDiscoveryInvalid",
|
|
5597
|
-
frp_stopped: "frpStopped",
|
|
5598
|
-
frp_exited: "frpExited",
|
|
5599
|
-
gateway_start_failed: "gatewayStartFailed"
|
|
5600
|
-
}[values.controllerCode ?? ""];
|
|
5601
|
-
if (actionKey !== void 0) action = t(actionKey);
|
|
5602
|
-
}
|
|
6764
|
+
if (reason === "remote-controller-error") action = diagnosticControllerAction(facts.controllerCode, locale, action);
|
|
5603
6765
|
return {
|
|
5604
6766
|
detail,
|
|
5605
6767
|
action
|
|
@@ -7033,7 +8195,7 @@ window.__ModuleLoader__.load({
|
|
|
7033
8195
|
style rule of its own. */
|
|
7034
8196
|
.dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:var(--dsw-alias-label-primary-bluish,#2563eb);font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:36px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#475569);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:44px;margin-top:3px;padding:7px 10px;border:1px solid var(--dsw-alias-state-error-primary,#dc2626);border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#dc2626);font:12px/1.3 system-ui;cursor:pointer}
|
|
7035
8197
|
.dsh-mobile-control__danger:hover:not(:disabled),.dsh-mobile-control__device-revoke:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgb(220 38 38 / 8%))}
|
|
7036
|
-
.dsh-mobile-control__frp-setup{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-setup[hidden]{display:none}.dsh-mobile-control__frp-step{padding:11px 0}.dsh-mobile-control__frp-step + .dsh-mobile-control__frp-step{border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}.dsh-mobile-control__frp-step>strong{display:block;margin-bottom:3px;font-size:12px;line-height:1.4}.dsh-mobile-control__frp-step>p{margin:0 0 9px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes{margin:0 0 9px;padding:0;list-style:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes li{position:relative;margin:0;padding-left:12px}.dsh-mobile-control__frp-changes li+li{margin-top:5px}.dsh-mobile-control__frp-changes li::before{content:"•";position:absolute;left:2px;color:var(--dsw-alias-label-tertiary,#98a1ad)}.dsh-mobile-control__frp-step>.dsh-mobile-control__frp-requirement{padding:8px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:11px}.dsh-mobile-control__frp-fields{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px}.dsh-mobile-control__field{display:flex;min-width:0;flex-direction:column;gap:5px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__field:nth-child(3),.dsh-mobile-control__field:nth-child(4){grid-column:1/-1}.dsh-mobile-control__field input{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__frp-action{box-sizing:border-box;width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__frp-action:disabled{cursor:not-allowed;opacity:.5}.dsh-mobile-control__remote-workspace{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f7f8fa)}.dsh-mobile-control__stage-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.dsh-mobile-control__stage-header .dsh-mobile-control__section-title{margin:0}.dsh-mobile-control__stage-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:5px}.dsh-mobile-control__stage-value{max-width:115px;overflow:hidden;color:var(--dsw-alias-label-primary,#16181d);font:650 11px/1.3 system-ui;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__state-badge{flex:none;padding:3px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#606873);font:650 11px/1.25 system-ui}.dsh-mobile-control__state-badge.is-ready{background:var(--dsw-alias-state-success-tertiary,#e6f7f0);color:var(--dsw-alias-label-primary,#087454)}.dsh-mobile-control__state-badge.is-busy{background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8)}.dsh-mobile-control__state-badge.is-attention{background:var(--dsw-alias-state-warn-tertiary,#fff4dc);color:var(--dsw-alias-label-primary,#935100)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__status{box-sizing:border-box;margin:0 0 10px;padding:9px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-setup-body{margin:0 0 10px}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup{margin:0}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__section-title,.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__frp-setup>.dsh-mobile-control__section-title{display:none}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__details{margin:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{margin-top:2px}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin:10px 0 0}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__manage-row{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
8198
|
+
.dsh-mobile-control__frp-setup{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-setup[hidden]{display:none}.dsh-mobile-control__frp-step{padding:11px 0}.dsh-mobile-control__frp-step + .dsh-mobile-control__frp-step{border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}.dsh-mobile-control__frp-step>strong{display:block;margin-bottom:3px;font-size:12px;line-height:1.4}.dsh-mobile-control__frp-step>p{margin:0 0 9px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes{margin:0 0 9px;padding:0;list-style:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes li{position:relative;margin:0;padding-left:12px}.dsh-mobile-control__frp-changes li+li{margin-top:5px}.dsh-mobile-control__frp-changes li::before{content:"•";position:absolute;left:2px;color:var(--dsw-alias-label-tertiary,#98a1ad)}.dsh-mobile-control__frp-step>.dsh-mobile-control__frp-requirement{padding:8px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:11px}.dsh-mobile-control__frp-fields{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px}.dsh-mobile-control__frp-fields>.dsh-mobile-control__field:nth-child(5),.dsh-mobile-control__frp-fields>.dsh-mobile-control__field:nth-child(6){grid-column:1/-1}.dsh-mobile-control__frp-fields>.dsh-mobile-control__field[hidden]{display:none}.dsh-mobile-control__frp-fields select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 32px 9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:14px/1.4 system-ui}.dsh-mobile-control__frp-fields select:disabled{cursor:not-allowed;opacity:.55}.dsh-mobile-control__frp-hint{margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__field{display:flex;min-width:0;flex-direction:column;gap:5px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__field:nth-child(3),.dsh-mobile-control__field:nth-child(4){grid-column:1/-1}.dsh-mobile-control__field input{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__frp-action{box-sizing:border-box;width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__frp-action:disabled{cursor:not-allowed;opacity:.5}.dsh-mobile-control__remote-workspace{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f7f8fa)}.dsh-mobile-control__stage-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.dsh-mobile-control__stage-header .dsh-mobile-control__section-title{margin:0}.dsh-mobile-control__stage-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:5px}.dsh-mobile-control__stage-value{max-width:115px;overflow:hidden;color:var(--dsw-alias-label-primary,#16181d);font:650 11px/1.3 system-ui;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__state-badge{flex:none;padding:3px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#606873);font:650 11px/1.25 system-ui}.dsh-mobile-control__state-badge.is-ready{background:var(--dsw-alias-state-success-tertiary,#e6f7f0);color:var(--dsw-alias-label-primary,#087454)}.dsh-mobile-control__state-badge.is-busy{background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8)}.dsh-mobile-control__state-badge.is-attention{background:var(--dsw-alias-state-warn-tertiary,#fff4dc);color:var(--dsw-alias-label-primary,#935100)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__status{box-sizing:border-box;margin:0 0 10px;padding:9px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-setup-body{margin:0 0 10px}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup{margin:0}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__section-title,.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__frp-setup>.dsh-mobile-control__section-title{display:none}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__details{margin:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{margin-top:2px}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin:10px 0 0}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__manage-row{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}
|
|
7037
8199
|
.dsh-mobile-control__frp-overview{display:grid;grid-template-columns:32px minmax(0,1fr);align-items:center;gap:10px;margin:0 0 10px;padding:10px;border:1px solid var(--dsw-alias-state-success-primary,#a9dfc9);border-radius:11px;background:var(--dsw-alias-state-success-tertiary,#edf9f4)}.dsh-mobile-control__frp-overview[hidden]{display:none}.dsh-mobile-control__frp-overview-mark{display:grid;width:32px;height:32px;place-items:center;border-radius:50%;background:#087454;color:#fff;font:700 15px/1 system-ui}.dsh-mobile-control__frp-overview-body{display:flex;min-width:0;flex-direction:column;gap:2px}.dsh-mobile-control__frp-overview-body strong{color:var(--dsw-alias-label-primary,#075d46);font-size:12px;line-height:1.35}.dsh-mobile-control__frp-overview-body span{overflow:hidden;color:var(--dsw-alias-label-secondary,#357061);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__frp-group{margin:8px 0 0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__frp-group>summary{box-sizing:border-box;min-height:44px;padding:12px 34px 10px 12px;color:var(--dsw-alias-label-primary,#16181d);font:650 12px/1.4 system-ui;cursor:pointer}.dsh-mobile-control__frp-group[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l2,#e1e5eb);background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-group>.dsh-mobile-control__frp-step{padding:12px}.dsh-mobile-control__frp-group>.dsh-mobile-control__frp-step>strong:first-child{display:none}.dsh-mobile-control__frp-group .dsh-mobile-control__frp-step{border-top:0}.dsh-mobile-control__frp-setup>.dsh-mobile-control__frp-step{margin-bottom:8px;padding:10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__tunnel{margin:0 0 10px}.dsh-mobile-control__tunnel[hidden]{display:none}
|
|
7038
8200
|
.dsh-mobile-control__lan-setup{box-sizing:border-box;margin:0 0 12px;padding:14px;border:1px solid #6f96db;border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f5f8ff)}.dsh-mobile-control__lan-setup[hidden],.dsh-mobile-control__lan-setup-form[hidden]{display:none}.dsh-mobile-control__lan-setup>.dsh-mobile-control__section-title{margin-bottom:5px}.dsh-mobile-control__lan-setup>.dsh-mobile-control__intro{margin-bottom:12px}.dsh-mobile-control__lan-setup-form{display:grid;gap:9px}.dsh-mobile-control__lan-setup .dsh-mobile-control__field select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 32px 9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:14px/1.4 system-ui}.dsh-mobile-control__lan-setup .dsh-mobile-control__component-note{margin:0}.dsh-mobile-control__lan-setup-actions{display:grid;grid-template-columns:1fr 1.35fr;gap:8px}.dsh-mobile-control__lan-setup-actions button{box-sizing:border-box;min-width:0;min-height:44px;padding:9px 10px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__lan-setup-actions button:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__lan-setup>.dsh-mobile-control__status{margin:10px 0 0;line-height:1.5}.dsh-mobile-control__lan-setup>.dsh-mobile-control__status:empty{display:none}
|
|
7039
8201
|
.dsh-mobile-control__access{display:flex;align-items:baseline;gap:6px;min-width:0;margin:0 0 12px}.dsh-mobile-control__access[hidden]{display:none}.dsh-mobile-control__access-label{flex:none;color:var(--dsw-alias-label-secondary,#606873);white-space:nowrap}.dsh-mobile-control__access-label::after{content:":"}.dsh-mobile-control__access-link{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary-bluish,#2563eb);text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__access-link:hover{text-decoration:underline}.dsh-mobile-control__qr{display:flex;justify-content:center;margin:0 0 12px}.dsh-mobile-control__qr[hidden]{display:none}.dsh-mobile-control__qr img{border-radius:12px;background:#fff;padding:8px}
|
|
@@ -7068,11 +8230,22 @@ window.__ModuleLoader__.load({
|
|
|
7068
8230
|
style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
|
|
7069
8231
|
document.head.append(style);
|
|
7070
8232
|
if (!desktopAdmin) {
|
|
7071
|
-
const removeSettingsAction = ctx.slots.inject("settings.general.item", () =>
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
8233
|
+
const removeSettingsAction = ctx.slots.inject("settings.general.item", () => {
|
|
8234
|
+
const removeSwitchComputer = ctx.slots.register({
|
|
8235
|
+
name: "settings.general.item",
|
|
8236
|
+
id: "dsh-mobile-switch-computer",
|
|
8237
|
+
order: 100
|
|
8238
|
+
}, MobileSwitchComputerRow);
|
|
8239
|
+
const removeTaskNotifications = ctx.slots.register({
|
|
8240
|
+
name: "settings.general.item",
|
|
8241
|
+
id: "dsh-mobile-task-notifications",
|
|
8242
|
+
order: 110
|
|
8243
|
+
}, MobileTaskNotificationRow);
|
|
8244
|
+
return () => {
|
|
8245
|
+
removeTaskNotifications();
|
|
8246
|
+
removeSwitchComputer();
|
|
8247
|
+
};
|
|
8248
|
+
});
|
|
7076
8249
|
const removeCustom = installCustomAssets();
|
|
7077
8250
|
const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface);
|
|
7078
8251
|
return () => {
|
|
@@ -7141,7 +8314,10 @@ window.__ModuleLoader__.load({
|
|
|
7141
8314
|
exports.clientReleaseInfo = clientReleaseInfo;
|
|
7142
8315
|
exports.combineClientSignalLifetime = combineClientSignalLifetime;
|
|
7143
8316
|
exports.combineClientSignals = combineClientSignals;
|
|
8317
|
+
exports.createFrpAttachFrpcTomlForClipboard = createFrpAttachFrpcTomlForClipboard;
|
|
8318
|
+
exports.createFrpAttachTemplateForClipboard = createFrpAttachTemplateForClipboard;
|
|
7144
8319
|
exports.createFrpServerTemplateForClipboard = createFrpServerTemplateForClipboard;
|
|
8320
|
+
exports.diagnosticControllerAction = diagnosticControllerAction;
|
|
7145
8321
|
exports.diagnosticEntriesForRender = diagnosticEntriesForRender;
|
|
7146
8322
|
exports.diagnosticOverallForChecks = diagnosticOverallForChecks;
|
|
7147
8323
|
exports.diagnosticServerCopy = diagnosticServerCopy;
|
|
@@ -7150,6 +8326,7 @@ window.__ModuleLoader__.load({
|
|
|
7150
8326
|
exports.extensionGenerationHeaders = extensionGenerationHeaders;
|
|
7151
8327
|
exports.extensionRouteUrl = extensionRouteUrl;
|
|
7152
8328
|
exports.failClosedExtensionGenerationReplacement = failClosedExtensionGenerationReplacement;
|
|
8329
|
+
exports.frpAttachFormErrorCode = frpAttachFormErrorCode;
|
|
7153
8330
|
exports.handleMissingExtensionManifest = handleMissingExtensionManifest;
|
|
7154
8331
|
exports.inject = inject;
|
|
7155
8332
|
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|