Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,20 @@ import WireNetwork
extension NetworkService {

static func make(backendEnvironment: BackendEnvironment, minTLSVersion: TLSVersion) -> NetworkService {
let service = NetworkService(
baseURL: backendEnvironment.url,
serverTrustValidator: ServerTrustValidator(
pinnedKeys: backendEnvironment.pinnedKeys,
currentDateProvider: .system
)
)

let config = URLSessionConfigurationFactory(
minTLSVersion: minTLSVersion,
proxySettings: backendEnvironment.proxySettings
).makeRESTAPISessionConfiguration()

let session = URLSession(configuration: config, delegate: service, delegateQueue: nil)
service.configure(with: session)

return service
return NetworkService(
baseURL: backendEnvironment.url,
urlSessionConfiguration: config,
serverTrustValidator: ServerTrustValidator(
pinnedKeys: backendEnvironment.pinnedKeys,
currentDateProvider: .system
)
)
}

}
12 changes: 5 additions & 7 deletions WireNetwork/Sources/WireNetwork/Assembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,11 @@ public final class Assembly {
authenticationManager: authenticationManager
)

public lazy var apiNetworkService: NetworkService = {
let service = NetworkService(baseURL: backendEnvironment.url, serverTrustValidator: serverTrustValidator)
let config = urlSessionConfigurationFactory.makeRESTAPISessionConfiguration()
let session = URLSession(configuration: config, delegate: service, delegateQueue: nil)
service.configure(with: session)
return service
}()
public lazy var apiNetworkService = NetworkService(
baseURL: backendEnvironment.url,
urlSessionConfiguration: urlSessionConfigurationFactory.makeRESTAPISessionConfiguration(),
serverTrustValidator: serverTrustValidator
)

