Używanie proxy w Swift z URLSession — podstawy
Konfiguracja proxy w Swift przez URLSession to zadanie, które wielu programistów iOS i macOS odkłada na później — dokumentacja Apple jest rozproszona, a klucze CFNetwork trudne do znalezienia. Jeśli budujesz aplikację, która musi pobierać dane z API ograniczonych regionalnie, monitorować ceny lub scrapować publiczne strony, Swift proxy staje się niezbędny. W tym przewodniku pokazuję krok po kroku, jak skonfigurować URLSessionConfiguration.connectionProxyDictionary, jak obsłużyć autoryzację i geo-targeting, jak używać SOCKS5 oraz jak pisać wydajny kod async/await z obsługą błędów.
Dlaczego proxy w URLSession to problem
URLSession domyślnie używa ustawień systemowych (System Configuration), co oznacza, że jeśli użytkownik ma ustawione proxy w Wi-Fi, URLSession je zastosuje. Programowa konfiguracja proxy wymaga ustawienia connectionProxyDictionary na obiekcie URLSessionConfiguration przed utworzeniem sesji. Słownik ten używa kluczy z frameworka CFNetwork, zdefiniowanych w dokumentacji CFNetwork Apple. Problem polega na tym, że te klucze nie są bezpośrednio widoczne w Swift — trzeba je przekonwertować z CFString na String.
Dodatkowo URLSession ma ograniczoną obsługę proxy authentication — klucze kCFProxyUsernameKey i kCFProxyPasswordKey są nielidalne w wielu wersjach iOS/macOS (patrz dokumentacja URLSessionConfiguration.connectionProxyDictionary), co zmusza do ręcznego ustawiania nagłówka Proxy-Authorization lub implementacji delegata urlSession(_:didReceive:completionHandler:).
Konfiguracja HTTP/HTTPS proxy w URLSession
Zacznijmy od podstawowej konfiguracji proxy HTTP i HTTPS, kierującego ruch przez bramę ProxyHat gate.proxyhat.com na porcie 8080. Musimy ustawić osobne klucze dla HTTP i HTTPS, ponieważ URLSession traktuje je niezależnie.
import Foundation
func createProxySession(username: String, password: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
// Klucze CFNetwork — konwertujemy z CFString
let kHTTPEnable = kCFNetworkProxiesHTTPEnable as String
let kHTTPProxy = kCFNetworkProxiesHTTPProxy as String
let kHTTPPort = kCFNetworkProxiesHTTPPort as String
let kHTTPSEnable = kCFNetworkProxiesHTTPSEnable as String
let kHTTPSProxy = kCFNetworkProxiesHTTPSProxy as String
let kHTTPSPort = kCFNetworkProxiesHTTPSPort as String
config.connectionProxyDictionary = [
kHTTPEnable: true,
kHTTPProxy: "gate.proxyhat.com",
kHTTPPort: 8080,
kHTTPSEnable: true,
kHTTPSProxy: "gate.proxyhat.com",
kHTTPSPort: 8080
]
// Proxy-Authorization header — bo kCFProxyUsernameKey/PasswordKey
// są nielidalne w URLSession
let token = "\(username):\(password)"
let base64 = Data(token.utf8).base64EncodedString()
config.httpAdditionalHeaders = [
"Proxy-Authorization": "Basic \(base64)"
]
return URLSession(configuration: config)
}
// Użycie
let session = createProxySession(
username: "user-country-US-city-newyork-session-abc123",
password: "twoje_haslo"
)
let url = URL(string: "https://httpbin.org/ip")!
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print("Błąd: \(error)")
return
}
if let data = data {
print(String(data: data, encoding: .utf8) ?? "")
}
}
task.resume()
Zwróć uwagę na URLSessionConfiguration.ephemeral — używamy go zamiast .default, aby uniknąć wycieków danych do pamięci podręcznej i plików cookie, które mogą ujawnić prawdziwe IP użytkownika. httpAdditionalHeaders ustawia nagłówek Proxy-Authorization globalnie dla całej sesji, co jest najprostszym sposobem autoryzacji.
Autoryzacja i geo-targeting — Proxy-Authorization vs delegat 407
Geo-targeting w ProxyHat odbywa się przez zakodowanie parametrów w nazwie użytkownika, np. user-country-US-city-newyork-session-abc123. To oznacza, że pełny token Basic Auth zawiera znaki specjalne, które muszą być poprawnie zakodowane w Base64.
Powyższe podejście z httpAdditionalHeaders działa w większości przypadków, ale niektóre serwery proxy mogą najpierw odrzucić żądanie z kodem 407 Proxy Authentication Required, zamiast zaakceptować nagłówek uprzedzający. Wtedy musisz zaimplementować delegata urlSession(_:didReceive:completionHandler:):
import Foundation
class ProxyAuthDelegate: NSObject, URLSessionDelegate {
let username: String
let password: String
init(username: String, password: String) {
self.username = username
self.password = password
}
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
// Sprawdź, czy to wyzwanie proxy (nie serwera)
if challenge.protectionSpace.authenticationMethod
== NSURLAuthenticationMethodHTTPProxy ||
challenge.protectionSpace.authenticationMethod
== NSURLAuthenticationMethodHTTPSProxy {
let credential = URLCredential(
user: username,
password: password,
persistence: .forSession
)
completionHandler(.useCredential, credential)
} else {
// Pozostałe wyzwania (TLS itp.) — domyślne zachowanie
completionHandler(.performDefaultHandling, nil)
}
}
}
// Użycie z geo-targetingiem
let delegate = ProxyAuthDelegate(
username: "user-country-DE-city-berlin-session-sess42",
password: "twoje_haslo"
)
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable as String: true,
kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort as String: 8080,
kCFNetworkProxiesHTTPSEnable as String: true,
kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort as String: 8080
]
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
Wybór między nagłówkiem Proxy-Authorization a delegatem zależy od Twojego przypadku użycia. Nagłówek jest prostszy i działa z URLSession.shared, ale nie obsługuje ponownego wyzwania. Delegat jest bardziej niezawodny, ale wymaga utworzenia dedykowanej sesji. W produkcji zalecam oba podejścia jednocześnie — nagłówek jako domyślne, delegat jako fallback.
SOCKS5 proxy w URLSession
ProxyHat obsługuje również SOCKS5 na porcie 1080. Konfiguracja SOCKS5 w URLSession używa kluczy kCFStreamPropertySOCKSProxy*, które są mniej udokumentowane, ale działają poprawnie:
import Foundation
func createSOCKS5Session(username: String, password: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
let kProxyEnable = kCFNetworkProxiesSOCKSEnable as String
let kProxyHost = kCFStreamPropertySOCKSProxyHost as String
let kProxyPort = kCFStreamPropertySOCKSProxyPort as String
let kProxyVersion = kCFStreamPropertySOCKSVersion as String
let kProxyUser = kCFStreamPropertySOCKSProxyUser as String
let kProxyPass = kCFStreamPropertySOCKSProxyPassword as String
config.connectionProxyDictionary = [
kProxyEnable: true,
kProxyHost: "gate.proxyhat.com",
kProxyPort: 1080,
kProxyVersion: kCFStreamSocketSOCKSVersion5 as String,
kProxyUser: username,
kProxyPass: password
]
return URLSession(configuration: config)
}
let socksSession = createSOCKS5Session(
username: "user-country-GB-session-task99",
password: "twoje_haslo"
)
SOCKS5 w URLSession obsługuje natywnie autoryzację username/password przez klucze kCFStreamPropertySOCKSProxyUser i kCFStreamPropertySOCKSProxyPassword — w przeciwieństwie do HTTP proxy, gdzie te klucze są nielidalne. Jeśli więc potrzebujesz prostszej autoryzacji bez nagłówków, SOCKS5 może być lepszym wyborem.
Dlaczego proxy rezydencjalne są potrzebne — Swift web scraping w praktyce
Wiele API i stron web blokuje adresy IP z centrów danych (datacenter). Przykładowo, Cloudflare i Akamai mają listy znanych zakresów IP centrów danych i odrzucają żądania z tych adresów z kodem 403 lub wstawiają wyzwanie CAPTCHA. Proxy rezydencjalne pochodzą z prawdziwych urządzeń domowych przydzielonych przez ISP, co sprawia, że ruch wygląda jak organiczny.
Typowe scenariusze, w których potrzebujesz proxy rezydencjalnych w Swift:
- Scraping SERP — Google ogranicza liczbę zapytań z jednego IP do około 100 zapytań/dzień przed wyświetleniem CAPTCHA.
- Monitoring cen e-commerce — sklepy jak Amazon i Walmart blokują podejrzane wzorce ruchu z datacenter.
- Treści regionalnie ograniczone — API streamingowe i sportowe często sprawdzają geolokalizację IP.
- Testowanie aplikacji — QA zespoły potrzebują symulować ruch z różnych krajów.
Oto kompletny przykład async/await z URLSession.shared.data(for:), dekodowaniem Codable i TaskGroup dla współbieżności:
import Foundation
// Model odpowiedzi z httpbin.org/ip
struct IPResponse: Codable {
let origin: String
}
// Konfiguracja sesji z proxy
func makeProxySession(country: String, session: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable as String: true,
kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort as String: 8080,
kCFNetworkProxiesHTTPSEnable as String: true,
kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort as String: 8080
]
let username = "user-country-\(country)-session-\(session)"
let token = "\(username):twoje_haslo"
let base64 = Data(token.utf8).base64EncodedString()
config.httpAdditionalHeaders = ["Proxy-Authorization": "Basic \(base64)"]
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 30
return URLSession(configuration: config)
}
// Funkcja pobierająca IP przez proxy
func fetchIP(country: String, sessionID: String) async throws -> String {
let session = makeProxySession(country: country, session: sessionID)
let url = URL(string: "https://httpbin.org/ip")!
let (data, response) = try await session.data(for: URLRequest(url: url))
guard let httpResp = response as? HTTPURLResponse,
httpResp.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let decoded = try JSONDecoder().decode(IPResponse.self, from: data)
return decoded.origin
}
// Współbieżne pobieranie z wielu krajów
func fetchMultipleCountries() async {
let countries = ["US", "DE", "GB", "JP", "BR"]
do {
let results = try await withThrowingTaskGroup(of: (String, String).self) { group in
for country in countries {
group.addTask {
let ip = try await fetchIP(country: country, sessionID: "batch-\(country)")
return (country, ip)
}
}
var collected: [(String, String)] = []
for try await result in group {
collected.append(result)
}
return collected
}
for (country, ip) in results.sorted(by: { $0.0 < $1.0 }) {
print("\(country): \(ip)")
}
} catch {
print("Błąd współbieżny: \(error)")
}
}
// Uruchomienie
Task { await fetchMultipleCountries() }
Powyższy kod tworzy osobną sesję URLSession dla każdego kraju z unikalnym sessionID, co daje sticky session — wszystkie żądania w ramach jednej sesji używają tego samego IP. TaskGroup uruchamia zapytania współbieżnie, a URLSession zarządza pulą połączeń wewnętrznie. Zwróć uwagę, że tworzenie nowej URLSession dla każdego zadania nie jest idealne — w produkcji powinieneś buforować sesje.
Wskazówki produkcyjne — TLS, retry, ATS i prywatność
Delegat TLS i walidacja certyfikatów
W niektórych przypadkach (np. corporate MITM proxy) musisz obsłużyć niestandardowe certyfikaty. Użyj URLSessionDelegate do walidacji TLS:
import Foundation
class TLSPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
guard let trust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Walidacja standardowa + opcjonalne pinning
var error: CFError?
let isValid = SecTrustEvaluateWithError(trust, &error)
if isValid {
completionHandler(.useCredential, URLCredential(trust: trust))
} else {
print("TLS validation failed: \(error?.localizedDescription ?? "unknown")")
completionHandler(.cancelAuthenticationChallenge, nil)
}
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
Retry z exponential backoff
Zapytania przez proxy mogą zawodzić z powodu timeoutów, rate limitów (HTTP 429) lub tymczasowych błędów sieci. Implementacja retry z exponential backoff jest kluczowa:
import Foundation
enum ProxyError: Error {
case maxRetriesExceeded
case rateLimited(retryAfter: TimeInterval)
}
func fetchWithRetry(
url: URL,
session: URLSession,
maxRetries: Int = 3,
baseDelay: TimeInterval = 1.0
) async throws -> Data {
var attempt = 0
while attempt < maxRetries {
do {
let (data, response) = try await session.data(for: URLRequest(url: url))
if let httpResp = response as? HTTPURLResponse {
if httpResp.statusCode == 429 {
let retryAfter = httpResp.value(forHTTPHeaderField: "Retry-After")
let delay = TimeInterval(retryAfter ?? "5") ?? 5
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
attempt += 1
continue
}
if httpResp.statusCode == 200 {
return data
}
if (500...599).contains(httpResp.statusCode) {
let delay = baseDelay * pow(2.0, Double(attempt))
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
attempt += 1
continue
}
return data // inne kody — zwróć dane
}
} catch let error as URLError {
let delay = baseDelay * pow(2.0, Double(attempt))
print("Próba \(attempt + 1) błąd: \(error.code.rawValue) — retry za \(delay)s")
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
attempt += 1
continue
}
}
throw ProxyError.maxRetriesExceeded
}
App Transport Security (ATS)
Apple wymaga TLS 1.2+ dla wszystkich połączeń przez App Transport Security. Proxy HTTP (nie HTTPS) między aplikacją a serwerem docelowym jest w pełni kompatybilne z ATS, ponieważ proxy tylko przekazuje tunelowane żądanie TLS — nie dekoduje go. Nie musisz wyłączać ATS, jeśli używasz proxy HTTP dla ruchu HTTPS do celu. Jeśli jednak próbujesz połączyć się z celem HTTP (nie HTTPS), musisz dodać wyjątek ATS w Info.plist.
Prywatność na urządzeniu
- Używaj
URLSessionConfiguration.ephemeral, aby uniknąć zapisywania ciasteczek i pamięci podręcznej na dysku. - Nie loguj zawartości nagłówka
Proxy-Authorization— zawiera on poświadczenia w Base64. - Dodaj
NSPrivacyAccessedAPICategoryUserDefaultsdo Privacy Manifest, jeśli przechowujesz konfigurację proxy w UserDefaults. - Aplikacje na iOS 17+ muszą deklarować powody używania API sieciowych w Privacy Manifest.
Konfiguracja ProxyHat — gotowe w 5 minut
ProxyHat udostępnia bramę gate.proxyhat.com na porcie 8080 (HTTP) i 1080 (SOCKS5). Wszystkie parametry geo-targetingu i sesji kodujesz w nazwie użytkownika:
| Parametr | Format w username | Przykład |
|---|---|---|
| Kraj | country-XX |
user-country-US |
| Miasto | city-name |
user-country-US-city-newyork |
| Sticky session | session-ID |
user-session-abc123 |
| Kombinacja | wszystkie razem | user-country-DE-city-berlin-session-s1 |
Sprawdź ceny ProxyHat oraz dostępne lokalizacje. ProxyHat udostępnia również SDK dla Python i Node.js, które odzwierciedlają tę samą bramę — przydatne, jeśli Twój backend nie jest w Swift. Pełną dokumentację API znajdziesz na docs.proxyhat.com.
Jeśli interesuje Cię scraping wyników wyszukiwania, zobacz nasz przewodnik SERP tracking. Do ogólnego scrapingu stron polecamy web scraping use case.
Etyka i prawo — co musisz wiedzieć
Używanie proxy do pobierania publicznych danych jest legalne w większości jurysdykcji, ale istnieją granice:
- USA — CFAA (Computer Fraud and Abuse Act): scraping publicznych danych nie narusza CFAA według orzeczenia Van Buren v. United States (2021), ale omijanie technicznych barier (CAPTCHA, rate limiting) może być problematyczne. Zobacz stanowisko FTC w sprawie scraping.
- EU — GDPR: jeśli scrapujesz dane osobowe (np. profile użytkowników), musisz mieć podstawę prawną. Pobieranie publicznych danych nieosobowych nie podlega GDPR.
- App Store Guidelines: Apple wymaga, aby aplikacje nie zbierały danych bez zgody użytkownika i nie naruszały ToS stron trzecich. Scraping w aplikacji klienckiej może naruszyć punkt 5.2.2 guidelines.
- Zawsze sprawdzaj
robots.txti ToS przed scrapingiem.
Złota zasada: jeśli istnieje oficjalne API, użyj go najpierw. Proxy są dla przypadków, gdy API nie istnieje, jest zbyt ograniczone lub nie pokrywa potrzebnych regionów.
Kluczowe wnioski
Kluczowe wnioski:
- Używaj
connectionProxyDictionaryz kluczamikCFNetworkProxiesHTTP*ikCFNetworkProxiesHTTPS*do konfiguracji proxy HTTP/HTTPS w URLSession.- Klucze
kCFProxyUsernameKey/kCFProxyPasswordKeysą nielidalne w URLSession — używaj nagłówkaProxy-Authorization: Basiclub delegataurlSession(_:didReceive:).- SOCKS5 na porcie 1080 obsługuje natywnie autoryzację username/password przez
kCFStreamPropertySOCKSProxyUser/Password.- Geo-targeting i sticky sessions koduj w nazwie użytkownika:
user-country-US-city-newyork-session-abc123.- Implementuj retry z exponential backoff dla HTTP 429 i 5xx.
- Proxy HTTP nie narusza ATS — TLS jest tunelowane.
- Preferuj oficjalne API; używaj proxy tylko do publicznych danych i zgodnie z prawem.
FAQ
Czym jest używanie proxy w Swift?
Używanie proxy w Swift polega na skonfigurowaniu URLSessionConfiguration.connectionProxyDictionary z kluczami CFNetwork, aby kierować ruch HTTP/HTTPS lub SOCKS5 przez serwer proxy — np. bramę ProxyHat gate.proxyhat.com:8080. Pozwala to na ukrywanie IP, geo-targeting i omijanie blokad regionalnych w aplikacjach iOS i macOS.
Dlaczego używanie proxy w Swift jest ważne?
Proxy są kluczowe, gdy endpointy API blokują adresy IP z centrów danych (datacenter), nakładają limity zapytań lub ograniczają treści regionalnie. Bez proxy rezydencjalnego aplikacja iOS może otrzymywać kody 403/429 lub CAPTCHA. Proxy rezydencjalne pochodzą z prawdziwych ISP, co sprawia, że ruch wygląda organicznie i omija te blokady.
Który typ proxy najlepiej działa w Swift?
Proxy rezydencjalne (residential) są najlepsze do scrapingu i dostępu do treści regionalnie ograniczonych, ponieważ ich adresy IP pochodzą z ISP i nie są na listach blokad. Proxy datacenter są szybsze i tańsze, ale łatwo wykrywalne. SOCKS5 jest preferowany, gdy potrzebujesz natywnej autoryzacji username/password bez nagłówków. Proxy mobilne są najtrudniejsze do zablokowania, ale najdroższe.
Jak unikać blokad przy używaniu proxy w Swift?
Implementuj rotację sesji (zmienne sessionID), używaj sticky sessions tylko gdy potrzebujesz spójności IP, dodaj retry z exponential backoff dla HTTP 429/5xx, ogranicz współbieżność do 5–10 jednoczesnych żądań na sesję, ustaw realistyczne nagłówki User-Agent i opóźnienia między zapytaniami (np. 200–500ms). Konfiguruj timeoutIntervalForRequest na 15s, aby unikać zawieszeń.
Czy proxy HTTP w URLSession narusza App Transport Security?
Nie. Proxy HTTP działa jako tunel dla ruchu HTTPS — sesja TLS jest nawiązywana między aplikacją a serwerem docelowym, a proxy tylko przekazuje zaszyfrowane pakiety. ATS wymaga TLS 1.2+ dla połączenia końcowego, a nie dla proxy. Nie musisz wyłączać ATS, chyba że łączysz się z celem HTTP (nie HTTPS).






