Wprowadzenie do używania proxy w Swift
Używanie proxy w Swift to umiejętność, której potrzebują wszyscy deweloperzy iOS i macOS, gdy ich aplikacje muszą pobierać dane z endpointów, które blokują adresy IP z centrów danych lub ograniczają dostęp regionalny. Apple nie dostarcza wysokopoziomowego API proxy — jedynym sposobem jest ręczna konfiguracja URLSessionConfiguration.connectionProxyDictionary lub implementacja własnego URLProtocol. W tym przewodniku pokazujemy, jak skonfigurować residential proxy w URLSession, obsłużyć autoryzację, geo-targeting i SOCKS5, a także jak zbudować produkcyjny potok pobierania danych z async/await.
Dla kontekstu: ruch z adresów IP centrów danych jest rutynowo blokowany przez serwery CDN takie jak Cloudflare, które według danych rynkowych chronią około 20% wszystkich stron internetowych. Residential proxy rozwiązują ten problem, ponieważ pochodzą z adresów przydzielonych rzeczywistym dostawcom ISP, co sprawia, że ruch wygląda jak naturalny ruch użytkownika.
Dlaczego potrzebujesz residential proxy w aplikacjach Swift
Endpointy API i serwery WWW stosują różne mechanizmy obrony: limitów zapytań na IP, blokad geograficznych, weryfikacji typu połączenia (datacenter vs residential) oraz systemów anti-bot. Gdy Twoja aplikacja iOS wykonuje zapytania z serwera backendowego lub gdy testujesz endpointy z symulatora, ruch może pochodzić z IP centrum danych.
Residential proxy są szczególnie przydatne w trzech scenariuszach:
- SERP tracking i web scraping — wyszukiwarki i platformy e-commerce blokują zautomatyzowane zapytania z datacenter IP. Zobacz nasze przypadki użycia SERP tracking.
- Treści regionalnie zablokowane — streaming, sklepy i API zwracają różne dane w zależności od kraju. Geo-targetowanie proxy pozwala symulować lokalizację użytkownika.
- Testy QA — weryfikacja, czy aplikacja poprawnie obsługuje odpowiedzi z różnych regionów.
| Cecha | Residential proxy | Datacenter proxy | Mobile proxy |
|---|---|---|---|
| Pochodzenie IP | ISP (prawdziwi użytkownicy) | Serwery hostingowe | Sieci komórkowe |
| Ryzyko blokady | Niskie | Wysokie | Bardzo niskie |
| Opóźnienie | Średnie (50–200 ms) | Niskie (10–50 ms) | Wyższe (100–500 ms) |
| Cena | Średnia | Niska | Wysoka |
| Idealne do | Scraping, SERP, QA | Wewnętrzne API, testy | App store, social media |
Podstawowa konfiguracja HTTP proxy w URLSession
Swift i Foundation oferują URLSessionConfiguration.connectionProxyDictionary, który akceptuje klucze z CFNetwork. Aby skierować ruch przez bramkę ProxyHat, ustawiamy kCFNetworkProxiesHTTPEnable, kCFNetworkProxiesHTTPProxy i kCFNetworkProxiesHTTPPort — oraz odpowiedniki HTTPS.
import Foundation
func makeProxySession(username: String, password: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
// HTTP proxy
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort: 8080,
// HTTPS proxy (CONNECT tunnel)
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort: 8080
] as [String: Any]
return URLSession(configuration: config)
}
Powyższy kod kieruje cały ruch HTTP i HTTPS przez gate.proxyhat.com:8080. Konfiguracja ephemeral zapobiega zapisywaniu ciasteczek i pamięci podręcznej na dysku, co jest istotne przy scrapingu.
Autoryzacja i geo-targeting — Proxy-Authorization
Klucze kCFProxyUsernameKey i kCFProxyPasswordKey są niestabilne w URLSession na iOS i macOS — bywają ignorowane lub powodują podwójne wyzwania 407. Niezawodnym podejściem jest ręczne dodanie nagłówka Proxy-Authorization: Basic do każdego zapytania.
ProxyHat koduje flagi geo-targeting i sesji w nazwie użytkownika. Na przykład user-country-US-city-newyork-session-abc123 kieruje ruch przez Nowy Jork z sesją sticky o identyfikatorze abc123.
import Foundation
struct ProxyAuth {
let username: String // np. "user-country-US-city-newyork-session-abc123"
let password: String
/// Nagłówek Proxy-Authorization w formacie Basic
var headerValue: String {
let raw = "\(username):\(password)"
let data = raw.data(using: .utf8) ?? Data()
return "Basic " + data.base64EncodedString()
}
}
func makeAuthorizedRequest(url: URL, auth: ProxyAuth) -> URLRequest {
var request = URLRequest(url: url, timeoutInterval: 30)
request.setValue(auth.headerValue, forHTTPHeaderField: "Proxy-Authorization")
return request
}
// Przykład użycia
let auth = ProxyAuth(
username: "user-country-DE-city-berlin-session-sess42",
password: "twoje_haslo"
)
let request = makeAuthorizedRequest(
url: URL(string: "https://api.example.com/data")!,
auth: auth
)
Alternatywa: URLSessionDelegate i wyzwanie 407
Jeśli wolisz obsłużyć wyzwanie 407 przez delegata, zaimplementuj urlSession(_:didReceive:completionHandler:). To podejście jest bardziej idiomatyczne, ale wymaga, aby serwer proxy wysłał nagłówek Proxy-Authenticate.
import Foundation
final class ProxyChallengeHandler: 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
) {
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodHTTPBasic,
NSURLAuthenticationMethodDefault:
let credential = URLCredential(
user: username,
password: password,
persistence: .forSession
)
completionHandler(.useCredential, credential)
default:
completionHandler(.performDefaultHandling, nil)
}
}
}
// Użycie
let delegate = ProxyChallengeHandler(
username: "user-country-US-session-xyz789",
password: "twoje_haslo"
)
let session = URLSession(
configuration: .ephemeral,
delegate: delegate,
delegateQueue: nil
)
Oba podejścia działają, ale nagłówek Proxy-Authorization jest bardziej przewidywalny i działa także wtedy, gdy serwer proxy nie wysyła wyzwania 407. Pełną listę dostępnych lokalizacji znajdziesz na stronie lokalizacje ProxyHat.
SOCKS5 proxy na porcie 1080
ProxyHat obsługuje również SOCKS5 na porcie 1080. W connectionProxyDictionary używasz kluczy kCFStreamPropertySOCKSProxy*.
import Foundation
func makeSOCKS5Session(username: String, password: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFNetworkProxiesSOCKSEnable: true,
kCFNetworkProxiesSOCKSProxy: "gate.proxyhat.com",
kCFNetworkProxiesSOCKSPort: 1080,
kCFNetworkProxiesSOCKSVersion: kCFStreamSocketSOCKSVersion5 as Any
] as [String: Any]
return URLSession(configuration: config)
}
// Format URL SOCKS5: socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080
// Np. socks5://user-country-FR-session-par01:pass@gate.proxyhat.com:1080
SOCKS5 jest przydatny, gdy potrzebujesz pełnego tunelowania TCP (nie tylko HTTP/HTTPS) lub gdy endpoint wymaga proxy, które nie modyfikuje nagłówków HTTP. Pamiętaj, że autoryzacja SOCKS5 z nazwą użytkownika i hasłem jest obsługiwana przez klucze CFNetwork, ale — podobnie jak przy HTTP — warto testować na rzeczywistych urządzeniach.
Async/await z residential proxy i TaskGroup
W nowoczesnym Swift (iOS 15+, macOS 12+) używamy async/await z URLSession.shared.data(for:). Poniższy przykład pobiera dane z wielu endpointów równolegle przez proxy, dekoduje JSON przez Codable i używa TaskGroup do kontroli współbieżności.
import Foundation
struct Product: Codable {
let id: Int
let name: String
let price: Double
}
struct ProxyConfig {
let host = "gate.proxyhat.com"
let port = 8080
let username: String
let password: String
}
actor ProductFetcher {
let proxy: ProxyConfig
init(proxy: ProxyConfig) {
self.proxy = proxy
}
/// Konfiguruje sesję z proxy i nagłówkiem autoryzacji
private func makeSession() -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: proxy.host,
kCFNetworkProxiesHTTPPort: proxy.port,
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: proxy.host,
kCFNetworkProxiesHTTPSPort: proxy.port
] as [String: Any]
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 120
return URLSession(configuration: config)
}
/// Pobiera jeden produkt przez proxy
func fetchProduct(id: Int) async throws -> Product {
let session = makeSession()
let auth = ProxyAuth(username: proxy.username, password: proxy.password)
var request = URLRequest(
url: URL(string: "https://api.example.com/products/\(id)")!
)
request.setValue(auth.headerValue, forHTTPHeaderField: "Proxy-Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse,
200..<300 ~= http.statusCode else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(Product.self, from: data)
}
/// Pobiera wiele produktów równolegle z limitem współbieżności
func fetchProducts(ids: [Int], concurrency: Int = 5) async -> [Product] {
await withTaskGroup(of: Product?.self, returning: [Product].self) { group in
var results: [Product] = []
var iterator = ids.makeIterator()
// Start: inicjujemy `concurrency` zadań
for _ in 0..<concurrency {
guard let id = iterator.next() else { break }
group.addTask { try? await self.fetchProduct(id: id) }
}
// Pobieramy wyniki i dodajemy nowe zadania
for await result in group {
if let product = result {
results.append(product)
}
if let id = iterator.next() {
group.addTask { try? await self.fetchProduct(id: id) }
}
}
return results
}
}
}
// Użycie
let proxy = ProxyConfig(
username: "user-country-US-city-newyork-session-batch01",
password: "twoje_haslo"
)
Task {
let fetcher = ProductFetcher(proxy: proxy)
let products = await fetcher.fetchProducts(ids: Array(1...50), concurrency: 5)
print("Pobrano \(products.count) produktów")
}
Wzorzec TaskGroup z ograniczeniem współbieżności do 5 jednoczesnych zapytań zapobiega przeciążeniu serwera i zmniejsza ryzyko blokady. W środowisku produkcyjnym zwiększaj tę wartość stopniowo, monitorując wskaźnik sukcesu.
Wskazówki produkcyjne
TLS i URLSessionDelegate
Przy użyciu proxy HTTPS (CONNECT tunnel), URLSession nawiązuje tunel TLS bezpośrednio z serwerem docelowym — certyfikaty są weryfikowane normalnie. Jeśli jednak potrzebujesz dostosować obsługę TLS (np. pinning certyfikatów), zaimplementuj urlSession(_:didReceive:completionHandler:) dla NSURLAuthenticationMethodServerTrust.
import Foundation
final class TLSPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// Tu: weryfikacja trust — np. porównanie z zapisanym certyfikatem
// Dla uproszczenia akceptujemy systemowy trust
completionHandler(.useCredential, URLCredential(trust: trust))
}
}
Ponowienia z wykładniczym wycofywaniem
Sieci i proxy mogą zawodzić. Implementuj ponowienia z wykładniczym wycofywaniem (exponential backoff), aby zwiększyć niezawodność bez przeciążania infrastruktury.
import Foundation
enum RetryError: Error { case maxRetriesExceeded }
func fetchWithRetry(
request: URLRequest,
session: URLSession,
maxRetries: Int = 3,
baseDelay: UInt64 = 500_000_000 // 500 ms w nanosekundach
) async throws -> Data {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
let (data, response) = try await session.data(for: request)
if let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode {
return data
}
// 429 lub 5xx — ponów
if let http = response as? HTTPURLResponse,
http.statusCode == 429 || (500..<600).contains(http.statusCode) {
lastError = URLError(.badServerResponse)
} else {
return data // zwróć mimo błędu — niech dekoder zdecyduje
}
} catch {
lastError = error
}
// Wykładnicze wycofywanie: 500ms, 1000ms, 2000ms
let delay = baseDelay * UInt64(1 << attempt)
try await Task.sleep(nanoseconds: delay)
}
throw lastError ?? RetryError.maxRetriesExceeded
}
// Użycie
let session = URLSession(configuration: .ephemeral)
var request = URLRequest(url: URL(string: "https://api.example.com/data")!)
request.setValue("Basic ...", forHTTPHeaderField: "Proxy-Authorization")
Task {
do {
let data = try await fetchWithRetry(request: request, session: session)
print("Pobrano \(data.count) bajtów")
} catch {
print("Błąd po ponowieniach: \(error)")
}
}
App Transport Security (ATS)
ATS na iOS wymaga TLS 1.2+ dla wszystkich połączeń. Połączenia przez proxy HTTP CONNECT są zgodne z ATS, ponieważ tunel TLS jest nawiązywany z serwerem docelowym. Jeśli jednak używasz czystego HTTP (nie HTTPS) przez proxy, musisz dodać wyjątek ATS w Info.plist — co Apple zaleca ograniczać do absolutnie niezbędnych przypadków.
Prywatność na urządzeniu
Na iOS 14+ aplikacje muszą deklarować NSPrivacyTracking i NSPrivacyCollectedDataTypes w PrivacyInfo.xcprivacy. Jeśli Twoja aplikacja używa proxy do pobierania danych publicznych i nie śledzi użytkowników, upewnij się, że manifest prywatności to odzwierciedla. Proxy nie zbiera danych użytkownika aplikacji — służy jedynie jako pośrednik sieciowy.
ProxyHat SDK vs ręczna konfiguracja
ProxyHat oferuje SDK dla Python i Node.js, które odzwierciedlają tę samą bramkę gate.proxyhat.com. W Swift nie ma oficjalnego SDK, ale konfiguracja ręczna opisana powyżej jest prosta i daje pełną kontrolę. Jeśli Twój backend jest w Python lub Node, możesz użyć SDK do scrapingu po stronie serwera, a w aplikacji iOS jedynie wyświetlać dane.
Sprawdź dokumentację ProxyHat po szczegóły integracji backendowej oraz cennik ProxyHat.
Etyka i aspekty prawne
Web scraping i używanie proxy wiążą się z odpowiedzialnością prawną i etyczną:
- Computer Fraud and Abuse Act (CFAA) — w USA nieautoryzowany dostęp do chronionych systemów może naruszać CFAA. Pobieranie publicznie dostępnych danych zazwyczaj nie jest nielegalne, ale omijanie mechanizmów uwierzytelniania lub kontroli dostępu już tak. Zobacz tekst ustawy CFAA.
- RODO/GDPR — w UE przetwarzanie danych osobowych pobranych z sieci podlega GDPR. Jeśli pobierasz dane osobowe, potrzebujesz podstawy prawnej.
- Wytyczne App Store — Apple zabrania aplikacji, które zbierają dane bez zgody użytkownika lub naruszają warunki korzystania z usług innych platform. Upewnij się, że Twoja aplikacja nie narusza
App Store Review Guidelines. - Preferuj oficjalne API — jeśli platforma udostępnia API, używaj go zamiast scrapingu. To bardziej niezawodne i zgodne z prawem.
Kluczowa zasada: scrapuj tylko publicznie dostępne dane, szanuj
robots.txt, ograniczaj częstotliwość zapytań i używaj proxy etycznie — nie do omijania płatnych ścian ani mechanizmów uwierzytelniania.
Kluczowe wnioski
connectionProxyDictionaryz kluczami CFNetwork to jedyny sposób na proxy w URLSession —kCFProxyUsernameKeyjest niestabilny, więc używaj nagłówkaProxy-Authorization.- Geo-targeting i sesje sticky koduje się w nazwie użytkownika:
user-country-US-city-newyork-session-abc123. - SOCKS5 na porcie 1080 jest dostępny przez klucze
kCFNetworkProxiesSOCKS*. - Async/await z TaskGroup i limitem współbieżności (np. 5) to wzorzec produkcyjny do równoległego scrapingu.
- Ponowienia z wykładniczym wycofywaniem zwiększają niezawodność bez przeciążania infrastruktury.
- Residential proxy są potrzebne, gdy endpointy blokują IP z centrów danych lub ograniczają dostęp regionalny.
- Zawsze preferuj oficjalne API i przestrzegaj przepisów CFAA, GDPR i wytycznych App Store.
Sprawdź również nasze przypadki użycia web scraping, aby zobaczyć, jak zespoły stosują ProxyHat w praktyce.