public lazy var authenticationManager: some AuthenticationManagerProtocol = AuthenticationManager(
clientID: clientID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,52 +58,31 @@ extension NetworkService {

let restService = NetworkService(
baseURL: backendConfig.endpoints.restAPIURL,
urlSessionConfiguration: configFactory.makeRESTAPISessionConfiguration(),
serverTrustValidator: ServerTrustValidator(
pinnedKeys: backendConfig.pinnedKeys,
currentDateProvider: .system
)
)

let restSession = URLSession(
configuration: configFactory.makeRESTAPISessionConfiguration(),
delegate: restService,
delegateQueue: nil
)

restService.configure(with: restSession)

let webSocketService = NetworkService(
baseURL: backendConfig.endpoints.websocketURL,
urlSessionConfiguration: configFactory.makeWebSocketSessionConfiguration(),
serverTrustValidator: ServerTrustValidator(
pinnedKeys: backendConfig.pinnedKeys,
currentDateProvider: .system
)
)

let webSocketSession = URLSession(
configuration: configFactory.makeWebSocketSessionConfiguration(),
delegate: webSocketService,
delegateQueue: nil
)

webSocketService.configure(with: webSocketSession)

let blacklistService = NetworkService(
baseURL: backendConfig.endpoints.blacklistURL,
urlSessionConfiguration: configFactory.makeBlacklistSessionConfiguration(),
serverTrustValidator: ServerTrustValidator(
pinnedKeys: backendConfig.pinnedKeys,
currentDateProvider: .system
)
)

let blacklistSession = URLSession(
configuration: configFactory.makeBlacklistSessionConfiguration(),
delegate: blacklistService,
delegateQueue: nil
)

blacklistService.configure(with: blacklistSession)

return (
rest: restService,
webSocket: webSocketService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,27 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireLogging
public import Foundation

import WireLogging

// sourcery: AutoMockable
public protocol NetworkServiceProtocol {
public protocol NetworkServiceProtocol: Sendable {

func executeRequest(_ request: URLRequest) async throws -> (Data, HTTPURLResponse)

}

public final class NetworkService: NSObject, NetworkServiceProtocol {
public final class NetworkService: NetworkServiceProtocol {

let baseURL: URL
private let serverTrustValidator: ServerTrustValidator
private var urlSession: URLSession?
private let webSocketStore = WebSocketStore()

private let urlSession: URLSession
private let webSocketStore: WebSocketStore

public init(
baseURL: URL,
urlSessionConfiguration configuration: URLSessionConfiguration,
serverTrustValidator: ServerTrustValidator
) {
// Make sure the base url is a directory path,
Expand All @@ -55,22 +57,21 @@ public final class NetworkService: NSObject, NetworkServiceProtocol {
self.baseURL = baseURL
}

self.serverTrustValidator = serverTrustValidator
}
let webSocketStore = WebSocketStore()
self.webSocketStore = webSocketStore

deinit {
urlSession?.invalidateAndCancel()
let delegate = NetworkServiceSessionDelegate(
serverTrustValidator: serverTrustValidator,
webSocketStore: webSocketStore
)
self.urlSession = URLSession(configuration: configuration, delegate: delegate, delegateQueue: .none)
}

public func configure(with urlSession: URLSession) {
self.urlSession = urlSession
deinit {
urlSession.invalidateAndCancel()
}

public func executeRequest(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
guard let urlSession else {
throw NetworkServiceError.serviceNotConfigured
}

guard let url = request.url else {
throw NetworkServiceError.invalidRequest
}
Expand Down Expand Up @@ -98,10 +99,6 @@ public final class NetworkService: NSObject, NetworkServiceProtocol {
}

func executeWebSocketRequest(_ request: URLRequest) async throws -> WebSocket {
guard let urlSession else {
throw NetworkServiceError.serviceNotConfigured
}

guard let url = request.url else {
throw NetworkServiceError.invalidRequest
}
Expand All @@ -119,95 +116,3 @@ public final class NetworkService: NSObject, NetworkServiceProtocol {
}

}

extension NetworkService: URLSessionWebSocketDelegate {

public func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?
) {
WireLogger.network.debug("web socket task did open")
if let request = webSocketTask.currentRequest {
WireLogger.network.log(request)
}
}

public func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?
) {
WireLogger.network
.debug(
"web socket task did close. Close code: \(closeCode), Reason: \(String(data: reason ?? Data(), encoding: .utf8) ?? "No reason")"
)
Task {
await webSocketStore.retrieve(for: webSocketTask)?.close()
await webSocketStore.remove(for: webSocketTask)
}
}

}

extension NetworkService: URLSessionTaskDelegate {

public func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
// NOTE: This method is not called when when using async/await APIs.
if let error {
WireLogger.network.error("task did complete with error: \(error)")
} else {
WireLogger.network.debug("task did complete")
}
}

public func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge:
URLAuthenticationChallenge
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
let protectionSpace = challenge.protectionSpace

if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
guard let trust = challenge.protectionSpace.serverTrust else {
// If this is missing it is Apple breaking its API contract so crash.
fatalError("Missing server trust")
}

do {
try await serverTrustValidator.validate(trust: trust, host: protectionSpace.host)
return (.performDefaultHandling, challenge.proposedCredential)
} catch {
return (.cancelAuthenticationChallenge, nil)
}
} else {
return (.performDefaultHandling, challenge.proposedCredential)
}
}

}

// MARK: - WebSocketStore

/// Actor to manage web socket state safely across concurrent access
private actor WebSocketStore {
private var webSocketsByTask = [URLSessionWebSocketTask: WebSocket]()

func store(_ webSocket: WebSocket, for task: URLSessionWebSocketTask) {
webSocketsByTask[task] = webSocket
}

func retrieve(for task: URLSessionWebSocketTask) -> WebSocket? {
webSocketsByTask[task]
}

func remove(for task: URLSessionWebSocketTask) {
webSocketsByTask[task] = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

enum NetworkServiceError: Error {

case serviceNotConfigured
case invalidRequest
case notAHTTPURLResponse

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
import WireLogging

final class NetworkServiceSessionDelegate: NSObject, URLSessionWebSocketDelegate, URLSessionTaskDelegate, Sendable {

private let serverTrustValidator: ServerTrustValidator
private let webSocketStore: WebSocketStore

init(
serverTrustValidator: ServerTrustValidator,
webSocketStore: WebSocketStore
) {
self.serverTrustValidator = serverTrustValidator
self.webSocketStore = webSocketStore
super.init()
}

// MARK: - URLSessionWebSocketDelegate

func urlSession(
_ session: URLSession,

Check warning on line 39 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "session" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xvTcRR7BnB5F8l&open=AZ0lL2xvTcRR7BnB5F8l&pullRequest=4483
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?

Check warning on line 41 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "protocol" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xvTcRR7BnB5F8m&open=AZ0lL2xvTcRR7BnB5F8m&pullRequest=4483
) {
WireLogger.network.debug("web socket task did open")
if let request = webSocketTask.currentRequest {
WireLogger.network.log(request)
}
}

func urlSession(
_ session: URLSession,

Check warning on line 50 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "session" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xwTcRR7BnB5F8n&open=AZ0lL2xwTcRR7BnB5F8n&pullRequest=4483
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?
) {
WireLogger.network
.debug(
"web socket task did close. Close code: \(closeCode), Reason: \(String(data: reason ?? Data(), encoding: .utf8) ?? "No reason")"
)
Task {
await webSocketStore.retrieve(for: webSocketTask)?.close()
await webSocketStore.remove(for: webSocketTask)
}
}

// MARK: - URLSessionTaskDelegate

func urlSession(
_ session: URLSession,

Check warning on line 68 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "session" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xwTcRR7BnB5F8o&open=AZ0lL2xwTcRR7BnB5F8o&pullRequest=4483
task: URLSessionTask,

Check warning on line 69 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "task" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xwTcRR7BnB5F8p&open=AZ0lL2xwTcRR7BnB5F8p&pullRequest=4483
didCompleteWithError error: (any Error)?
) {
// NOTE: This method is not called when when using async/await APIs.
if let error {
WireLogger.network.error("task did complete with error: \(error)")
} else {
WireLogger.network.debug("task did complete")
}
}

func urlSession(
_ session: URLSession,

Check warning on line 81 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "session" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xwTcRR7BnB5F8q&open=AZ0lL2xwTcRR7BnB5F8q&pullRequest=4483
task: URLSessionTask,

Check warning on line 82 in WireNetwork/Sources/WireNetwork/Network/NetworkService/NetworkServiceSessionDelegate.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "task" or name it "_".

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ0lL2xwTcRR7BnB5F8r&open=AZ0lL2xwTcRR7BnB5F8r&pullRequest=4483
didReceive challenge:
URLAuthenticationChallenge
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {

let protectionSpace = challenge.protectionSpace
if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
guard let trust = challenge.protectionSpace.serverTrust else {
// If this is missing it is Apple breaking its API contract so crash.
fatalError("Missing server trust")
}

do {
try await serverTrustValidator.validate(trust: trust, host: protectionSpace.host)
return (.performDefaultHandling, challenge.proposedCredential)
} catch {
return (.cancelAuthenticationChallenge, nil)
}
} else {
return (.performDefaultHandling, challenge.proposedCredential)
}

}

}
Loading
Loading