Initial project setup - Phases 1-3 complete
This commit is contained in:
106
ProxyCore/Sources/Shared/CURLParser.swift
Normal file
106
ProxyCore/Sources/Shared/CURLParser.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
import Foundation
|
||||
|
||||
public struct ParsedCURLRequest: Sendable {
|
||||
public var method: String = "GET"
|
||||
public var url: String = ""
|
||||
public var headers: [(key: String, value: String)] = []
|
||||
public var body: String?
|
||||
}
|
||||
|
||||
public enum CURLParser {
|
||||
public static func parse(_ curlString: String) -> ParsedCURLRequest? {
|
||||
var result = ParsedCURLRequest()
|
||||
let trimmed = curlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard trimmed.lowercased().hasPrefix("curl") else { return nil }
|
||||
|
||||
let tokens = tokenize(trimmed)
|
||||
var i = 0
|
||||
|
||||
while i < tokens.count {
|
||||
let token = tokens[i]
|
||||
|
||||
switch token {
|
||||
case "curl":
|
||||
break
|
||||
case "-X", "--request":
|
||||
i += 1
|
||||
if i < tokens.count {
|
||||
result.method = tokens[i].uppercased()
|
||||
}
|
||||
case "-H", "--header":
|
||||
i += 1
|
||||
if i < tokens.count {
|
||||
let header = tokens[i]
|
||||
if let colonIndex = header.firstIndex(of: ":") {
|
||||
let key = String(header[header.startIndex..<colonIndex]).trimmingCharacters(in: .whitespaces)
|
||||
let value = String(header[header.index(after: colonIndex)...]).trimmingCharacters(in: .whitespaces)
|
||||
result.headers.append((key: key, value: value))
|
||||
}
|
||||
}
|
||||
case "-d", "--data", "--data-raw", "--data-binary":
|
||||
i += 1
|
||||
if i < tokens.count {
|
||||
result.body = tokens[i]
|
||||
if result.method == "GET" {
|
||||
result.method = "POST"
|
||||
}
|
||||
}
|
||||
default:
|
||||
if !token.hasPrefix("-") && result.url.isEmpty {
|
||||
result.url = token
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
return result.url.isEmpty ? nil : result
|
||||
}
|
||||
|
||||
private static func tokenize(_ input: String) -> [String] {
|
||||
var tokens: [String] = []
|
||||
var current = ""
|
||||
var inSingleQuote = false
|
||||
var inDoubleQuote = false
|
||||
var escaped = false
|
||||
|
||||
for char in input {
|
||||
if escaped {
|
||||
current.append(char)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if char == "\\" && !inSingleQuote {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if char == "'" && !inDoubleQuote {
|
||||
inSingleQuote.toggle()
|
||||
continue
|
||||
}
|
||||
|
||||
if char == "\"" && !inSingleQuote {
|
||||
inDoubleQuote.toggle()
|
||||
continue
|
||||
}
|
||||
|
||||
if char.isWhitespace && !inSingleQuote && !inDoubleQuote {
|
||||
if !current.isEmpty {
|
||||
tokens.append(current)
|
||||
current = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
current.append(char)
|
||||
}
|
||||
|
||||
if !current.isEmpty {
|
||||
tokens.append(current)
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
18
ProxyCore/Sources/Shared/Constants.swift
Normal file
18
ProxyCore/Sources/Shared/Constants.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
public enum ProxyConstants {
|
||||
public static let proxyHost = "127.0.0.1"
|
||||
public static let proxyPort: Int = 9090
|
||||
public static let appGroupIdentifier = "group.com.treyt.proxyapp"
|
||||
public static let extensionBundleIdentifier = "com.treyt.proxyapp.PacketTunnel"
|
||||
public static let maxBodySizeBytes = 1_048_576 // 1 MB - truncate larger bodies
|
||||
public static let certificateCacheSize = 500
|
||||
|
||||
public static let httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
|
||||
|
||||
public static let commonHeaders = [
|
||||
"Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language",
|
||||
"Authorization", "Cache-Control", "Connection", "Content-Length",
|
||||
"Content-Type", "Cookie", "Host", "Origin", "Referer", "User-Agent"
|
||||
]
|
||||
}
|
||||
103
ProxyCore/Sources/Shared/IPCManager.swift
Normal file
103
ProxyCore/Sources/Shared/IPCManager.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
|
||||
/// Lightweight IPC between the main app and the packet tunnel extension
|
||||
/// using Darwin notifications (fire-and-forget signals) and shared UserDefaults.
|
||||
public final class IPCManager: Sendable {
|
||||
public static let shared = IPCManager()
|
||||
|
||||
private let suiteName = "group.com.treyt.proxyapp"
|
||||
|
||||
public enum Notification: String, Sendable {
|
||||
case newTrafficCaptured = "com.treyt.proxyapp.newTraffic"
|
||||
case configurationChanged = "com.treyt.proxyapp.configChanged"
|
||||
case extensionStarted = "com.treyt.proxyapp.extensionStarted"
|
||||
case extensionStopped = "com.treyt.proxyapp.extensionStopped"
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Darwin Notifications
|
||||
|
||||
public func post(_ notification: Notification) {
|
||||
let name = CFNotificationName(notification.rawValue as CFString)
|
||||
CFNotificationCenterPostNotification(
|
||||
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
name, nil, nil, true
|
||||
)
|
||||
}
|
||||
|
||||
public func observe(_ notification: Notification, callback: @escaping @Sendable () -> Void) {
|
||||
let name = notification.rawValue as CFString
|
||||
let center = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
|
||||
// Store callback in a static dictionary keyed by notification name
|
||||
DarwinCallbackStore.shared.register(name: notification.rawValue, callback: callback)
|
||||
|
||||
CFNotificationCenterAddObserver(
|
||||
center, nil,
|
||||
{ _, _, name, _, _ in
|
||||
guard let cfName = name?.rawValue as? String else { return }
|
||||
DarwinCallbackStore.shared.fire(name: cfName)
|
||||
},
|
||||
name, nil,
|
||||
.deliverImmediately
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Shared UserDefaults
|
||||
|
||||
public var sharedDefaults: UserDefaults? {
|
||||
UserDefaults(suiteName: suiteName)
|
||||
}
|
||||
|
||||
public var isSSLProxyingEnabled: Bool {
|
||||
get { sharedDefaults?.bool(forKey: "sslProxyingEnabled") ?? false }
|
||||
set { sharedDefaults?.set(newValue, forKey: "sslProxyingEnabled") }
|
||||
}
|
||||
|
||||
public var isBlockListEnabled: Bool {
|
||||
get { sharedDefaults?.bool(forKey: "blockListEnabled") ?? false }
|
||||
set { sharedDefaults?.set(newValue, forKey: "blockListEnabled") }
|
||||
}
|
||||
|
||||
public var isBreakpointEnabled: Bool {
|
||||
get { sharedDefaults?.bool(forKey: "breakpointEnabled") ?? false }
|
||||
set { sharedDefaults?.set(newValue, forKey: "breakpointEnabled") }
|
||||
}
|
||||
|
||||
public var isNoCachingEnabled: Bool {
|
||||
get { sharedDefaults?.bool(forKey: "noCachingEnabled") ?? false }
|
||||
set { sharedDefaults?.set(newValue, forKey: "noCachingEnabled") }
|
||||
}
|
||||
|
||||
public var isDNSSpoofingEnabled: Bool {
|
||||
get { sharedDefaults?.bool(forKey: "dnsSpoofingEnabled") ?? false }
|
||||
set { sharedDefaults?.set(newValue, forKey: "dnsSpoofingEnabled") }
|
||||
}
|
||||
|
||||
public var hideSystemTraffic: Bool {
|
||||
get { sharedDefaults?.bool(forKey: "hideSystemTraffic") ?? false }
|
||||
set { sharedDefaults?.set(newValue, forKey: "hideSystemTraffic") }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Darwin Callback Storage
|
||||
|
||||
private final class DarwinCallbackStore: @unchecked Sendable {
|
||||
static let shared = DarwinCallbackStore()
|
||||
private var callbacks: [String: @Sendable () -> Void] = [:]
|
||||
private let lock = NSLock()
|
||||
|
||||
func register(name: String, callback: @escaping @Sendable () -> Void) {
|
||||
lock.lock()
|
||||
callbacks[name] = callback
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func fire(name: String) {
|
||||
lock.lock()
|
||||
let cb = callbacks[name]
|
||||
lock.unlock()
|
||||
cb?()
|
||||
}
|
||||
}
|
||||
40
ProxyCore/Sources/Shared/WildcardMatcher.swift
Normal file
40
ProxyCore/Sources/Shared/WildcardMatcher.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
|
||||
public enum WildcardMatcher {
|
||||
/// Matches a string against a glob pattern with `*` (zero or more chars) and `?` (single char).
|
||||
public static func matches(_ string: String, pattern: String) -> Bool {
|
||||
let s = Array(string.lowercased())
|
||||
let p = Array(pattern.lowercased())
|
||||
return matchHelper(s, 0, p, 0)
|
||||
}
|
||||
|
||||
private static func matchHelper(_ s: [Character], _ si: Int, _ p: [Character], _ pi: Int) -> Bool {
|
||||
var si = si
|
||||
var pi = pi
|
||||
var starIdx = -1
|
||||
var matchIdx = 0
|
||||
|
||||
while si < s.count {
|
||||
if pi < p.count && (p[pi] == "?" || p[pi] == s[si]) {
|
||||
si += 1
|
||||
pi += 1
|
||||
} else if pi < p.count && p[pi] == "*" {
|
||||
starIdx = pi
|
||||
matchIdx = si
|
||||
pi += 1
|
||||
} else if starIdx != -1 {
|
||||
pi = starIdx + 1
|
||||
matchIdx += 1
|
||||
si = matchIdx
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
while pi < p.count && p[pi] == "*" {
|
||||
pi += 1
|
||||
}
|
||||
|
||||
return pi == p.count
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user