From 01f98a523775d2e81ad2f871a4a0cf184edf4f69 Mon Sep 17 00:00:00 2001 From: Niklas Berglund Date: Tue, 30 Apr 2024 13:00:55 +0200 Subject: [PATCH] Implement packet capture client and models --- ios/Configurations/UITests.xcconfig.template | 5 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 30 ++- .../xcshareddata/swiftpm/Package.resolved | 22 -- .../Base/BaseUITestCase.swift | 61 +++++ .../External apps/SafariApp.swift | 27 +++ ios/MullvadVPNUITests/Info.plist | 2 + ios/MullvadVPNUITests/LeakTests.swift | 40 ++++ .../Networking/FirewallRule.swift | 10 +- .../Networking/Networking.swift | 8 +- .../Networking/PacketCaptureAPIClient.swift | 215 ++++++++++++++++++ 10 files changed, 383 insertions(+), 37 deletions(-) delete mode 100644 ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/MullvadVPNUITests/External apps/SafariApp.swift create mode 100644 ios/MullvadVPNUITests/LeakTests.swift create mode 100644 ios/MullvadVPNUITests/Networking/PacketCaptureAPIClient.swift diff --git a/ios/Configurations/UITests.xcconfig.template b/ios/Configurations/UITests.xcconfig.template index 030fe46d3129..5eb4ba364dac 100644 --- a/ios/Configurations/UITests.xcconfig.template +++ b/ios/Configurations/UITests.xcconfig.template @@ -19,7 +19,7 @@ AD_SERVING_DOMAIN = vpnlist.to // A domain which should be reachable. Used to verify Internet connectivity. Must be running a server on port 80. SHOULD_BE_REACHABLE_DOMAIN = mullvad.net -// Base URL for the firewall API, Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8 +// Base URL for the firewall API. Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8 FIREWALL_API_BASE_URL = http:/${}/8.8.8.8 // URL for Mullvad provided JSON data with information about the connection. https://am.i.mullvad.net/json for production, https://am.i.stagemole.eu/json for staging. @@ -27,3 +27,6 @@ AM_I_JSON_URL = https:/${}/am.i.stagemole.eu/json // Specify whether app logs should be extracted and attached to test report for failing tests ATTACH_APP_LOGS_ON_FAILURE = 0 + +// Base URL for the packet capture API. Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8 +PACKET_CAPTURE_BASE_URL = http:/${}/8.8.8.8 diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index bb540b34be0a..8b19f6fa1ad0 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -653,6 +653,7 @@ 8585CBE32BC684180015B6A4 /* EditAccessMethodPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8585CBE22BC684180015B6A4 /* EditAccessMethodPage.swift */; }; 8587A05D2B84D43100152938 /* ChangeLogAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */; }; 8590896F2B61763B003AF5F5 /* LoggedOutUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590896B2B61763B003AF5F5 /* LoggedOutUITestCase.swift */; }; + 85978A542BE0F10E00F999A7 /* PacketCaptureAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85978A532BE0F10E00F999A7 /* PacketCaptureAPIClient.swift */; }; 85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A42B872BB44D31007BABF7 /* DeviceManagementPage.swift */; }; 85B267612B849ADB0098E3CD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 85B267602B849ADB0098E3CD /* mullvad-api.h */; }; 85C7A2E92B89024B00035D5A /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C7A2E82B89024B00035D5A /* SettingsTests.swift */; }; @@ -660,6 +661,8 @@ 85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590896A2B61763B003AF5F5 /* BaseUITestCase.swift */; }; 85E3BDE52B70E18C00FA71FD /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E3BDE42B70E18C00FA71FD /* Networking.swift */; }; 85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */; }; + 85F1E17E2C0A256200DB8F55 /* LeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1E17D2C0A256200DB8F55 /* LeakTests.swift */; }; + 85F1E1812C0A2A0C00DB8F55 /* SafariApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1E1802C0A2A0C00DB8F55 /* SafariApp.swift */; }; 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0B2B6903990015DCED /* WelcomePage.swift */; }; 85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */; }; A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */; }; @@ -1995,11 +1998,14 @@ 859089692B61763B003AF5F5 /* LoggedInWithTimeUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedInWithTimeUITestCase.swift; sourceTree = ""; }; 8590896A2B61763B003AF5F5 /* BaseUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = ""; }; 8590896B2B61763B003AF5F5 /* LoggedOutUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedOutUITestCase.swift; sourceTree = ""; }; + 85978A532BE0F10E00F999A7 /* PacketCaptureAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketCaptureAPIClient.swift; sourceTree = ""; }; 85A42B872BB44D31007BABF7 /* DeviceManagementPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementPage.swift; sourceTree = ""; }; 85B267602B849ADB0098E3CD /* mullvad-api.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../../mullvad-api/include/mullvad-api.h"; sourceTree = ""; }; 85C7A2E82B89024B00035D5A /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; 85D039972BA4711800940E7F /* SettingsMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationTests.swift; sourceTree = ""; }; 85E3BDE42B70E18C00FA71FD /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; + 85F1E17D2C0A256200DB8F55 /* LeakTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeakTests.swift; sourceTree = ""; }; + 85F1E1802C0A2A0C00DB8F55 /* SafariApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariApp.swift; sourceTree = ""; }; 85FB5A0B2B6903990015DCED /* WelcomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage.swift; sourceTree = ""; }; 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionPage.swift; sourceTree = ""; }; A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountsProxy+Stubs.swift"; sourceTree = ""; }; @@ -3905,22 +3911,24 @@ 852969262B4D9C1F007EAD4C /* MullvadVPNUITests */ = { isa = PBXGroup; children = ( - 8518F6392B601910009EB113 /* Base */, - 85557B0C2B591B0F00795FE1 /* Networking */, - 852969312B4E9220007EAD4C /* Pages */, - 7A45CFCD2C08697100D80B21 /* Screenshots */, - 852969372B4ED20E007EAD4C /* Info.plist */, 8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */, 85B267602B849ADB0098E3CD /* mullvad-api.h */, + 852969372B4ED20E007EAD4C /* Info.plist */, 852969272B4D9C1F007EAD4C /* AccountTests.swift */, 85557B112B594FC900795FE1 /* ConnectivityTests.swift */, A9BFAFFE2BD004ED00F2BCA1 /* CustomListsTests.swift */, + 85F1E17D2C0A256200DB8F55 /* LeakTests.swift */, 850201DA2B503D7700EF8C96 /* RelayTests.swift */, 85D039972BA4711800940E7F /* SettingsMigrationTests.swift */, 85C7A2E82B89024B00035D5A /* SettingsTests.swift */, 8518F6392B601910009EB113 /* Test base classes */, 856952E12BD6B04C008C1F84 /* XCUIElement+Extensions.swift */, 85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */, + 8518F6392B601910009EB113 /* Base */, + 85F1E17F2C0A29FA00DB8F55 /* External apps */, + 85557B0C2B591B0F00795FE1 /* Networking */, + 852969312B4E9220007EAD4C /* Pages */, + 7A45CFCD2C08697100D80B21 /* Screenshots */, ); path = MullvadVPNUITests; sourceTree = ""; @@ -3972,10 +3980,19 @@ 85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */, 85E3BDE42B70E18C00FA71FD /* Networking.swift */, 856952DB2BD2922A008C1F84 /* PartnerAPIClient.swift */, + 85978A532BE0F10E00F999A7 /* PacketCaptureAPIClient.swift */, ); path = Networking; sourceTree = ""; }; + 85F1E17F2C0A29FA00DB8F55 /* External apps */ = { + isa = PBXGroup; + children = ( + 85F1E1802C0A2A0C00DB8F55 /* SafariApp.swift */, + ); + path = "External apps"; + sourceTree = ""; + }; A907639F2B2857D50045ADF0 /* Socks5 */ = { isa = PBXGroup; children = ( @@ -6084,6 +6101,7 @@ 8529693C2B4F0257007EAD4C /* Alert.swift in Sources */, 8542F7532BCFBD050035C042 /* SelectLocationFilterPage.swift in Sources */, 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */, + 85F1E1812C0A2A0C00DB8F55 /* SafariApp.swift in Sources */, 85D039982BA4711800940E7F /* SettingsMigrationTests.swift in Sources */, 85021CAE2BDBC4290098B400 /* AppLogsPage.swift in Sources */, 850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */, @@ -6108,9 +6126,11 @@ 85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */, 7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */, 856952DC2BD2922A008C1F84 /* PartnerAPIClient.swift in Sources */, + 85F1E17E2C0A256200DB8F55 /* LeakTests.swift in Sources */, 85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */, 855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */, 8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */, + 85978A542BE0F10E00F999A7 /* PacketCaptureAPIClient.swift in Sources */, 85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */, 8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */, 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */, diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 929180afa433..000000000000 --- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", - "version" : "1.4.0" - } - }, - { - "identity" : "wireguard-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mullvad/wireguard-apple.git", - "state" : { - "revision" : "15242e1698fc45261285d7417ed2cd5130d7332e" - } - } - ], - "version" : 2 -} diff --git a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift index a4b3a7d4e200..7bc013ed8a8c 100644 --- a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift +++ b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift @@ -31,6 +31,13 @@ class BaseUITestCase: XCTestCase { /// Default relay to use in tests static let testsDefaultRelayName = "se-got-wg-001" + /// True when the current test case is capturing packets + private var currentTestCaseShouldCapturePackets = false + + /// True when a packet capture session is active + private var packetCaptureSessionIsActive = false + private var packetCaptureSession: PacketCaptureSession? + // swiftlint:disable force_cast let displayName = Bundle(for: BaseUITestCase.self) .infoDictionary?["DisplayName"] as! String @@ -125,6 +132,29 @@ class BaseUITestCase: XCTestCase { } } + /// Start packet capture for this test case + func startPacketCapture() { + currentTestCaseShouldCapturePackets = true + packetCaptureSessionIsActive = true + let packetCaptureClient = PacketCaptureAPIClient() + packetCaptureSession = packetCaptureClient.startCapture() + } + + /// Stop the current packet capture and return captured data + func stopPacketCapture() -> [Stream] { + packetCaptureSessionIsActive = false + guard let packetCaptureSession else { + XCTFail("Trying to stop capture when there is no active capture") + return [] + } + + let packetCaptureAPIClient = PacketCaptureAPIClient() + packetCaptureAPIClient.stopCapture(session: packetCaptureSession) + let capturedData = packetCaptureAPIClient.getParsedCaptureObjects(session: packetCaptureSession) + + return capturedData + } + // MARK: - Setup & teardown /// Override this class function to change the uninstall behaviour in suite level teardown @@ -141,12 +171,43 @@ class BaseUITestCase: XCTestCase { /// Test level setup override func setUp() { + currentTestCaseShouldCapturePackets = false // Reset for each test case run continueAfterFailure = false app.launch() } /// Test level teardown override func tearDown() { + if currentTestCaseShouldCapturePackets { + guard let packetCaptureSession = packetCaptureSession else { + XCTFail("Packet capture session unexpectedly not set up") + return + } + + let packetCaptureClient = PacketCaptureAPIClient() + + // If there's a an active session due to cancelled/failed test run make sure to end it + if packetCaptureSessionIsActive { + packetCaptureSessionIsActive = false + packetCaptureClient.stopCapture(session: packetCaptureSession) + } + + packetCaptureClient.stopCapture(session: packetCaptureSession) + let pcap = packetCaptureClient.getPCAP(session: packetCaptureSession) + let parsedCapture = packetCaptureClient.getParsedCapture(session: packetCaptureSession) + self.packetCaptureSession = nil + + let pcapAttachment = XCTAttachment(data: pcap) + pcapAttachment.name = self.name + ".pcap" + pcapAttachment.lifetime = .keepAlways + self.add(pcapAttachment) + + let jsonAttachment = XCTAttachment(data: parsedCapture) + jsonAttachment.name = self.name + ".json" + jsonAttachment.lifetime = .keepAlways + self.add(jsonAttachment) + } + app.terminate() if let testRun = self.testRun, testRun.failureCount > 0, attachAppLogsOnFailure == true { diff --git a/ios/MullvadVPNUITests/External apps/SafariApp.swift b/ios/MullvadVPNUITests/External apps/SafariApp.swift new file mode 100644 index 000000000000..42843cd009df --- /dev/null +++ b/ios/MullvadVPNUITests/External apps/SafariApp.swift @@ -0,0 +1,27 @@ +// +// SafariApp.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-05-31. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class SafariApp { + let app = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari") + + func launch() { + app.launch() + } + + @discardableResult func tapAddressBar() -> Self { + app.textFields.firstMatch.tap() + return self + } + + @discardableResult func enterText(_ text: String) -> Self { + app.typeText(text) + return self + } +} diff --git a/ios/MullvadVPNUITests/Info.plist b/ios/MullvadVPNUITests/Info.plist index a81366999153..734f3fca1c21 100644 --- a/ios/MullvadVPNUITests/Info.plist +++ b/ios/MullvadVPNUITests/Info.plist @@ -24,6 +24,8 @@ $(NO_TIME_ACCOUNT_NUMBER) PartnerApiToken $(PARTNER_API_TOKEN) + PacketCaptureAPIBaseURL + $(PACKET_CAPTURE_BASE_URL) ShouldBeReachableDomain $(SHOULD_BE_REACHABLE_DOMAIN) TestDeviceIdentifier diff --git a/ios/MullvadVPNUITests/LeakTests.swift b/ios/MullvadVPNUITests/LeakTests.swift new file mode 100644 index 000000000000..fc6c5644b73c --- /dev/null +++ b/ios/MullvadVPNUITests/LeakTests.swift @@ -0,0 +1,40 @@ +// +// LeakTests.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-05-31. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class LeakTests: LoggedInWithTimeUITestCase { + /// For now just the skeleton of a leak test - traffic is captured and parsed, but not analyzed + func testLeaks() throws { + startPacketCapture() + + TunnelControlPage(app) + .tapSecureConnectionButton() + + allowAddVPNConfigurationsIfAsked() + + TunnelControlPage(app) + .waitForSecureConnectionLabel() + + // Trigger traffic by navigating to website in Safari + let safariApp = SafariApp() + safariApp.launch() + safariApp.tapAddressBar() + safariApp.enterText("mullvad.net\n") + + app.launch() + TunnelControlPage(app) + .tapDisconnectButton() + + // Keep the capture open for a while + Thread.sleep(forTimeInterval: 5.0) + let capturedTraffic = stopPacketCapture() + + // Analyze captured traffic + } +} diff --git a/ios/MullvadVPNUITests/Networking/FirewallRule.swift b/ios/MullvadVPNUITests/Networking/FirewallRule.swift index 84f9a5682046..1ea16b2619e2 100644 --- a/ios/MullvadVPNUITests/Networking/FirewallRule.swift +++ b/ios/MullvadVPNUITests/Networking/FirewallRule.swift @@ -9,22 +9,16 @@ import Foundation import XCTest -enum NetworkingProtocol: String { - case TCP = "tcp" - case UDP = "udp" - case ICMP = "icmp" -} - struct FirewallRule { let fromIPAddress: String let toIPAddress: String - let protocols: [NetworkingProtocol] + let protocols: [NetworkTransportProtocol] /// - Parameters: /// - fromIPAddress: Block traffic originating from this source IP address. /// - toIPAddress: Block traffic to this destination IP address. /// - protocols: Protocols which should be blocked. If none is specified all will be blocked. - private init(fromIPAddress: String, toIPAddress: String, protocols: [NetworkingProtocol]) { + private init(fromIPAddress: String, toIPAddress: String, protocols: [NetworkTransportProtocol]) { self.fromIPAddress = fromIPAddress self.toIPAddress = toIPAddress self.protocols = protocols diff --git a/ios/MullvadVPNUITests/Networking/Networking.swift b/ios/MullvadVPNUITests/Networking/Networking.swift index 6e75925d940a..2ac3a49c2d25 100644 --- a/ios/MullvadVPNUITests/Networking/Networking.swift +++ b/ios/MullvadVPNUITests/Networking/Networking.swift @@ -10,6 +10,12 @@ import Foundation import Network import XCTest +enum NetworkTransportProtocol: String, Codable { + case TCP = "tcp" + case UDP = "udp" + case ICMP = "icmp" +} + enum NetworkingError: Error { case notConfiguredError case internalError(reason: String) @@ -44,7 +50,7 @@ class Networking { interfaceAddress.sa_family == UInt8(AF_INET) { // Check if interface is en0 which is the WiFi connection on the iPhone let name = String(cString: interfacePointer.pointee.ifa_name) - if name == "en0" { + if name.hasPrefix("en") { // Convert interface address to a human readable string: var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) if getnameinfo( diff --git a/ios/MullvadVPNUITests/Networking/PacketCaptureAPIClient.swift b/ios/MullvadVPNUITests/Networking/PacketCaptureAPIClient.swift new file mode 100644 index 000000000000..d8307fe11a10 --- /dev/null +++ b/ios/MullvadVPNUITests/Networking/PacketCaptureAPIClient.swift @@ -0,0 +1,215 @@ +// +// PacketCaptureAPIClient.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-04-30. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +struct PacketCaptureSession { + var identifier = UUID().uuidString +} + +/// Represents a packet in packet capture +struct Packet: Codable { + let fromPeer: Bool + let timestamp: Int64 + + enum CodingKeys: String, CodingKey { + case fromPeer = "from_peer" + case timestamp + } +} + +/// Represents a stream in packet capture +struct Stream: Codable { + let peerAddr: String + let otherAddr: String + let flowID: String? + let transportProtocol: NetworkTransportProtocol + let packets: [Packet] + + enum CodingKeys: String, CodingKey { + case peerAddr = "peer_addr" + case otherAddr = "other_addr" + case flowID = "flow_id" + case transportProtocol = "transport_protocol" + case packets + } +} + +class PacketCaptureAPIClient { + // swiftlint:disable force_cast + let baseURL = URL( + string: Bundle(for: PacketCaptureAPIClient.self) + .infoDictionary?["PacketCaptureAPIBaseURL"] as! String + )! + // swiftlint:enable force_cast + + /// Start a new capture session + func startCapture() -> PacketCaptureSession { + let session = PacketCaptureSession() + + let jsonDictionary = [ + "label": session.identifier, + ] + + _ = sendRequest( + httpMethod: "POST", + endpoint: "capture", + contentType: "application/json", + jsonData: jsonDictionary + ) + + return session + } + + /// Stop capture for session + func stopCapture(session: PacketCaptureSession) { + _ = sendJSONRequest(httpMethod: "POST", endpoint: "stop-capture/\(session.identifier)", jsonData: nil) + } + + /// Get captured traffic from this session parsed to objects + func getParsedCaptureObjects(session: PacketCaptureSession) -> [Stream] { + let parsedData = getParsedCapture(session: session) + let decoder = JSONDecoder() + + do { + let streams = try decoder.decode([Stream].self, from: parsedData) + return streams + } catch { + XCTFail("Failed to decode parsed capture") + return [] + } + } + + /// Get captured traffic from this session parsed to JSON + func getParsedCapture(session: PacketCaptureSession) -> Data { + var deviceIPAddress: String + + do { + deviceIPAddress = try Networking.getIPAddress() + } catch { + XCTFail("Failed to get device IP address") + return Data() + } + + let responseData = sendJSONRequest( + httpMethod: "PUT", + endpoint: "parse-capture/\(session.identifier)", + jsonData: [deviceIPAddress] + ) + + return responseData + } + + /// Get PCAP file contents for the capture of this session + func getPCAP(session: PacketCaptureSession) -> Data { + let response = sendPCAPRequest(httpMethod: "GET", endpoint: "last-capture/\(session.identifier)", jsonData: nil) + return response + } + + private func sendJSONRequest(httpMethod: String, endpoint: String, jsonData: Any?) -> Data { + let responseData = sendRequest( + httpMethod: httpMethod, + endpoint: endpoint, + contentType: "application/json", + jsonData: jsonData + ) + + guard let responseData else { + XCTFail("Unexpectedly didn't get any data from JSON request") + return Data() + } + + return responseData + } + + private func sendPCAPRequest(httpMethod: String, endpoint: String, jsonData: Any?) -> Data { + let responseData = sendRequest( + httpMethod: httpMethod, + endpoint: endpoint, + contentType: "application/pcap", + jsonData: jsonData + ) + + guard let responseData else { + XCTFail("Unexpectedly didn't get any data from response") + return Data() + } + + XCTAssertFalse(responseData.isEmpty, "PCAP response data should not be empty") + + return responseData + } + + private func sendRequest(httpMethod: String, endpoint: String, contentType: String?, jsonData: Any?) -> Data? { + let url = baseURL.appendingPathComponent(endpoint) + + var request = URLRequest(url: url) + request.httpMethod = httpMethod + + if let contentType { + request.setValue(contentType, forHTTPHeaderField: "Content-Type") + } + + if let jsonData = jsonData { + do { + request.httpBody = try JSONSerialization.data(withJSONObject: jsonData) + } catch { + XCTFail("Failed to serialize JSON data") + } + } + + var requestResponse: URLResponse? + var requestError: Error? + var responseData: Data? + + let completionHandlerInvokedExpectation = XCTestExpectation( + description: "Completion handler for the request is invoked" + ) + + let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in + requestResponse = response + requestError = error + + guard let data = data, + let response = response as? HTTPURLResponse, + error == nil else { + XCTFail("Error: \(error?.localizedDescription ?? "Unknown error")") + return + } + + if 200 ... 204 ~= response.statusCode && error == nil { + responseData = data + } else { + XCTFail("Request failed") + } + + completionHandlerInvokedExpectation.fulfill() + } + + dataTask.resume() + + let waitResult = XCTWaiter.wait(for: [completionHandlerInvokedExpectation], timeout: 30) + + if waitResult != .completed { + XCTFail("Failed to send packet capture API request - timeout") + } else { + if let response = requestResponse as? HTTPURLResponse { + if (200 ... 201 ~= response.statusCode) == false { + XCTFail("Packet capture API request failed - unexpected server response") + } + } + + if let error = requestError { + XCTFail("Packet capture API request failed - encountered error \(error.localizedDescription)") + } + } + + return responseData + } +}