aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Network
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/Network')
-rw-r--r--Hotline/Network/HotlineClient.swift241
-rw-r--r--Hotline/Network/HotlineProtocol.swift118
-rw-r--r--Hotline/Network/HotlineTracker.swift48
3 files changed, 381 insertions, 26 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
+ }
+}
diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift
new file mode 100644
index 0000000..787675e
--- /dev/null
+++ b/Hotline/Network/HotlineProtocol.swift
@@ -0,0 +1,118 @@
+import Foundation
+
+struct HotlineServer: Identifiable, Hashable {
+ let id = UUID()
+ let address: String
+ let port: UInt16
+ let users: UInt16
+ let name: String?
+ let description: String?
+
+ static func == (lhs: HotlineServer, rhs: HotlineServer) -> Bool {
+ return lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(self.id)
+ }
+}
+
+struct HotlineTransactionParameter {
+ let id: UInt16
+ let dataSize: UInt16
+ let data: Data
+}
+
+struct HotlineTransaction {
+ static let headerSize = 20
+
+ let flags: UInt8
+ let isReply: UInt8
+ let type: HotlineTransactionType
+ let id: UInt32
+ let errorCode: UInt32
+ let totalSize: UInt32
+ let dataSize: UInt32
+
+ var parameterCount: UInt16 = 0
+ var parameters: [HotlineTransactionParameter]? = nil
+
+ func encode(to data: inout Data) {
+ data.appendUInt8(self.flags)
+ data.appendUInt8(self.isReply)
+ data.appendUInt16(self.type.rawValue)
+ data.appendUInt32(self.id)
+ data.appendUInt32(self.errorCode)
+ data.appendUInt32(self.totalSize)
+ data.appendUInt32(self.dataSize)
+
+ if let p = self.parameters, p.count > 0 {
+ data.appendUInt16(UInt16(p.count))
+ for param in p {
+ data.appendUInt16(param.id)
+ data.appendUInt16(param.dataSize)
+ data.append(param.data)
+ }
+ }
+ }
+}
+
+enum HotlineTransactionType: UInt16 {
+ case unknown = 0
+ case error = 100
+ case getMessages = 101
+ case newMessage = 102
+ case oldPostNews = 103
+ case serverMessage = 104
+ case sendChat = 105
+ case chatMessage = 106
+ case login = 107
+ case sendInstantMessage = 108
+ case showAgreement = 109
+ case disconnectUser = 110
+ case disconnectMessage = 111
+ case inviteToNewChat = 112
+ case inviteToChat = 113
+ case rejectChatInvite = 114
+ case joinChat = 115
+ case leaveChat = 116
+ case notifyChatOfUserChange = 117
+ case notifyChatOfUserDelete = 118
+ case notifyChatSubject = 119
+ case setChatSubject = 120
+ case agreed = 121
+ case serverBanner = 122
+ case getFileNameList = 200
+ case downloadFile = 202
+ case uploadFile = 203
+ case deleteFile = 204
+ case newFolder = 205
+ case getFileInfo = 206
+ case setFileInfo = 207
+ case moveFile = 208
+ case makeFileAlias = 209
+ case downloadFolder = 210
+ case downloadInfo = 211
+ case downloadBanner = 212
+ case uploadFolder = 213
+ case getUserNameList = 300
+ case notifyOfUserChange = 301
+ case notifyOfUserDelete = 302
+ case getClientInfoText = 303
+ case setClientUserInfo = 304
+ case newUser = 350
+ case deleteUser = 351
+ case getUser = 352
+ case setUser = 353
+ case userAccess = 354
+ case userBroadcast = 355
+ case getNewsCategoryNameList = 370
+ case getNewsArticleNameList = 371
+ case deleteNewsItem = 380
+ case newNewsFolder = 381
+ case newNewsCategory = 382
+ case getNewsArticleData = 400
+ case postNewsArticle = 410
+ case deleteNewsArticle = 411
+}
+
diff --git a/Hotline/Network/HotlineTracker.swift b/Hotline/Network/HotlineTracker.swift
index 9b21ab2..2c44bc8 100644
--- a/Hotline/Network/HotlineTracker.swift
+++ b/Hotline/Network/HotlineTracker.swift
@@ -1,15 +1,6 @@
import Foundation
import Network
-struct HotlineServer: Identifiable {
- var id = UUID()
- let address: String
- let port: UInt16
- let users: UInt16
- let name: String?
- let description: String?
-}
-
enum HotlineTrackerStatus: Int {
case disconnected
case connecting
@@ -19,7 +10,6 @@ enum HotlineTrackerStatus: Int {
class HotlineTracker : ObservableObject {
let serverAddress: NWEndpoint.Host
let serverPort: NWEndpoint.Port = NWEndpoint.Port(rawValue: 5498)!
- let callback: ([HotlineServer]) -> Void
static let magicPacket = Data([
0x48, 0x54, 0x52, 0x4B, // 'HTRK'
@@ -35,9 +25,8 @@ class HotlineTracker : ObservableObject {
@Published var connectionStatus: HotlineTrackerStatus = .disconnected
@Published var servers: [HotlineServer] = []
- init(address: String, callback: @escaping ([HotlineServer]) -> Void) {
+ init(address: String) {
self.serverAddress = NWEndpoint.Host(address)
- self.callback = callback
}
func fetch() {
@@ -56,20 +45,28 @@ class HotlineTracker : ObservableObject {
switch newState {
case .ready:
print("READY TO SEND AND RECEIVE DATA")
- self?.connectionStatus = .connected
+ DispatchQueue.main.async {
+ self?.connectionStatus = .connected
+ }
self?.sendMagic()
case .cancelled:
print("CONNECTION CANCELLED")
- self?.connectionStatus = .disconnected
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
case .failed(let err):
print("CONNECTION ERROR \(err)")
- self?.connectionStatus = .disconnected
+ DispatchQueue.main.async {
+ self?.connectionStatus = .disconnected
+ }
default:
print("CONNECTION OTHER THING")
}
}
- self.connectionStatus = .connecting
+ DispatchQueue.main.async {
+ self.connectionStatus = .connecting
+ }
self.connection?.start(queue: .global())
}
@@ -89,7 +86,7 @@ class HotlineTracker : ObservableObject {
return
}
-// let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion]
+ // let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion]
c.send(content: HotlineTracker.magicPacket, completion: .contentProcessed { [weak self] (error) in
if let err = error {
@@ -120,9 +117,9 @@ class HotlineTracker : ObservableObject {
self.disconnect()
return
}
-// if let data = data, !data.isEmpty {
+ // if let data = data, !data.isEmpty {
print("HotlineTracker: received magic response!")
-// }
+ // }
if let error = error {
print("HotlineTracker: receive error \(error)")
@@ -153,7 +150,7 @@ class HotlineTracker : ObservableObject {
if let data = data, !data.isEmpty {
print("HotlineTracker: received \(data.count) header bytes")
-
+
self.maxDataLength = Int(data[2]) * 0xFF + Int(data[3])
self.maxDataLength -= 4
print("HotlineTracker: message size = \(self.maxDataLength)")
@@ -182,7 +179,7 @@ class HotlineTracker : ObservableObject {
guard let self = self else {
return
}
-
+
if let data = data, !data.isEmpty {
print("HotlineTracker: received \(data.count) bytes")
self.bytes.append(contentsOf: data)
@@ -216,7 +213,7 @@ class HotlineTracker : ObservableObject {
// Description size (1 byte)
// Description (description size)
- var servers: [HotlineServer] = []
+ var foundServers: [HotlineServer] = []
var cursor = 0
for _ in 1...self.serverCount {
@@ -243,7 +240,7 @@ class HotlineTracker : ObservableObject {
print("SERVER: \(server)")
- servers.append(server)
+ foundServers.append(server)
cursor += 11 + nameLength + 1 + descLength
}
@@ -255,9 +252,8 @@ class HotlineTracker : ObservableObject {
}
DispatchQueue.main.async {
- print("CALLING CALLBACK")
- self.servers = servers
- self.callback(servers)
+// print("CALLING CALLBACK")
+ self.servers = foundServers
}
}