summaryrefslogtreecommitdiffhomepage
path: root/ios/PacketTunnelCore/IPC/AppMessageHandler.swift
blob: 7d05ed7b4460ed3e782d6cdeb039f4c9c21562ae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//
//  AppMessageHandler.swift
//  PacketTunnel
//
//  Created by pronebird on 19/09/2023.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadLogging
import MullvadREST

/**
 Actor handling packet tunnel IPC (app) messages and patching them through to the right facility.
 */
public struct AppMessageHandler {
    private let logger = Logger(label: "AppMessageHandler")
    private let packetTunnelActor: PacketTunnelActorProtocol
    private let urlRequestProxy: URLRequestProxyProtocol
    private let apiRequestProxy: APIRequestProxyProtocol

    public init(
        packetTunnelActor: PacketTunnelActorProtocol,
        urlRequestProxy: URLRequestProxyProtocol,
        apiRequestProxy: APIRequestProxyProtocol
    ) {
        self.packetTunnelActor = packetTunnelActor
        self.urlRequestProxy = urlRequestProxy
        self.apiRequestProxy = apiRequestProxy
    }

    /**
     Handle app message received via packet tunnel IPC.
    
     - Message data is expected to be a serialized `TunnelProviderMessage`.
     - Reply is expected to be wrapped in `TunnelProviderReply`.
     - Return `nil` in the event of error or when the call site does not expect any reply.
    
     Calls to reconnect and notify actor when private key is changed are meant to run in parallel because those tasks are serialized in `TunnelManager` and await
     the acknowledgment from IPC before starting next operation, hence it's critical to return as soon as possible.
     (See `TunnelManager.reconnectTunnel()`, `SendTunnelProviderMessageOperation`)
     */
    public func handleAppMessage(_ messageData: Data) async -> Data? {
        guard let message = decodeMessage(messageData) else { return nil }

        logger.debug("Received app message: \(message)")

        switch message {
        case let .sendURLRequest(request):
            return await encodeReply(urlRequestProxy.sendRequest(request))

        case let .sendAPIRequest(request):
            return await encodeReply(apiRequestProxy.sendRequest(request))

        case let .cancelURLRequest(id):
            urlRequestProxy.cancelRequest(identifier: id)
            return nil

        case let .cancelAPIRequest(id):
            apiRequestProxy.cancelRequest(identifier: id)
            return nil

        case .getTunnelStatus:
            return await encodeReply(packetTunnelActor.observedState)

        case .privateKeyRotation:
            packetTunnelActor.notifyKeyRotation(date: Date())
            return nil

        case let .reconnectTunnel(nextRelay):
            packetTunnelActor.reconnect(to: nextRelay, reconnectReason: ActorReconnectReason.userInitiated)
            // Instead of waiting for the UI process to send another `getTunnelStatus` message, reply immediately that the PacketTunnel is reconnecting
            guard let observedState = await packetTunnelActor.observedState.connectionState else { return nil }
            let reconnectingState = ObservedState.reconnecting(observedState)
            return encodeReply(reconnectingState)
        }
    }

    /// Deserialize `TunnelProviderMessage` or return `nil` on error. Errors are logged but ignored.
    private func decodeMessage(_ data: Data) -> TunnelProviderMessage? {
        do {
            return try TunnelProviderMessage(messageData: data)
        } catch {
            logger.error(error: error, message: "Failed to decode the app message.")
            return nil
        }
    }

    /// Encode `TunnelProviderReply` or return `nil` on error. Errors are logged but ignored.
    private func encodeReply<T: Codable>(_ reply: T) -> Data? {
        do {
            return try TunnelProviderReply(reply).encode()
        } catch {
            logger.error(error: error, message: "Failed to encode the app message reply.")
            return nil
        }
    }
}