aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Network
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/Network')
-rw-r--r--Hotline/Network/HotlineClient.swift179
-rw-r--r--Hotline/Network/HotlineProtocol.swift234
2 files changed, 305 insertions, 108 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift
index 5193fe5..b5092a0 100644
--- a/Hotline/Network/HotlineClient.swift
+++ b/Hotline/Network/HotlineClient.swift
@@ -24,12 +24,14 @@ class HotlineClient : ObservableObject {
@Published var userList: [HotlineUser] = []
@Published var chatMessages: [String] = []
- let userName: String = "bolt"
- let userIconID: UInt32 = 128
-
+ var userName: String = "bolt"
+ var userIconID: UInt16 = 128
+ var serverVersion: UInt16 = 151
var server: HotlineServer?
var connection: NWConnection?
+ private var transactionLog: [UInt32:HotlineTransactionType] = [:]
+
init() {
}
@@ -94,6 +96,10 @@ class HotlineClient : ObservableObject {
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()
@@ -110,7 +116,7 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: waiting for transaction...")
+ print("HotlineClient ⏳")
c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (headerData, context, isComplete, error) in
guard let self = self else {
return
@@ -127,39 +133,40 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: received \(headerData.count) header bytes")
+// 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
+ 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 parameterData = parameterData, !parameterData.isEmpty else {
- print("HotlineClient: transaction parameter data is empty!")
+ guard let fieldData = fieldData, !fieldData.isEmpty else {
+ print("HotlineClient: transaction field data is empty!")
self.disconnect()
return
}
- let parameterCount = parameterData.readUInt16(at: 0)!
+ let fieldCount = fieldData.readUInt16(at: 0)!
- if parameterCount > 0 {
+ if fieldCount > 0 {
var dataCursor = 2
- for _ in 0..<parameterCount {
+ for _ in 0..<fieldCount {
if
- let fieldID = parameterData.readUInt16(at: dataCursor),
- let fieldSize = parameterData.readUInt16(at: dataCursor + 2),
- let fieldData = parameterData.readData(at: dataCursor + 4, length: Int(fieldSize)) {
+ 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.parameters.append(HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData))
-// transaction.parameters[fieldType] = HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData)
+
+ transaction.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData))
+// transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData)
}
else {
- print("HotlineClient: UNKNOWN PARAM TYPE!", fieldID, fieldSize)
+ print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize)
}
dataCursor += 4 + Int(fieldSize)
@@ -201,10 +208,12 @@ class HotlineClient : ObservableObject {
let transactionSize = data.readUInt32(at: 12),
let dataSize = data.readUInt32(at: 16) {
- 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)
}
+ else {
+ print("HotlineClient: Unknown type \(type) parsing with \(dataSize) bytes")
+ }
}
return nil
@@ -249,8 +258,11 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: completed handshake")
- self.sendLogin()
+ print("HotlineClient 🤝")
+ self.sendLogin() { [weak self] in
+ self?.sendSetClientUserInfo()
+ self?.sendGetUserList()
+ }
self.receiveTransaction()
}
})
@@ -262,15 +274,13 @@ class HotlineClient : ObservableObject {
}
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)
+ 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)
- print("HotlineClient: logging in...")
self.sendTransaction(t) { [weak self] in
- print("HotlineClient: logged in!")
DispatchQueue.main.async {
self?.connectionStatus = .loggedIn
}
@@ -279,44 +289,100 @@ class HotlineClient : ObservableObject {
}
}
+ 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.setParameterString(type: .userName, val: self.userName)
- t.setParameterUInt32(type: .userIconID, val: self.userIconID)
- t.setParameterUInt32(type: .options, val: 0)
-
- print("HotlineClient: agreeing")
+ 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.setParameterString(type: .data, val: message)
-
- print("HotlineClient: sending chat...")
+ t.setFieldString(type: .data, val: message)
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 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:
- print("HotlineClient: GOT REPLY TRANSACTION? \(transaction)")
+ self.processReply(transaction)
+// print("HotlineClient: Received reply transaction: \(transaction)")
+
case .chatMessage:
- print("HotlineClient: CHAT MESSAGE!")
- if
- let chatTextParam = transaction.getParameter(type: .data),
+ if
+ let chatTextParam = transaction.getField(type: .data),
let chatText = chatTextParam.getString(),
- let userNameParam = transaction.getParameter(type: .userName),
+ let userNameParam = transaction.getField(type: .userName),
let userName = userNameParam.getString(),
- let userIDParam = transaction.getParameter(type: .userID),
+ let userIDParam = transaction.getField(type: .userID),
let userID = userIDParam.getUInt16() {
print("HotlineClient: \(userName):\(userID): \(chatText)")
DispatchQueue.main.async {
@@ -325,37 +391,42 @@ class HotlineClient : ObservableObject {
}
case .getUserNameList:
print("HotlineClient: GOT USER NAME LIST!")
- let userList = transaction.getParameterList(type: .userInfo)
+ 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.getParameter(type: .userName),
+// print("HotlineClient: user changed")
+ if let p = transaction.getField(type: .userName),
let userName = p.getString() {
- print("HotlineClient: user name \(userName)")
+ print("HotlineClient: User changed \(userName)")
}
case .disconnectMessage:
- print("HotlineClient: DISCONNECTED BY SERVER!")
+ print("HotlineClient ❌")
self.disconnect()
case .showAgreement:
- if let agreementParam = transaction.getParameter(type: .data) {
+ if let noAgreementField = transaction.getField(type: .noServerAgreement) {
+ print("NO AGREEMENT?")
+ }
+ if let agreementParam = transaction.getField(type: .data) {
if let agreementText = agreementParam.getString() {
- print("AGREEMENT:", agreementText)
+ print("\n\n--------------------------\n")
+ print(agreementText)
+ print("\n--------------------------\n\n")
DispatchQueue.main.async {
self.agreement = agreementText
}
- self.sendAgree() {
+// self.sendAgree() {
// self.sendGetUserList()
- }
+// }
}
}
case .userAccess:
- print("HotlineClient: user access transaction.")
+ print("")
default:
- print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.parameters.count) parameters")
- print(transaction.parameters)
+ 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
index d5fe734..5372e78 100644
--- a/Hotline/Network/HotlineProtocol.swift
+++ b/Hotline/Network/HotlineProtocol.swift
@@ -19,15 +19,58 @@ struct HotlineServer: Identifiable, Hashable {
struct HotlineUser: Identifiable, Hashable {
let id: UInt16
- let userName: String
+ 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 {
@@ -46,21 +89,82 @@ struct HotlineUserInfo {
}
}
-struct HotlineTransactionParameter {
+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 data.readUInt8(at: 0)
+ return self.data.readUInt8(at: 0)
}
func getUInt16() -> UInt16? {
- return data.readUInt16(at: 0)
+ return self.data.readUInt16(at: 0)
}
func getUInt32() -> UInt32? {
- return data.readUInt32(at: 0)
+ 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? {
@@ -89,7 +193,7 @@ struct HotlineTransaction {
var totalSize: UInt32 = UInt32(HotlineTransaction.headerSize)
var dataSize: UInt32 = 0
- var parameters: [HotlineTransactionParameter] = []
+ var fields: [HotlineTransactionField] = []
init(type: HotlineTransactionType) {
self.type = type
@@ -105,48 +209,34 @@ struct HotlineTransaction {
self.dataSize = dataSize
}
- mutating func setParameterUInt8(type: HotlineTransactionFieldType, val: UInt8) {
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val)))
-// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val))
+ mutating func setFieldUInt8(type: HotlineTransactionFieldType, val: UInt8) {
+ self.fields.append(HotlineTransactionField(type: type, val: val))
}
- mutating func setParameterUInt16(type: HotlineTransactionFieldType, val: UInt16) {
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)))
-// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val))
+ mutating func setFieldUInt16(type: HotlineTransactionFieldType, val: UInt16) {
+ self.fields.append(HotlineTransactionField(type: type, val: val))
}
- mutating func setParameterUInt32(type: HotlineTransactionFieldType, val: UInt32) {
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)))
-// self.parameters[type] = HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val))
+ mutating func setFieldUInt32(type: HotlineTransactionFieldType, val: UInt32) {
+ self.fields.append(HotlineTransactionField(type: type, val: val))
}
- mutating func setParameterEncodedString(type: HotlineTransactionFieldType, val: String) {
- let encodedVal = String(val.utf8.map { char in
- Character(UnicodeScalar(0xFF - char))
- })
-
- self.setParameterString(type: type, val: encodedVal)
+ mutating func setFieldEncodedString(type: HotlineTransactionFieldType, val: String) {
+ self.fields.append(HotlineTransactionField(type: type, string: val, encrypt: true))
}
- mutating func setParameterString(type: HotlineTransactionFieldType, val: String) {
- var stringData = Data()
-// stringData.appendUInt16(UInt16(val.count))
- stringData.append(contentsOf: val.utf8)
-
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(stringData.count), data: stringData))
-// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(stringData.count), data: stringData)
+ mutating func setFieldString(type: HotlineTransactionFieldType, val: String) {
+ self.fields.append(HotlineTransactionField(type: type, string: val))
}
- func getParameter(type: HotlineTransactionFieldType) -> HotlineTransactionParameter? {
- return self.parameters.first { p in
+ func getField(type: HotlineTransactionFieldType) -> HotlineTransactionField? {
+ return self.fields.first { p in
p.type == type
}
-
-// return self.parameters[type]
}
- func getParameterList(type: HotlineTransactionFieldType) -> [HotlineTransactionParameter] {
- return self.parameters.filter { p in
+ func getFieldList(type: HotlineTransactionFieldType) -> [HotlineTransactionField] {
+ return self.fields.filter { p in
p.type == type
}
}
@@ -164,40 +254,76 @@ struct HotlineTransaction {
data.appendUInt32(self.id)
data.appendUInt32(self.errorCode)
- if self.parameters.count > 0 {
- var parameterData = Data()
- parameterData.appendUInt16(UInt16(self.parameters.count))
- for param in self.parameters {
- parameterData.appendUInt16(param.type.rawValue)
- parameterData.appendUInt16(param.dataSize)
- parameterData.append(param.data)
+ 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(parameterData.count))
- data.appendUInt32(UInt32(parameterData.count))
- data.append(parameterData)
+ data.appendUInt32(UInt32(fieldData.count))
+ data.appendUInt32(UInt32(fieldData.count))
+ data.append(fieldData)
}
else {
- data.appendUInt32(0)
- data.appendUInt32(0)
+ 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 userIconID = 104 // Integer
- case userID = 103 // Integer
- case data = 101 // String
- case userAccess = 110 // 64-bit integer??
- case userFlags = 112
+ 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 bannerID = 161
- case serverName = 162
- case userInfo = 300
+ 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 {