aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Network/HotlineClient.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/Network/HotlineClient.swift')
-rw-r--r--Hotline/Network/HotlineClient.swift241
1 files changed, 241 insertions, 0 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift
new file mode 100644
index 0000000..d3b6c98
--- /dev/null
+++ b/Hotline/Network/HotlineClient.swift
@@ -0,0 +1,241 @@
+import Foundation
+import Network
+
+enum HotlineClientStatus: Int {
+ case disconnected
+ case connecting
+ case connected
+}
+
+class HotlineClient : ObservableObject {
+ static let shared = HotlineClient()
+
+ static let handshakePacket = Data([
+ 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID
+ 0x48, 0x4F, 0x54, 0x4C, // Sub-protocol ID
+ 0x00, 0x01, // Version
+ 0x00, 0x02, // Sub-version
+ ])
+
+ @Published var connectionStatus: HotlineClientStatus = .disconnected
+
+ var server: HotlineServer?
+ var connection: NWConnection?
+ var bytes = Data()
+ var handshakeComplete = false
+
+ init() {
+
+ }
+
+ func connect(to server: HotlineServer) {
+ self.server = server
+
+ let serverAddress = NWEndpoint.Host(server.address)
+ let serverPort = NWEndpoint.Port(rawValue: server.port)!
+
+ self.connection = NWConnection(host: serverAddress, port: serverPort, using: .tcp)
+ self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
+ switch newState {
+ case .ready:
+ print("HotlineClient: connection ready!")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .connected
+ }
+ self?.sendHandshake()
+ case .cancelled:
+ print("HotlineClient: connection cancelled")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
+ case .failed(let err):
+ print("HotlineClient: connection error \(err)")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
+ default:
+ print("HotlineClient: unhandled connection state \(newState)")
+ }
+ }
+
+ DispatchQueue.main.async {
+ self.connectionStatus = .connecting
+ }
+ self.connection?.start(queue: .global())
+ }
+
+ private func disconnect() {
+ guard let c = connection else {
+ print("HotlineClient: already disconnected")
+ return
+ }
+
+ c.cancel()
+ self.connection = nil
+ }
+
+ private func sendHandshake() {
+ guard let c = connection else {
+ print("HotlineClient: invalid connection to send handshake.")
+ return
+ }
+
+ c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in
+ if let err = error {
+ print("HotlineClient: sending magic failed \(err)")
+ return
+ }
+
+ print("HotlineClient: sent handshake packet!")
+
+ self?.receiveHandshake()
+ })
+ }
+
+ private func receiveHandshake() {
+ guard let c = connection else {
+ print("HotlineTracker: invalid connection to receive magic.")
+ return
+ }
+
+ print("HotlineClient: receiving handshake...")
+ c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in
+ guard let self = self, let data = data else {
+ return
+ }
+
+ if data.isEmpty {
+ print("HotlineClient: empty handshake response")
+ self.disconnect()
+ return
+ }
+
+ let protocolID = data.readUInt32(at: 0)!
+ if protocolID != 0x54525450 { // 'TRTP'
+ print("HotlineClient: invalid handshake protocol ID \(protocolID)")
+ self.disconnect()
+ return
+ }
+
+ let errorCode = data.readUInt32(at: 4)!
+ if errorCode != 0 { // 0 == no error
+ print("HotlineClient: handshake error", errorCode)
+ self.disconnect()
+ return
+ }
+
+ print("HotlineClient: completed handshake")
+ self.sendLogin()
+ }
+ }
+
+ private func sendLogin() {
+ guard let c = connection else {
+ print("HotlineClient: no connection for transaction.")
+ return
+ }
+
+ c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in
+ if let err = error {
+ print("HotlineClient: sending login failed \(err)")
+ return
+ }
+
+ print("HotlineClient: sent handshake packet!")
+
+
+ })
+ }
+
+ private func receiveTransaction() {
+ guard let c = connection else {
+ print("HotlineClient: no connection for transaction.")
+ return
+ }
+
+ print("HotlineClient: waiting on transaction header...")
+ c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (data, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ if let error = error {
+ print("HotlineClient: receive error \(error)")
+ self.disconnect()
+ return
+ }
+
+ if let data = data, !data.isEmpty {
+ print("HotlineClient: received \(data.count) header bytes")
+
+ let transaction = self.parseTransaction(data: data)
+ if var t = transaction {
+ if t.dataSize > 0 {
+ c.receive(minimumIncompleteLength: Int(t.dataSize), maximumLength: Int(t.dataSize)) { [weak self] (data, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ if let data = data, !data.isEmpty {
+ t.parameterCount = data.readUInt16(at: 0)!
+
+ if t.parameterCount > 0 {
+ t.parameters = []
+ var dataCursor = 2
+ for _ in 0..<t.parameterCount {
+ if
+ let fieldID = data.readUInt16(at: dataCursor),
+ let fieldSize = data.readUInt16(at: dataCursor + 2),
+ let fieldData = data.readData(at: dataCursor + 4, length: Int(fieldSize)) {
+ t.parameters?.append(HotlineTransactionParameter(id: fieldID, dataSize: fieldSize, data: fieldData))
+
+ dataCursor += 4 + Int(fieldSize)
+ }
+ }
+ }
+ }
+
+ self.processTransaction(t)
+ }
+ }
+ else {
+ self.processTransaction(t)
+ }
+ }
+
+// print("HotlineTracker: server count = \(self.serverCount)")
+ }
+
+// self.receiveListing()
+// }
+ }
+ }
+
+ private func processTransaction(_ transaction: HotlineTransaction) {
+ print("HotlineClient processing transaction \(transaction.type) with \(transaction.parameterCount) parameters")
+ }
+
+ private func parseTransaction(data: Data) -> HotlineTransaction? {
+ if
+ let flags = data.readUInt8(at: 0),
+ let isReply = data.readUInt8(at: 1),
+ let type = data.readUInt16(at: 2),
+ let id = data.readUInt32(at: 4),
+ let errorCode = data.readUInt32(at: 8),
+ let transactionSize = data.readUInt32(at: 12),
+ let dataSize = data.readUInt32(at: 16) {
+
+ return HotlineTransaction(
+ flags: flags,
+ isReply: isReply,
+ type: HotlineTransactionType(rawValue: type) ?? HotlineTransactionType.unknown,
+ id: id,
+ errorCode: errorCode,
+ totalSize: transactionSize,
+ dataSize: dataSize
+ )
+ }
+
+ return nil
+ }
+}