aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Network/HotlineClient.swift
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-11-29 20:13:23 -0800
committerDustin Mierau <dustin@mierau.me>2023-11-29 20:13:23 -0800
commit30255476781789df318a571622ab732681696139 (patch)
tree7e05705c09c1a455ca6573af686f9eb0629df894 /Hotline/Network/HotlineClient.swift
parent2bdcc0c3d751d7c7626d8b0e9f79331b803c668b (diff)
Further work on protocol.
Diffstat (limited to 'Hotline/Network/HotlineClient.swift')
-rw-r--r--Hotline/Network/HotlineClient.swift364
1 files changed, 242 insertions, 122 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift
index d3b6c98..5193fe5 100644
--- a/Hotline/Network/HotlineClient.swift
+++ b/Hotline/Network/HotlineClient.swift
@@ -5,6 +5,8 @@ enum HotlineClientStatus: Int {
case disconnected
case connecting
case connected
+ case loggingIn
+ case loggedIn
}
class HotlineClient : ObservableObject {
@@ -16,25 +18,37 @@ class HotlineClient : ObservableObject {
0x00, 0x01, // Version
0x00, 0x02, // Sub-version
])
-
+
@Published var connectionStatus: HotlineClientStatus = .disconnected
+ @Published var agreement: String?
+ @Published var userList: [HotlineUser] = []
+ @Published var chatMessages: [String] = []
+
+ let userName: String = "bolt"
+ let userIconID: UInt32 = 128
var server: HotlineServer?
var connection: NWConnection?
- var bytes = Data()
- var handshakeComplete = false
init() {
}
+ // MARK: -
+
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)
+ let tcpOptions = NWProtocolTCP.Options()
+ tcpOptions.enableKeepalive = true
+ tcpOptions.keepaliveInterval = 30
+ let connectionParameters: NWParameters
+ connectionParameters = NWParameters(tls: nil, tcp: tcpOptions)
+
+ self.connection = NWConnection(host: serverAddress, port: serverPort, using: connectionParameters)
self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
switch newState {
case .ready:
@@ -65,85 +79,28 @@ class HotlineClient : ObservableObject {
}
private func disconnect() {
- guard let c = connection else {
- print("HotlineClient: already disconnected")
- return
- }
-
- c.cancel()
+ self.connection?.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()
+ DispatchQueue.main.async {
+ self.connectionStatus = .disconnected
}
}
- private func sendLogin() {
+ // MARK: -
+
+ private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) {
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)")
+ c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in
+ if disconnectOnError, error != nil {
+ self?.disconnect()
return
}
- print("HotlineClient: sent handshake packet!")
-
-
+ callback?()
})
}
@@ -153,67 +110,86 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: waiting on transaction header...")
- c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (data, context, isComplete, error) in
+ print("HotlineClient: waiting for transaction...")
+ c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (headerData, context, isComplete, error) in
guard let self = self else {
return
}
if let error = error {
- print("HotlineClient: receive error \(error)")
+ print("HotlineClient: transaction 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)
- }
+ guard let headerData = headerData, !headerData.isEmpty else {
+ self.receiveTransaction()
+ return
+ }
+
+ print("HotlineClient: received \(headerData.count) header bytes")
+
+ if var transaction = self.parseTransaction(data: headerData) {
+ // Receive additional data if the transaction has data attached to it.
+ print("DATA SIZE: \(transaction.dataSize)")
+ if transaction.dataSize > 0 {
+ c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (parameterData, context, isComplete, error) in
+ guard let self = self else {
+ return
+ }
+
+ guard let parameterData = parameterData, !parameterData.isEmpty else {
+ print("HotlineClient: transaction parameter data is empty!")
+ self.disconnect()
+ return
+ }
+
+ let parameterCount = parameterData.readUInt16(at: 0)!
+
+ if parameterCount > 0 {
+ var dataCursor = 2
+ for _ in 0..<parameterCount {
+ if
+ let fieldID = parameterData.readUInt16(at: dataCursor),
+ let fieldSize = parameterData.readUInt16(at: dataCursor + 2),
+ let fieldData = parameterData.readData(at: dataCursor + 4, length: Int(fieldSize)) {
+
+ if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) {
+ transaction.parameters.append(HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData))
+// transaction.parameters[fieldType] = HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData)
+ }
+ else {
+ print("HotlineClient: UNKNOWN PARAM TYPE!", fieldID, fieldSize)
}
+
+ dataCursor += 4 + Int(fieldSize)
}
}
- self.processTransaction(t)
+ // Process the transaction if we have processed more than zero parameters here
+ // as we expect parameters at this point.
+ self.processTransaction(transaction)
}
- }
- else {
- self.processTransaction(t)
+
+ // Continue receiving transactions.
+ self.receiveTransaction()
}
}
-
-// print("HotlineTracker: server count = \(self.serverCount)")
+ else {
+ // In this case we have no further data to receive so we simply
+ // process the transaction and then continue receiving.
+ self.processTransaction(transaction)
+ self.receiveTransaction()
+ }
+ }
+ else {
+ // Here we failed to parse the current transaction.
+ // We should consider disconnecting perhaps.
+ // But for now we'll continue receiving.
+ self.receiveTransaction()
}
-
-// self.receiveListing()
-// }
}
}
-
- private func processTransaction(_ transaction: HotlineTransaction) {
- print("HotlineClient processing transaction \(transaction.type) with \(transaction.parameterCount) parameters")
- }
private func parseTransaction(data: Data) -> HotlineTransaction? {
if
@@ -225,17 +201,161 @@ class HotlineClient : ObservableObject {
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
- )
+ print("HotlineClient: Parsing transaction type \(type) with data \(dataSize)")
+ if let transactionType = HotlineTransactionType(rawValue: type) {
+ return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize)
+ }
}
return nil
}
+
+ // MARK: - Messages
+
+ 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 handshake failed \(err)")
+ 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()
+ self.receiveTransaction()
+ }
+ })
+ }
+
+ func sendLogin(callback: (() -> Void)? = nil) {
+ DispatchQueue.main.async {
+ self.connectionStatus = .loggingIn
+ }
+
+ var t = HotlineTransaction(type: .login)
+ t.setParameterEncodedString(type: .userLogin, val: "")
+ t.setParameterEncodedString(type: .userPassword, val: "")
+ t.setParameterUInt32(type: .userIconID, val: self.userIconID)
+ t.setParameterString(type: .userName, val: self.userName)
+ t.setParameterUInt32(type: .versionNumber, val: 151)
+
+ print("HotlineClient: logging in...")
+ self.sendTransaction(t) { [weak self] in
+ print("HotlineClient: logged in!")
+ DispatchQueue.main.async {
+ self?.connectionStatus = .loggedIn
+ }
+
+ callback?()
+ }
+ }
+
+ func sendAgree(callback: (() -> Void)? = nil) {
+ var t = HotlineTransaction(type: .agreed)
+ t.setParameterString(type: .userName, val: self.userName)
+ t.setParameterUInt32(type: .userIconID, val: self.userIconID)
+ t.setParameterUInt32(type: .options, val: 0)
+
+ print("HotlineClient: agreeing")
+ self.sendTransaction(t, callback: callback)
+ }
+
+ func sendChat(message: String, callback: (() -> Void)? = nil) {
+ var t = HotlineTransaction(type: .sendChat)
+ t.setParameterString(type: .data, val: message)
+
+ print("HotlineClient: sending chat...")
+ self.sendTransaction(t, callback: callback)
+ }
+
+ func sendGetUserList(callback: (() -> Void)? = nil) {
+ let t = HotlineTransaction(type: .getUserNameList)
+ print("HotlineClient: fetching user list...")
+ self.sendTransaction(t, callback: callback)
+ }
+
+ // MARK: - Incoming
+
+ private func processTransaction(_ transaction: HotlineTransaction) {
+ switch(transaction.type) {
+ case .reply:
+ print("HotlineClient: GOT REPLY TRANSACTION? \(transaction)")
+ case .chatMessage:
+ print("HotlineClient: CHAT MESSAGE!")
+ if
+ let chatTextParam = transaction.getParameter(type: .data),
+ let chatText = chatTextParam.getString(),
+ let userNameParam = transaction.getParameter(type: .userName),
+ let userName = userNameParam.getString(),
+ let userIDParam = transaction.getParameter(type: .userID),
+ let userID = userIDParam.getUInt16() {
+ print("HotlineClient: \(userName):\(userID): \(chatText)")
+ DispatchQueue.main.async {
+ self.chatMessages.append(chatText)
+ }
+ }
+ case .getUserNameList:
+ print("HotlineClient: GOT USER NAME LIST!")
+ let userList = transaction.getParameterList(type: .userInfo)
+ for u in userList {
+ let userInfo = u.getUserInfo()
+ print("HotlineClient: user \(userInfo.userName)")
+ }
+ case .notifyOfUserChange:
+ print("HotlineClient: user changed")
+ if let p = transaction.getParameter(type: .userName),
+ let userName = p.getString() {
+ print("HotlineClient: user name \(userName)")
+ }
+ case .disconnectMessage:
+ print("HotlineClient: DISCONNECTED BY SERVER!")
+ self.disconnect()
+ case .showAgreement:
+ if let agreementParam = transaction.getParameter(type: .data) {
+ if let agreementText = agreementParam.getString() {
+ print("AGREEMENT:", agreementText)
+ DispatchQueue.main.async {
+ self.agreement = agreementText
+ }
+ self.sendAgree() {
+// self.sendGetUserList()
+ }
+ }
+ }
+ case .userAccess:
+ print("HotlineClient: user access transaction.")
+ default:
+ print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.parameters.count) parameters")
+ print(transaction.parameters)
+ }
+ }
}