Używanie proxy w Swift: Kompletny przewodnik po URLSession i residential proxy

Praktyczny przewodnik dla deweloperów iOS i macOS: konfiguracja proxy w URLSession, autoryzacja, geo-targeting, SOCKS5, async/await i wzorce produkcyjne z bramką ProxyHat.

Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

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.
CechaResidential proxyDatacenter proxyMobile proxy
Pochodzenie IPISP (prawdziwi użytkownicy)Serwery hostingoweSieci komórkowe
Ryzyko blokadyNiskieWysokieBardzo niskie
OpóźnienieŚrednie (50–200 ms)Niskie (10–50 ms)Wyższe (100–500 ms)
CenaŚredniaNiskaWysoka
Idealne doScraping, SERP, QAWewnętrzne API, testyApp 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

  • connectionProxyDictionary z kluczami CFNetwork to jedyny sposób na proxy w URLSession — kCFProxyUsernameKey jest niestabilny, więc używaj nagłówka Proxy-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.

Gotowy, aby zacząć?

Dostęp do ponad 50 mln rezydencjalnych IP w ponad 148 krajach z filtrowaniem AI.

Zobacz cenyProxy rezydencjalne
← Powrót do Bloga