import Foundation /// Mirrors internal/protocols/xray — share-link parse + SOCKS/HTTP config (ports 1080/1081). enum XrayConfigBuilder { struct Link { var protocolKind: String // vless | vmess | trojan var address: String var port: Int var uuid: String var alterID: Int = 0 var security: String = "" var flow: String = "" var network: String = "tcp" var type: String = "" var host: String = "" var path: String = "" var tls: String = "none" var sni: String = "" var alpn: String = "" var fp: String = "" var pbk: String = "" var sid: String = "" var spx: String = "" var serviceName: String = "" var mode: String = "" var allowInsecure: Bool = false var packetEncoding: String = "" } static func buildJSON(uri: String, socksPort: Int = 1080, httpPort: Int = 1081) throws -> String { let link = try parse(uri) let outbound = try buildOutbound(link) let root: [String: Any] = [ "log": ["loglevel": "warning"], "inbounds": [ [ "tag": "socks-in", "listen": "127.0.0.1", "port": socksPort, "protocol": "socks", "settings": ["udp": true, "auth": "noauth"], ], [ "tag": "http-in", "listen": "127.0.0.1", "port": httpPort, "protocol": "http", ], ], "outbounds": [ outbound, ["protocol": "freedom", "tag": "direct"], ["protocol": "blackhole", "tag": "block"], ], ] let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) guard let s = String(data: data, encoding: .utf8) else { throw BuilderError.message("json encode failed") } return s + "\n" } static func parse(_ raw: String) throws -> Link { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) let lower = trimmed.lowercased() if lower.hasPrefix("vless://") { return try parseVLESS(trimmed) } if lower.hasPrefix("vmess://") { return try parseVMess(trimmed) } if lower.hasPrefix("trojan://") { return try parseTrojan(trimmed) } throw BuilderError.message("ожидалась ссылка vless://, vmess:// или trojan://") } // MARK: - Outbound / stream (config_write.go) private static func buildOutbound(_ link: Link) throws -> [String: Any] { let stream = try buildStreamSettings(link) switch link.protocolKind { case "vless": var user: [String: Any] = [ "id": link.uuid, "encryption": firstNonEmpty(link.security, "none"), ] if !link.flow.isEmpty { user["flow"] = link.flow } var settings: [String: Any] = [ "vnext": [[ "address": link.address, "port": link.port, "users": [user], ]], ] if !link.packetEncoding.isEmpty { settings["packetEncoding"] = link.packetEncoding } return [ "tag": "proxy", "protocol": "vless", "settings": settings, "streamSettings": stream, ] case "vmess": return [ "tag": "proxy", "protocol": "vmess", "settings": [ "vnext": [[ "address": link.address, "port": link.port, "users": [[ "id": link.uuid, "alterId": link.alterID, "security": firstNonEmpty(link.security, "auto"), ]], ]], ], "streamSettings": stream, ] case "trojan": return [ "tag": "proxy", "protocol": "trojan", "settings": [ "servers": [[ "address": link.address, "port": link.port, "password": link.uuid, ]], ], "streamSettings": stream, ] default: throw BuilderError.message("unsupported xray protocol \(link.protocolKind)") } } private static func buildStreamSettings(_ link: Link) throws -> [String: Any] { var network = firstNonEmpty(link.network, "tcp").lowercased() var stream: [String: Any] = ["network": network] switch network { case "ws", "websocket": stream["network"] = "ws" var ws: [String: Any] = [:] if !link.path.isEmpty { ws["path"] = link.path } if !link.host.isEmpty { ws["headers"] = ["Host": link.host] } stream["wsSettings"] = ws case "grpc", "gun": stream["network"] = "grpc" var grpc: [String: Any] = ["serviceName": firstNonEmpty(link.serviceName, link.path)] if !link.mode.isEmpty { grpc["multiMode"] = link.mode.lowercased() == "multi" } stream["grpcSettings"] = grpc case "h2", "http": stream["network"] = "h2" var h2: [String: Any] = [:] if !link.path.isEmpty { h2["path"] = link.path } if !link.host.isEmpty { h2["host"] = link.host.split(separator: ",").map { String($0) } } stream["httpSettings"] = h2 case "httpupgrade": stream["network"] = "httpupgrade" var hu: [String: Any] = [:] if !link.path.isEmpty { hu["path"] = link.path } if !link.host.isEmpty { hu["host"] = link.host } stream["httpupgradeSettings"] = hu case "xhttp", "splithttp": stream["network"] = "xhttp" var xh: [String: Any] = [:] if !link.path.isEmpty { xh["path"] = link.path } if !link.host.isEmpty { xh["host"] = link.host } if !link.mode.isEmpty { xh["mode"] = link.mode } stream["xhttpSettings"] = xh default: stream["network"] = "tcp" if !link.type.isEmpty && link.type != "none" { stream["tcpSettings"] = ["header": ["type": link.type]] } } let sec = firstNonEmpty(link.tls, "none").lowercased() switch sec { case "", "none", "0", "false": stream["security"] = "none" case "tls": stream["security"] = "tls" var tls: [String: Any] = [ "serverName": firstNonEmpty(link.sni, link.host, link.address), "allowInsecure": link.allowInsecure, ] if !link.fp.isEmpty { tls["fingerprint"] = link.fp } if !link.alpn.isEmpty { tls["alpn"] = link.alpn.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } } stream["tlsSettings"] = tls case "reality": if link.pbk.isEmpty { throw BuilderError.message("reality: нужен pbk (publicKey)") } stream["security"] = "reality" stream["realitySettings"] = [ "serverName": firstNonEmpty(link.sni, link.host, link.address), "fingerprint": firstNonEmpty(link.fp, "chrome"), "publicKey": link.pbk, "shortId": link.sid, "spiderX": link.spx, ] default: stream["security"] = sec } return stream } // MARK: - Parsers private static func parseVLESS(_ raw: String) throws -> Link { var remark = "" var body = raw if let hash = body.firstIndex(of: "#") { remark = String(body[body.index(after: hash)...]).removingPercentEncoding ?? String(body[body.index(after: hash)...]) body = String(body[.. Link { var body = raw if let hash = body.firstIndex(of: "#") { body = String(body[.. Link { var body = raw if let r = body.range(of: "vmess://", options: .caseInsensitive) { body = String(body[r.upperBound...]) } if let hash = body.firstIndex(of: "#") { body = String(body[.. [String: String] { var out: [String: String] = [:] for item in u.queryItems ?? [] { out[item.name] = item.value ?? "" } return out } private static func firstNonEmpty(_ vals: String?...) -> String { for v in vals { if let v, !v.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return v.trimmingCharacters(in: .whitespacesAndNewlines) } } return "" } private static func truthy(_ s: String?) -> Bool { switch (s ?? "").lowercased() { case "1", "true", "yes", "y", "on": return true default: return false } } private static func decodeBase64(_ s: String) -> String? { var cleaned = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") let pad = (4 - cleaned.count % 4) % 4 cleaned += String(repeating: "=", count: pad) guard let data = Data(base64Encoded: cleaned) else { return nil } return String(data: data, encoding: .utf8) } } enum BuilderError: LocalizedError { case message(String) var errorDescription: String? { switch self { case .message(let s): return s } } }