aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Network
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-04 09:12:58 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-04 09:12:58 -0800
commit471546236b8991f61d26c0e3aa8ee48b83b983af (patch)
treeb74331a8c8ea61ee7e1eaedfc7bbd1686bbc4702 /Hotline/Network
parent885119acdd27bd53ed3096285c3eca3e2d99ad70 (diff)
Starting to hash out a server view and tabs. Added research pdfs. etc.
Diffstat (limited to 'Hotline/Network')
-rw-r--r--Hotline/Network/HotlineClient.swift432
-rw-r--r--Hotline/Network/HotlineProtocol.swift388
-rw-r--r--Hotline/Network/HotlineTracker.swift260
3 files changed, 0 insertions, 1080 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift
deleted file mode 100644
index b5092a0..0000000
--- a/Hotline/Network/HotlineClient.swift
+++ /dev/null
@@ -1,432 +0,0 @@
-import Foundation
-import Network
-
-enum HotlineClientStatus: Int {
- case disconnected
- case connecting
- case connected
- case loggingIn
- case loggedIn
-}
-
-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
- @Published var agreement: String?
- @Published var userList: [HotlineUser] = []
- @Published var chatMessages: [String] = []
-
- var userName: String = "bolt"
- var userIconID: UInt16 = 128
- var serverVersion: UInt16 = 151
- var server: HotlineServer?
- var connection: NWConnection?
-
- private var transactionLog: [UInt32:HotlineTransactionType] = [:]
-
- init() {
-
- }
-
- // MARK: -
-
- func connect(to server: HotlineServer) {
- self.server = server
-
- let serverAddress = NWEndpoint.Host(server.address)
- let serverPort = NWEndpoint.Port(rawValue: server.port)!
-
- 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:
- 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() {
- self.connection?.cancel()
- self.connection = nil
-
- DispatchQueue.main.async {
- self.connectionStatus = .disconnected
- }
- }
-
- // MARK: -
-
- private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) {
- guard let c = connection else {
- return
- }
-
- print("HotlineClient => \(t.id) \(t.type)")
-
- self.transactionLog[t.id] = t.type
-
- c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in
- if disconnectOnError, error != nil {
- self?.disconnect()
- return
- }
-
- callback?()
- })
- }
-
- private func receiveTransaction() {
- guard let c = connection else {
- print("HotlineClient: no connection for transaction.")
- return
- }
-
- print("HotlineClient ⏳")
- 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: transaction error \(error)")
- self.disconnect()
- return
- }
-
- 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] (fieldData, context, isComplete, error) in
- guard let self = self else {
- return
- }
-
- guard let fieldData = fieldData, !fieldData.isEmpty else {
- print("HotlineClient: transaction field data is empty!")
- self.disconnect()
- return
- }
-
- let fieldCount = fieldData.readUInt16(at: 0)!
-
- if fieldCount > 0 {
- var dataCursor = 2
- for _ in 0..<fieldCount {
- if
- let fieldID = fieldData.readUInt16(at: dataCursor),
- let fieldSize = fieldData.readUInt16(at: dataCursor + 2),
- let fieldRemainingData = fieldData.readData(at: dataCursor + 4, length: Int(fieldSize)) {
-
- if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) {
-
- transaction.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData))
-// transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData)
- }
- else {
- print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize)
- }
-
- dataCursor += 4 + Int(fieldSize)
- }
- }
-
- // Process the transaction if we have processed more than zero parameters here
- // as we expect parameters at this point.
- self.processTransaction(transaction)
- }
-
- // Continue receiving transactions.
- self.receiveTransaction()
- }
- }
- 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()
- }
- }
- }
-
- 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) {
-
- if let transactionType = HotlineTransactionType(rawValue: type) {
- return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize)
- }
- else {
- print("HotlineClient: Unknown type \(type) parsing with \(dataSize) bytes")
- }
- }
-
- 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 🤝")
- self.sendLogin() { [weak self] in
- self?.sendSetClientUserInfo()
- self?.sendGetUserList()
- }
- self.receiveTransaction()
- }
- })
- }
-
- func sendLogin(callback: (() -> Void)? = nil) {
- DispatchQueue.main.async {
- self.connectionStatus = .loggingIn
- }
-
- var t = HotlineTransaction(type: .login)
- t.setFieldEncodedString(type: .userLogin, val: "")
- t.setFieldEncodedString(type: .userPassword, val: "")
- t.setFieldUInt16(type: .userIconID, val: self.userIconID)
- t.setFieldString(type: .userName, val: self.userName)
- t.setFieldUInt32(type: .versionNumber, val: 123)
-
- self.sendTransaction(t) { [weak self] in
- DispatchQueue.main.async {
- self?.connectionStatus = .loggedIn
- }
-
- callback?()
- }
- }
-
- func sendSetClientUserInfo(callback: (() -> Void)? = nil) {
- var t = HotlineTransaction(type: .setClientUserInfo)
- t.setFieldString(type: .userName, val: self.userName)
- t.setFieldUInt16(type: .userIconID, val: self.userIconID)
-
- self.sendTransaction(t, callback: callback)
- }
-
- func sendAgree(callback: (() -> Void)? = nil) {
- var t = HotlineTransaction(type: .agreed)
- t.setFieldString(type: .userName, val: self.userName)
- t.setFieldUInt16(type: .userIconID, val: self.userIconID)
- t.setFieldUInt32(type: .options, val: 0)
- self.sendTransaction(t, callback: callback)
- }
-
- func sendChat(message: String, callback: (() -> Void)? = nil) {
- var t = HotlineTransaction(type: .sendChat)
- t.setFieldString(type: .data, val: message)
- self.sendTransaction(t, callback: callback)
- }
-
- func sendGetUserList(callback: (() -> Void)? = nil) {
- let t = HotlineTransaction(type: .getUserNameList)
- self.sendTransaction(t, callback: callback)
- }
-
- // MARK: - Incoming
-
- private func processReply(_ transaction: HotlineTransaction) {
- guard transaction.errorCode == 0 else {
- if let errorParam = transaction.getField(type: .errorText), let errorText = errorParam.getString() {
- print("HotlineClient 😵 \(transaction.errorCode): \(errorText)")
- }
- else {
- print("HotlineClient 😵 \(transaction.errorCode)")
- }
- return
- }
-
- guard let repliedTransactionType = self.transactionLog[transaction.id] else {
- return
- }
-
- print("HotlineClient reply in response to \(repliedTransactionType)")
-
- switch(repliedTransactionType) {
- case .login:
- print("GOT REPLY TO LOGIN!")
-
- if
- let serverVersionField = transaction.getField(type: .versionNumber),
- let serverVersion = serverVersionField.getUInt16() {
- self.serverVersion = serverVersion
- print("SERVER VERSION: \(serverVersion)")
- }
- case .getUserNameList:
- print("GOT USER LIST")
- var newUserList: [HotlineUser] = []
- for u in transaction.getFieldList(type: .userNameWithInfo) {
- let info = u.getUserInfo()
- print("USER: \(info)")
- let user = HotlineUser(id: info.id, iconID: info.iconID, status: info.flags, name: info.userName)
- newUserList.append(user)
- }
- DispatchQueue.main.async {
- self.userList = newUserList
- }
- default:
- break
- }
- }
-
- private func processTransaction(_ transaction: HotlineTransaction) {
- if transaction.type == .reply {
- print("HotlineClient <= \(transaction.type) to \(transaction.id):")
- print(transaction)
- }
- else {
- print("HotlineClient <= \(transaction.type)")
- }
-
- switch(transaction.type) {
- case .reply:
- self.processReply(transaction)
-// print("HotlineClient: Received reply transaction: \(transaction)")
-
- case .chatMessage:
- if
- let chatTextParam = transaction.getField(type: .data),
- let chatText = chatTextParam.getString(),
- let userNameParam = transaction.getField(type: .userName),
- let userName = userNameParam.getString(),
- let userIDParam = transaction.getField(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.getFieldList(type: .userNameWithInfo)
- for u in userList {
- let userInfo = u.getUserInfo()
- print("HotlineClient: user \(userInfo.userName)")
- }
- case .notifyOfUserChange:
-// print("HotlineClient: user changed")
- if let p = transaction.getField(type: .userName),
- let userName = p.getString() {
- print("HotlineClient: User changed \(userName)")
- }
- case .disconnectMessage:
- print("HotlineClient ❌")
- self.disconnect()
- case .showAgreement:
- if let noAgreementField = transaction.getField(type: .noServerAgreement) {
- print("NO AGREEMENT?")
- }
- if let agreementParam = transaction.getField(type: .data) {
- if let agreementText = agreementParam.getString() {
- print("\n\n--------------------------\n")
- print(agreementText)
- print("\n--------------------------\n\n")
- DispatchQueue.main.async {
- self.agreement = agreementText
- }
-// self.sendAgree() {
-// self.sendGetUserList()
-// }
- }
- }
- case .userAccess:
- print("")
- default:
- print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters")
- print(transaction.fields)
- }
- }
-}
diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift
deleted file mode 100644
index 5372e78..0000000
--- a/Hotline/Network/HotlineProtocol.swift
+++ /dev/null
@@ -1,388 +0,0 @@
-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 HotlineUser: Identifiable, Hashable {
- let id: UInt16
- let iconID: UInt16
- let status: UInt16
- let name: String
-
- var isAdmin: Bool {
- return ((self.status & 0x0002) != 0)
- }
-
- var isIdle: Bool {
- return ((self.status & 0x0001) != 0)
- }
-
- static func == (lhs: HotlineUser, rhs: HotlineUser) -> Bool {
- return lhs.id == rhs.id
- }
-
- init(id: UInt16, iconID: UInt16, status: UInt16, name: String) {
- self.id = id
- self.iconID = iconID
- self.status = status
- self.name = name
- }
-
- init(from data: Data) {
- self.id = data.readUInt16(at: 0)!
- self.iconID = data.readUInt16(at: 2)!
- self.status = data.readUInt16(at: 4)!
-
- let userNameLength = Int(data.readUInt16(at: 6)!)
- self.name = data.readString(at: 8, length: userNameLength, encoding: .ascii)!
- }
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(self.id)
- }
-
- func encoded() -> Data {
- var data = Data()
- self.encode(to: &data)
- return data
- }
-
- func encode(to data: inout Data) {
- data.appendUInt16(self.id)
- data.appendUInt16(self.iconID)
- data.appendUInt16(self.status)
-
- let userNameData = name.data(using: .ascii, allowLossyConversion: true)!
-
- data.appendUInt16(UInt16(userNameData.count))
- data.append(userNameData)
- }
-}
-
-struct HotlineUserInfo {
- let id: UInt16
- let iconID: UInt16
- let flags: UInt16
- let userName: String
-
- init(data: Data) {
- self.id = data.readUInt16(at: 0)!
- self.iconID = data.readUInt16(at: 2)!
- self.flags = data.readUInt16(at: 4)!
-
- let userNameLength = data.readUInt16(at: 6)!
- self.userName = data.readString(at: 8, length: Int(userNameLength), encoding: .ascii)!
- }
-}
-
-struct HotlineTransactionField {
- let type: HotlineTransactionFieldType
- let dataSize: UInt16
- let data: Data
-
- init(type: HotlineTransactionFieldType, dataSize: UInt16, data: Data) {
- self.type = type
- self.dataSize = dataSize
- self.data = data
- }
-
- init(type: HotlineTransactionFieldType, val: UInt8) {
- self.init(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val))
- }
-
- init(type: HotlineTransactionFieldType, val: UInt16) {
- self.init(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val))
- }
-
- init(type: HotlineTransactionFieldType, val: UInt32) {
- self.init(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val))
- }
-
- init(type: HotlineTransactionFieldType, string: String, encoding: String.Encoding = .ascii, encrypt: Bool = false) {
- var stringInput = string
-
- if encrypt {
- stringInput = String(string.utf8.map { char in
- Character(UnicodeScalar(0xFF - char))
- })
- }
-
- var stringData: Data?
- stringData = stringInput.data(using: encoding, allowLossyConversion: true)
- if stringData == nil {
- stringData = Data()
- }
-
- self.init(type: type, dataSize: UInt16(stringData!.count), data: stringData!)
- }
-
- init(type: HotlineTransactionFieldType, string: String, encrypt: Bool) {
- self.init(type: type, string: string, encoding: .ascii, encrypt: encrypt)
- }
-
- func getUInt8() -> UInt8? {
- return self.data.readUInt8(at: 0)
- }
-
- func getUInt16() -> UInt16? {
- return self.data.readUInt16(at: 0)
- }
-
- func getUInt32() -> UInt32? {
- return self.data.readUInt32(at: 0)
- }
-
- func getInteger() -> Int? {
- switch(self.data.count) {
- case 1:
- if let val = self.getUInt8() {
- return Int(val)
- }
- case 2:
- if let val = self.getUInt16() {
- return Int(val)
- }
- case 4:
- if let val = self.getUInt32() {
- return Int(val)
- }
- default:
- break
- }
-
- return nil
- }
-
- func getString(encoding: String.Encoding = .ascii) -> String? {
- return String(data: self.data, encoding: encoding)
- }
-
- func getUserInfo() -> HotlineUserInfo {
- return HotlineUserInfo(data: self.data)
- }
-}
-
-struct HotlineTransaction {
- static let headerSize = 20
- static var sequenceID: UInt32 = 1
-
- static func nextID() -> UInt32 {
- HotlineTransaction.sequenceID += 1
- return HotlineTransaction.sequenceID
- }
-
- var flags: UInt8 = 0
- var isReply: UInt8 = 0
- var type: HotlineTransactionType
- var id: UInt32 = HotlineTransaction.nextID()
- var errorCode: UInt32 = 0
- var totalSize: UInt32 = UInt32(HotlineTransaction.headerSize)
- var dataSize: UInt32 = 0
-
- var fields: [HotlineTransactionField] = []
-
- init(type: HotlineTransactionType) {
- self.type = type
- }
-
- init(type: HotlineTransactionType, flags: UInt8, isReply: UInt8, id: UInt32, errorCode: UInt32, totalSize: UInt32, dataSize: UInt32) {
- self.type = type
- self.flags = flags
- self.isReply = isReply
- self.id = id
- self.errorCode = errorCode
- self.totalSize = totalSize
- self.dataSize = dataSize
- }
-
- mutating func setFieldUInt8(type: HotlineTransactionFieldType, val: UInt8) {
- self.fields.append(HotlineTransactionField(type: type, val: val))
- }
-
- mutating func setFieldUInt16(type: HotlineTransactionFieldType, val: UInt16) {
- self.fields.append(HotlineTransactionField(type: type, val: val))
- }
-
- mutating func setFieldUInt32(type: HotlineTransactionFieldType, val: UInt32) {
- self.fields.append(HotlineTransactionField(type: type, val: val))
- }
-
- mutating func setFieldEncodedString(type: HotlineTransactionFieldType, val: String) {
- self.fields.append(HotlineTransactionField(type: type, string: val, encrypt: true))
- }
-
- mutating func setFieldString(type: HotlineTransactionFieldType, val: String) {
- self.fields.append(HotlineTransactionField(type: type, string: val))
- }
-
- func getField(type: HotlineTransactionFieldType) -> HotlineTransactionField? {
- return self.fields.first { p in
- p.type == type
- }
- }
-
- func getFieldList(type: HotlineTransactionFieldType) -> [HotlineTransactionField] {
- return self.fields.filter { p in
- p.type == type
- }
- }
-
- func encoded() -> Data {
- var data = Data()
- self.encode(to: &data)
- return data
- }
-
- 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)
-
- if self.fields.count > 0 {
- var fieldData = Data()
- fieldData.appendUInt16(UInt16(self.fields.count))
- for f in self.fields {
- fieldData.appendUInt16(f.type.rawValue)
- fieldData.appendUInt16(f.dataSize)
- fieldData.append(f.data)
- }
-
- data.appendUInt32(UInt32(fieldData.count))
- data.appendUInt32(UInt32(fieldData.count))
- data.append(fieldData)
- }
- else {
- data.appendUInt32(2)
- data.appendUInt32(2)
- data.appendUInt16(0)
- }
- }
-}
-
-enum HotlineTransactionFieldType: UInt16 {
- case errorText = 100 // String
- case data = 101 // String
- case userName = 102 // String
- case userID = 103 // Integer
- case userIconID = 104 // Integer
- case userLogin = 105 // Encoded string
- case userPassword = 106 // Encoded string
- case referenceNumber = 107 // Integer
- case transferSize = 108 // Integer
- case chatOptions = 109 // Integer
- case userAccess = 110 // 64-bit integer?
- case userAlias = 111 // ???
- case userFlags = 112 // Integer
- case options = 113 // 32-bit integer?
- case chatID = 114 // Integer
- case chatSubject = 115 // String
- case waitingCount = 116 // Integer
- case serverAgreement = 150 // ???
- case serverBanner = 151 // Data?
- case serverBannerType = 152 // Integer
- case serverBannerURL = 153 // String
- case noServerAgreement = 154 // Integer
- case versionNumber = 160 // Integer
- case communityBannerID = 161 // Integer
- case serverName = 162 // String
- // TODO: Add file field types
- case quotingMessage = 214 // String?
- case automaticResponse = 215 // String
- case folderItemCount = 220 // Integer
- case userNameWithInfo = 300 // Data { user id: 2, icon id: 2, user flags: 2, user name size: 2, user name: size }
- case newsCategoryGUID = 319 // Data?
- case newsCategoryListData = 320 // Data { type: 1 (1 = folder, 10 = category, 255 = other), category name: rest }
- case newsArticleListData = 321 // Data
- case newsCategoryName = 322 // String
- case newsCategoryListData15 = 323 // Data
- case newsPath = 325 // Data
- case newsArticleID = 326 // Integer
- case newsArticleDataFlavor = 327 // String
- case newsArticleTitle = 328 // String
- case newsArticlePoster = 329 // String
- case newsArticleDate = 330 // Data { year: 2, ms: 2, secs: 4 }
- case newsArticlePrevious = 331 // Integer
- case newsArticleNext = 332 // Integer
- case newsArticleData = 333 // Data
- case newsArticleFlags = 334 // Integer
- case newsArticleParentArticle = 335 // Integer
- case newsArticleFirstChildArticle = 336 // Integer
- case newsArticleRecursiveDelete = 337 // Integer
-}
-
-enum HotlineTransactionType: UInt16 {
- case reply = 0
- case error = 100
- case getMessages = 101
- case newMessage = 102 // Server
- case oldPostNews = 103
- case serverMessage = 104 // Server
- case sendChat = 105
- case chatMessage = 106 // Server
- case login = 107
- case sendInstantMessage = 108
- case showAgreement = 109 // Server
- case disconnectUser = 110
- case disconnectMessage = 111 // Server
- case inviteToNewChat = 112
- case inviteToChat = 113 // Server
- case rejectChatInvite = 114
- case joinChat = 115
- case leaveChat = 116
- case notifyChatOfUserChange = 117 // Server
- case notifyChatOfUserDelete = 118 // Server
- case notifyChatSubject = 119 // Server
- case setChatSubject = 120
- case agreed = 121
- case serverBanner = 122 // Server
- 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 // Server
- case downloadBanner = 212
- case uploadFolder = 213
- case getUserNameList = 300
- case notifyOfUserChange = 301 // Server
- case notifyOfUserDelete = 302 // Server
- case getClientInfoText = 303
- case setClientUserInfo = 304
- case newUser = 350
- case deleteUser = 351
- case getUser = 352
- case setUser = 353
- case userAccess = 354 // Server
- case userBroadcast = 355 // Client & Server
- case getNewsCategoryNameList = 370
- case getNewsArticleNameList = 371
- case deleteNewsItem = 380
- case newNewsFolder = 381
- case newNewsCategory = 382
- case getNewsArticleData = 400
- case postNewsArticle = 410
- case deleteNewsArticle = 411
- case connectionKeepAlive = 500
-}
-
diff --git a/Hotline/Network/HotlineTracker.swift b/Hotline/Network/HotlineTracker.swift
deleted file mode 100644
index 2c44bc8..0000000
--- a/Hotline/Network/HotlineTracker.swift
+++ /dev/null
@@ -1,260 +0,0 @@
-import Foundation
-import Network
-
-enum HotlineTrackerStatus: Int {
- case disconnected
- case connecting
- case connected
-}
-
-class HotlineTracker : ObservableObject {
- let serverAddress: NWEndpoint.Host
- let serverPort: NWEndpoint.Port = NWEndpoint.Port(rawValue: 5498)!
-
- static let magicPacket = Data([
- 0x48, 0x54, 0x52, 0x4B, // 'HTRK'
- 0x00, 0x01 // Version
- ])
-
- var connection: NWConnection?
-
- var bytes = Data()
- var maxDataLength: Int = 0
- var serverCount: Int = 0
-
- @Published var connectionStatus: HotlineTrackerStatus = .disconnected
- @Published var servers: [HotlineServer] = []
-
- init(address: String) {
- self.serverAddress = NWEndpoint.Host(address)
- }
-
- func fetch() {
- self.reset()
- self.connect()
- }
-
- private func reset() {
- self.maxDataLength = 0
- self.serverCount = 0
- }
-
- private func connect() {
- self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp)
- self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
- switch newState {
- case .ready:
- print("READY TO SEND AND RECEIVE DATA")
- DispatchQueue.main.async {
- self?.connectionStatus = .connected
- }
- self?.sendMagic()
- case .cancelled:
- print("CONNECTION CANCELLED")
- DispatchQueue.main.async {
- self?.connectionStatus = .disconnected
- }
- case .failed(let err):
- print("CONNECTION ERROR \(err)")
- DispatchQueue.main.async {
- self?.connectionStatus = .disconnected
- }
- default:
- print("CONNECTION OTHER THING")
- }
- }
-
- DispatchQueue.main.async {
- self.connectionStatus = .connecting
- }
- self.connection?.start(queue: .global())
- }
-
- private func disconnect() {
- guard let c = connection else {
- print("HotlineTracker: already disconnected")
- return
- }
-
- c.cancel()
- self.connection = nil
- }
-
- private func sendMagic() {
- guard let c = connection else {
- print("HotlineTracker: invalid connection to send magic.")
- return
- }
-
- // 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 {
- print("HotlineTracker: sending magic failed \(err)")
- return
- }
-
- print("HotlineTracker: sent magic!")
-
- self?.receiveMagic()
- })
- }
-
- private func receiveMagic() {
- guard let c = connection else {
- print("HotlineTracker: invalid connection to receive magic.")
- return
- }
-
- print("HotlineTracker: receiving...")
- c.receive(minimumIncompleteLength: 6, maximumLength: 6) { [weak self] (data, context, isComplete, error) in
- guard let self = self, let data = data else {
- return
- }
-
- if data.isEmpty || !data.elementsEqual(HotlineTracker.magicPacket) {
- print("HotlineTracker: invalid magic response")
- self.disconnect()
- return
- }
- // if let data = data, !data.isEmpty {
- print("HotlineTracker: received magic response!")
- // }
-
- if let error = error {
- print("HotlineTracker: receive error \(error)")
- }
- else {
- self.receiveHeader()
- }
- }
- }
-
- private func receiveHeader() {
- guard let c = connection else {
- print("HotlineTracker: invalid connection to receive header.")
- return
- }
-
- print("HotlineTracker: receiving...")
- c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in
- guard let self = self else {
- return
- }
-
- if let error = error {
- print("HotlineTracker: receive error \(error)")
- self.disconnect()
- return
- }
-
- 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)")
-
- self.serverCount = Int(data[4]) * 256 + Int(data[5])
- print("HotlineTracker: server count = \(self.serverCount)")
- }
-
- if let error = error {
- print("HotlineTracker: receive error \(error)")
- }
- else {
- self.receiveListing()
- }
- }
- }
-
- private func receiveListing() {
- guard let c = connection else {
- print("HotlineTracker: invalid connection to receive data.")
- return
- }
-
- print("HotlineTracker: receiving...")
- c.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { [weak self] (data, context, isComplete, error) in
- guard let self = self else {
- return
- }
-
- if let data = data, !data.isEmpty {
- print("HotlineTracker: received \(data.count) bytes")
- self.bytes.append(contentsOf: data)
-
- if bytes.count >= maxDataLength {
- print("HotlineTracker: done with data, should close. \(self.bytes.count) \(self.maxDataLength)")
- self.disconnect()
- self.parseListing()
- return
- }
- }
-
- if let error = error {
- print("HotlineTracker: receive error \(error)")
- self.disconnect()
- }
- else {
- print("HotlineTracker: not complete")
- self.receiveListing()
- }
- }
- }
-
- private func parseListing() {
- // IP address (4 bytes)
- // Port number (2 bytes)
- // Number of users (2 bytes)
- // Unused (2 bytes)
- // Name size (1 byte)
- // Name (name size)
- // Description size (1 byte)
- // Description (description size)
-
- var foundServers: [HotlineServer] = []
-
- var cursor = 0
- for _ in 1...self.serverCount {
- if self.bytes.count < cursor + 12 {
- print("HotlineTracker: Data isn't long enough for next server")
- break
- }
-
- if
- let ip_1 = self.bytes.readUInt8(at: cursor),
- let ip_2 = self.bytes.readUInt8(at: cursor + 1),
- let ip_3 = self.bytes.readUInt8(at: cursor + 2),
- let ip_4 = self.bytes.readUInt8(at: cursor + 3),
- let port = self.bytes.readUInt16(at: cursor + 4),
- let userCount = self.bytes.readUInt16(at: cursor + 6),
- let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) {
-
- let nameLength = Int(nameLengthByte)
- if let name = self.bytes.readString(at: cursor + 11, length: nameLength, encoding: .utf8) ?? self.bytes.readString(at: cursor + 11, length: nameLength, encoding: .ascii) {
- if let descLengthByte = self.bytes.readUInt8(at: cursor + 11 + nameLength) {
- let descLength = Int(descLengthByte)
- if let desc = self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength, encoding: .utf8) ?? self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength, encoding: .ascii) {
- let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
-
- print("SERVER: \(server)")
-
- foundServers.append(server)
-
- cursor += 11 + nameLength + 1 + descLength
- }
- }
- }
- }
-
- print(cursor)
- }
-
- DispatchQueue.main.async {
-// print("CALLING CALLBACK")
- self.servers = foundServers
- }
-
- }
-}