102 lines
4.8 KiB
Swift
102 lines
4.8 KiB
Swift
import Foundation
|
||
|
||
/// Parsed phobos:// payload from the PCA Phobos panel:
|
||
/// [Interface] + [Peer] (WireGuard) + [instance] (wg-obfuscator) + "# pca-tcp = <port>".
|
||
struct PhobosProfile {
|
||
let raw: String
|
||
|
||
init(_ raw: String) { self.raw = raw }
|
||
|
||
/// phobos://<urlsafe base64>#name
|
||
static func fromLink(_ link: String) -> (profile: PhobosProfile, name: String)? {
|
||
var s = link.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard s.hasPrefix("phobos://") else { return nil }
|
||
s.removeFirst("phobos://".count)
|
||
var name = "Phobos"
|
||
if let hash = s.firstIndex(of: "#") {
|
||
name = String(s[s.index(after: hash)...]).removingPercentEncoding ?? name
|
||
s = String(s[..<hash])
|
||
}
|
||
var b64 = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||
while b64.count % 4 != 0 { b64 += "=" }
|
||
guard let data = Data(base64Encoded: b64), let text = String(data: data, encoding: .utf8),
|
||
text.contains("[Interface]"), text.contains("[instance]") else { return nil }
|
||
return (PhobosProfile(text), name)
|
||
}
|
||
|
||
/// Lines of a section, without "= none" placeholders and comments.
|
||
func lines(_ header: String) -> [(key: String, line: String)] {
|
||
var inSection = false
|
||
var out: [(String, String)] = []
|
||
for rawLine in raw.split(separator: "\n") {
|
||
let line = rawLine.trimmingCharacters(in: .whitespaces)
|
||
if line.hasPrefix("[") && line.hasSuffix("]") { inSection = line.lowercased() == header.lowercased(); continue }
|
||
guard inSection, !line.hasPrefix("#"), let eq = line.firstIndex(of: "=") else { continue }
|
||
let value = line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces)
|
||
if value.lowercased() == "none" { continue }
|
||
out.append((line[..<eq].trimmingCharacters(in: .whitespaces).lowercased(), line))
|
||
}
|
||
return out
|
||
}
|
||
|
||
private func value(_ header: String, _ key: String) -> String? {
|
||
lines(header).first { $0.key == key }.map { line in
|
||
line.line[line.line.index(after: line.line.firstIndex(of: "=")!)...].trimmingCharacters(in: .whitespaces)
|
||
}
|
||
}
|
||
|
||
var target: String? { value("[instance]", "target") }
|
||
var targetHost: String? { target.map { String($0[..<($0.lastIndex(of: ":") ?? $0.endIndex)]) } }
|
||
var targetPort: Int? { target.flatMap { $0.split(separator: ":").last.flatMap { Int($0) } } }
|
||
|
||
/// "# pca-tcp = 443" written by the panel when the server has the TCP fallback.
|
||
var tcpPort: Int? {
|
||
for rawLine in raw.split(separator: "\n") {
|
||
let line = rawLine.trimmingCharacters(in: .whitespaces)
|
||
if line.hasPrefix("#"), line.contains("pca-tcp"), let eq = line.firstIndex(of: "=") {
|
||
return Int(line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces))
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// "# pca-ws = wss://…" — the user's Keenetic (KeenDNS cloud subdomain -> wsgw), channel «Облако».
|
||
var wsURL: String? {
|
||
for rawLine in raw.split(separator: "\n") {
|
||
let line = rawLine.trimmingCharacters(in: .whitespaces)
|
||
if line.hasPrefix("#"), line.contains("pca-ws"), let eq = line.firstIndex(of: "=") {
|
||
return line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func obfuscatorConfig(listen: Int, target: String) -> String {
|
||
var out = "[instance]\nsource-if = 127.0.0.1\nsource-lport = \(listen)\ntarget = \(target)\n"
|
||
for item in lines("[instance]") where !["source-if", "source-lport", "target", "verbose"].contains(item.key) {
|
||
out += item.line + "\n"
|
||
}
|
||
return out + "verbose = INFO\n"
|
||
}
|
||
|
||
func wireGuardConfig(endpoint: String, allowedIPs: String?) -> String {
|
||
var out = "[Interface]\n"
|
||
for item in lines("[Interface]") where !item.key.hasSuffix("applications") && item.key != "mtu" { out += item.line + "\n" }
|
||
// ponytail: obfuscator masking adds bytes; the panel's default 1420 stalls on mobile networks,
|
||
// so 1420/missing -> 1280, any other MTU in the link wins (set per client in the panel)
|
||
let linkMTU = lines("[Interface]").first { $0.key == "mtu" }
|
||
.flatMap { Int($0.line.split(separator: "=").last?.trimmingCharacters(in: .whitespaces) ?? "") }
|
||
let mtu = (linkMTU == nil || linkMTU == 1420) ? 1280 : linkMTU!
|
||
out += "MTU = \(mtu)\n"
|
||
out += "\n[Peer]\n"
|
||
for item in lines("[Peer]") {
|
||
switch item.key {
|
||
case "endpoint": out += "Endpoint = \(endpoint)\n"
|
||
case "allowedips" where allowedIPs != nil: out += "AllowedIPs = \(allowedIPs!)\n"
|
||
default: out += item.line + "\n"
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
}
|