import type { ParsedNode } from './ss.js'; import { readInsecure } from './util.js'; // hysteria2://auth@host:port?sni=...&insecure=1&obfs=salamander&obfs-password=...#name // hy2:// is an accepted alias for the scheme. export function parseHysteria2(uri: string): ParsedNode { const url = new URL(uri); const server = url.hostname; const port = parseInt(url.port || '443', 10); const name = decodeURIComponent(url.hash.slice(1)) || 'Hysteria2'; // The auth string lives in the userinfo. For userpass auth it is "username:password". const user = decodeURIComponent(url.username); const pass = decodeURIComponent(url.password); const auth = pass ? `${user}:${pass}` : user; const params = url.searchParams; const sni = params.get('sni') || ''; const obfs = params.get('obfs') || ''; const obfsPassword = params.get('obfs-password') || params.get('obfs_password') || ''; // pinSHA256 means the node uses a self-signed / SNI-masqueraded cert verified by // fingerprint rather than the standard CA chain. Surge's server-cert-fingerprint-sha256 // proved unreliable here (it still validates against the SNI hostname and rejects the // cert), so in that case we skip standard verification — matching the effective behavior // of clients like Shadowrocket that connect fine via pinSHA256. const hasPin = !!(params.get('pinSHA256') || params.get('pinsha256')); let line = `${name} = hysteria2, ${server}, ${port}, password=${auth}`; if (sni) line += `, sni=${sni}`; if (obfs === 'salamander' && obfsPassword) line += `, salamander-password=${obfsPassword}`; line += `, skip-cert-verify=${readInsecure(params) || hasPin}`; return { name, type: 'hysteria2', server, port, surgeLine: line }; }