Tüm Yazılar
KategoriTools
Okuma Süresi
9 dk
Yayın Tarihi
2026-10-15
Kelime Sayısı
1.290kelime

Kahveni hazırla - bu içerikli bir makale!

iOSDevelopmentTools 2.0: saklamadan önce maskeleyen uygulama içi network inspector

Özet

iOSDevelopmentTools 2.0, URLSession trafiğini URLProtocol ile nasıl kaydediyor, gizli değerleri saklamadan önce nasıl maskeliyor; redirect neden değişti.

  • Bir testçi "ödeme ekranında hata çıktı" dediğinde ilk soru genellikle şudur: uygulama ne gönderdi, sunucu ne döndü?
  • Proxy ya da delegate sarmalamak yerine URLProtocol.
  • Paketi Swift Package Manager ile ekleyin: swift .package(url: "https://github.com/muhittincamdali/iOSDevelopmentTools.git", from: "2.0.0") Ardından uygulamanın init'inde capture'ı başlatın ve konsolu kök view'a ekleyin.
  • Kaynak kod: [github.com/muhittincamdali/iOSDevelopmentTools](https://github.com/muhittincamdali/iOSDevelopmentTools)
iOSDevelopmentTools 2.0: saklamadan önce maskeleyen uygulama içi network inspector

İçindekiler


Problem

Bir testçi "ödeme ekranında hata çıktı" dediğinde ilk soru genellikle şudur: uygulama ne gönderdi, sunucu ne döndü? Xcode'a bağlı bir simülatörde bunu bulmak kolay. Başkasının elindeki bir test cihazında ise değil: debugger yok, konsol logu gitmiş, cihaza proxy kurmak da çoğu testçinin atmayacağı bir adım.

iOSDevelopmentTools bu bilgiyi uygulamanın içine koyuyor. 2.0.0 sürümü, uygulamanın URLSession ile yaptığı istekleri kaydediyor, log mesajlarını tutuyor, bir cihaz raporu topluyor ve üçünü de sallama, buton ya da pencereyle açılan bir SwiftUI konsolunda gösteriyor. Bir isteği açıp header'larını, gövdelerini ve sürelerini görebilir, ya da cURL komutu veya HAR dosyası olarak paylaşabilirsiniz.

Böyle bir aracın belirgin bir riski var: bir hata raporuna eklenen ekran görüntüsünde görmek istemeyeceğiniz verinin tam kendisini saklıyor. Giriş gövdeleri, Authorization header'ları, cookie'ler ve API anahtarları hep onun içinden geçiyor. Bu yüzden problemin ikinci yarısı, hassas hiçbir şeyin baştan saklanmaması.

Reponun geçmişi kısa ve bu konuda açık. Ağustos 2025'te network, storage, analytics ve utility modülleriyle açıldı; v1.0.0 Şubat 2026'da Alamofire tabanlı bir client üzerine etiketlendi. Haziran 2026'da bu modüller silindi ve yerine yer tutucular kondu; bunların arasında startLoading() metodu isteği hiç göndermeyen, yani yakaladığı her isteği askıda bırakan bir network interceptor da vardı (commit 34e9643). Ekim 2026'daki 2.0.0 yeniden kurulumu bunların hepsini kaldırdı ve işe redaction politikasından başladı (813eb7a, PR #19).

Tasarım kararları ve alternatifler

Proxy ya da delegate sarmalamak yerine URLProtocol. Harici bir proxy tüm trafiği görür ama cihazda sertifika ve ayarlanmış bir ağ ister. Session delegate'lerini sarmalamak yalnız sizin oluşturduğunuz session'larda işe yarar. URLProtocol.registerClass ile kaydedilen bir URLProtocol alt sınıfı URLSession.shared'ın isteklerini görür; bir configuration'ın protocolClasses listesine eklenmesi de diğer session'ları kapsar. NetworkCapture.start() ikisini birden yapar: CaptureProtocol'ü kaydeder ve URLSessionConfiguration'ın default ile ephemeral sınıf getter'larını, NetworkCapture.install(into:) çağıran sürümlerle değiş tokuş eder. Böylece URLSession üzerine kurulu kütüphaneler de kaydedilir. stop() bunları geri alır.

Protocol her isteği kopyalar, kopyayı maskeler ve onu saklar; asıl isteği ise tek bir dahili ForwardingSession üzerinden iletir. Sunucu, uygulamanın gönderdiğini alır; uygulamanın session'ı da yanıtı değişmeden alır. Bu tasarımın bedeli belgelenmiş: isteğe değil uygulamanın session'ına ait ayarlar (delegate'in server trust işleme mantığı, waitsForConnectivity, özel bir URLCache gibi) kaydedilen isteklere uygulanmaz. Background session'lar ve WebSocket task'ları hiç görülmez.

Göstermeden önce değil, saklamadan önce maskele. RedactionPolicy bir NetworkEntry oluşturulmadan önce çalışır; bu yüzden konsol, HTTPArchive dosyaları ve curlCommand() yalnız değiştirilmiş değerleri görür. Adlar eşleştirmeden önce normalize edilir (küçük harf, dört kata kadar percent-decode, NFKC, yalnız ASCII harf ve rakam); böylece X-API-Key, api_key ve API%5FKEY üçü de apikey parçasıyla eşleşir. Eşleştirme, fazla gizleme yönünde hata yapar: author adlı bir JSON anahtarı auth içerdiği için gizlenir.

Kontrol edilemeyeni saklama. Capture sınırında kesilmiş bir JSON gövdesi parse edilemez; ilk byte'larındaki bir sır bulunamaz. Varsayılan politika böyle bir gövdenin yalnız boyutunu saklar ve onu byte'larını tutmadan withheld olarak işaretler. Aynısı bozuk JSON, 64 seviyeden derin JSON, bir nesnenin aynı anahtarı tekrarladığı JSON ve Werkzeug gibi sunucuların katı bir parser'dan farklı okuduğu bir dizi multipart biçimi için de geçerli.

Sınırlı bellek. NetworkLogger varsayılan olarak en fazla 500 kayıt ve her gövdeden 256 KiB tutar; bu değerler 1–10.000 kayıt ve 0–16 MiB aralığına sıkıştırılır. İlk sürümlerden biri eski kayıtları Array.removeFirst ile atıyordu; bu, her istekte tüm geçmişi, capture thread'lerinin beklediği lock tutulurken kopyalıyordu. b7a73dc bunu sabit boyutlu bir ring buffer ile değiştirdi. 4 MiB üstü upload'lar belleğe okunmak yerine 64 KiB'lık parçalarla stream edilir.

Bağımlılık yok, Swift 6 strict concurrency. Konsol view'ları dışındaki her şey Sendable. 1.0.0'ın bağımlılıkları (Alamofire, SwiftLint, Quick, Nimble) kaldırıldı.

Gerçek bir hata: bazen başarısız olan redirect

Sürümdeki en öğretici düzeltme 244d9f1: "stop failing a load right after reporting its redirect".

CaptureProtocol önce Apple'ın CustomHTTPProtocol örneğini izliyordu. Bir redirect'te uygulamanın session'ına wasRedirectedTo çağırıyor, ardından kendi yüklemesini CocoaError(.userCancelled) ile başarısız sayıyordu. Session normalde bu hata gelmeden eski yüklemeyi durdurur, ama her zaman değil. Commit'e göre iOS simülatöründe redirect testinin yedi koşusundan birinde session.data 3072 hatası fırlattı; 2.000 redirect'lik bir döngü de macOS'ta üç koşunun üçünde başarısız oldu. Delegate'i redirect'i reddeden bir session ise redirect yanıtı yerine her seferinde bu hatayı alıyordu.

Yalın bir URLProtocol ile yapılan deneyler nedenini gösterdi. Redirect'i izleyen bir session eski yüklemeyi durdurur ve yenisini başlatır. Redirect'i reddeden bir delegate ise protocol'e hiçbir sinyal vermez. Protocol'ün redirect'in hemen ardından gönderdiği her şey, geçişten önce task'a ulaşabilir.

Düzeltme, redirect'i tek başına bildiriyor ve iletilen task'ın redirect yanıtına kadar devam etmesine izin veriyor; bu yanıtın disposition'ı bekletiliyor. Uygulamanın session'ı yüklemeyi durdurursa bekletilen yanıt iptal ediliyor. Yarım saniye (redirectDecisionTime) içinde durdurmazsa redirect reddedilmiş sayılıyor ve bekletilen yanıt, gövdesi ve sonu, capture yokmuş gibi uygulamaya gidiyor. Stub sunucu da gerçek bir sunucu gibi 302'yi bir gövdeyle gönderecek şekilde değiştirildi ve iki test eklendi: "Redirects reach the target every time, not a cancellation error" ve "A redirect the app's session refuses returns the redirect response". Commit, yeni testlerin eski kodda başarısız olduğunu not ediyor.

Birkaç commit boyunca süren ikinci bir ders daha var: ayraç aramak için Swift Character'ı yanlış birim. U+0301 gibi bir birleştirici işaret &, = ya da tırnağın hemen ardından gelirse onunla tek bir Character olur; Character üzerinde yapılan arama ayracı görmez, sunucu ise görür. e9d0b1e'den önce password=<U+0301>hunter2 biçimli bir form gövdesi olduğu gibi saklanıyordu; bbd81b6'dan önce ardından birleştirici işaret gelen bir tırnak curlCommand()'ın tırnaklamasından kaçıyor, böylece yapıştırılan komut bir header değerindeki metni çalıştırabiliyordu. fac086f aynı sorunu LogStore'un uzunluk sınırında buldu: bir harf ve bir milyon birleştirici işaretten oluşan mesaj tek Character sayılıyordu. Üçü de artık UTF-8 byte'ları ya da Unicode scalar'ları üzerinde çalışıyor.

Kullanım

Paketi Swift Package Manager ile ekleyin:

swift
1.package(url: "https://github.com/muhittincamdali/iOSDevelopmentTools.git", from: "2.0.0")

Ardından uygulamanın init'inde capture'ı başlatın ve konsolu kök view'a ekleyin. Bu blok README'nin quick start bölümünden birebir alındı; aynı kodu ReadmeSamples.swift üzerinden hem örnek uygulama hem test süiti derliyor:

swift
1@main
2struct MyApp: App {
3 init() {
4 #if DEBUG
5 NetworkCapture.start()
6 #endif
7 }
8 
9 var body: some Scene {
10 WindowGroup {
11 ContentView()
12 #if DEBUG
13 .devToolsConsoleOnShake()
14 #endif
15 }
16 }
17}

devToolsConsoleOnShake() yalnız iOS'ta var. macOS'ta README konsola kendi penceresini veriyor:

swift
1struct MacDebugScenes: Scene {
2 var body: some Scene {
3 Window("Developer Tools", id: "devtools") {
4 DevToolsConsole()
5 }
6 .keyboardShortcut("d", modifiers: [.command, .option])
7 }
8}

Bir hata raporu için saklanan kayıtlar doğrudan dışa aktarılabilir:

swift
1func exportForBugReport() throws -> (har: Data, curl: [String], device: String) {
2 let entries = NetworkLogger.shared.entries
3 let har = try HTTPArchive(entries: entries).jsonData()
4 let curl = entries.map { $0.curlCommand() }
5 let device = DeviceSnapshot.current().report()
6 return (har, curl, device)
7}

Kendi hassas adlarınızı eklemek için varsayılan parçaları genişletin: RedactionPolicy(nameFragments: RedactionPolicy.defaultNameFragments + ["iban", "ssn"]).

Test süitinde 148 Swift Testing testi var (v2.0.0 etiketinde macOS'ta swift test); CI süiti bir iOS simülatöründe de koşuyor. Network testleri istekleri bir uygulama session'ından capture üzerinden bir stub sunucuya gönderiyor ve sunucunun ne aldığını, uygulamanın ne aldığını ve neyin saklandığını kontrol ediyor.

Sınırlar

  • Yalnız debug ve test build'leri. Maskeleme bildiği sırları kaldırır, ama konsol yine de her isteği gösterir. "Keeping the tools out of release builds" adlı DocC makalesi #if DEBUG ya da paketi bağlayan ayrı bir internal target öneriyor.
  • Her şey kaydedilmez. Background session'lar, WebSocket task'ları, http ve https dışındaki şemalar ve yok sayılan host'lar yakalanmaz. Background trafiği NetworkLogger'ın record(request:response:body:error:startDate:endDate:) metoduyla elle eklenebilir.
  • Session ayarları taşınmaz. Session delegate'inizdeki sertifika pinning, özel cache ya da cookie deposu ve waitsForConnectivity kaydedilen isteklere uygulanmaz. Bunları test ederken o host'u yok sayın ya da capture'ı durdurun.
  • Maskelemenin sınırları var. Düz metin, XML ve yüklenen dosyalar kontrol edilmez. Path segmentleri, ;name=value parametreleri dışında kontrol edilmez. Hiçbir parçayı içermeyen bir adın altındaki sır saklanır. Log mesajları yazıldığı gibi saklanır.
  • Reddedilen redirect geç ulaşır. Session sinyal vermediği için redirect yanıtı, geldikten yarım saniye sonra uygulamaya ulaşır.
  • Platformlar. iOS 17 ve macOS 14, Swift 6.2. Konsol için tvOS ve visionOS desteği, açılışlar arası kalıcılık ve istek mock'lama yol haritasında; henüz yayında değil.

Kaynak kod: github.com/muhittincamdali/iOSDevelopmentTools

Etiketler

#swift#ios#urlsession#debugging#swiftui
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

12+ yıllık deneyime sahip Senior Mobile Engineer. iOS, Flutter ve React Native ile mobil ürünler geliştiriyorum; full-stack ve AI-native çalışıyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik