aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-01-12 09:50:18 -0800
committerDustin Mierau <dustin@mierau.me>2024-01-12 09:50:18 -0800
commit2f210991684c87073dd7534d2d0416d91cf50275 (patch)
tree03dc2edb225ab583fbc286de37f927194377a085 /Hotline
parent768bc9136c91b5dc31eef96088e7d1796243d81b (diff)
Experimenting with a simpler stream/delegate bsaed socket library for Tracker listing for now. Added consume methods to Data and [UInt8] arrays to make reading bytes off the wire easier to read..
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift300
-rw-r--r--Hotline/Models/Hotline.swift12
-rw-r--r--Hotline/Shared/NetSocket.swift227
-rw-r--r--Hotline/Utility/FoundationExtensions.swift261
-rw-r--r--Hotline/macOS/TrackerView.swift25
5 files changed, 634 insertions, 191 deletions
diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift
index 350db43..bd5c937 100644
--- a/Hotline/Hotline/HotlineTrackerClient.swift
+++ b/Hotline/Hotline/HotlineTrackerClient.swift
@@ -1,12 +1,6 @@
import Foundation
import Network
-enum HotlineTrackerStatus: Int {
- case disconnected
- case connecting
- case connected
-}
-
struct HotlineTracker: Identifiable, Equatable {
let id: UUID = UUID()
var address: String
@@ -22,197 +16,129 @@ struct HotlineTracker: Identifiable, Equatable {
}
}
+enum HotlineTrackerStatus: Int {
+ case disconnected
+ case connecting
+ case connected
+}
+
+private enum HotlineTrackerStage {
+ case magic
+ case header
+ case listing
+ case done
+}
+
class HotlineTrackerClient {
- static let magicPacket = Data([
+ static let MagicPacket: [UInt8] = [
0x48, 0x54, 0x52, 0x4B, // 'HTRK'
0x00, 0x01 // Version
- ])
+ ]
private var tracker: HotlineTracker
private var connectionStatus: HotlineTrackerStatus = .disconnected
private var servers: [HotlineServer] = []
+
+ private var socket: NetSocket = NetSocket()
+ private var stage: HotlineTrackerStage = .magic
- private var serverAddress: NWEndpoint.Host
- private var serverPort: NWEndpoint.Port
- private var connection: NWConnection?
- private var bytes = Data()
- private var maxDataLength: Int = 0
+ private var serverAddress: String
+ private var serverPort: Int
+ private var expectedDataLength: Int = 0
private var serverCount: Int = 0
+ private var fetchContinuation: CheckedContinuation<[HotlineServer], any Error>?
+
init() {
let t = HotlineTracker("hltracker.com")
self.tracker = t
- self.serverAddress = NWEndpoint.Host(t.address)
- self.serverPort = NWEndpoint.Port(rawValue: t.port)!
+ self.serverAddress = t.address
+ self.serverPort = Int(t.port)
+ self.socket.delegate = self
}
init(tracker: HotlineTracker) {
self.tracker = tracker
- self.serverAddress = NWEndpoint.Host(tracker.address)
- self.serverPort = NWEndpoint.Port(rawValue: tracker.port)!
+ self.serverAddress = tracker.address
+ self.serverPort = Int(tracker.port)
+ self.socket.delegate = self
}
- func fetchServers(address: String, port: Int, callback: (([HotlineServer]) -> Void)? = nil) async -> [HotlineServer] {
- self.serverAddress = NWEndpoint.Host(address)
- self.serverPort = NWEndpoint.Port(rawValue: UInt16(port))!
+ @MainActor func fetchServers(address: String, port: Int) async throws -> [HotlineServer] {
+ self.serverAddress = address
+ self.serverPort = Int(port)
self.reset()
- return await withCheckedContinuation { [weak self] continuation in
- self?.connect { [weak self] in
- continuation.resume(returning: self?.servers ?? [])
- }
+ return try await withCheckedThrowingContinuation { [weak self] continuation in
+ self?.fetchContinuation = continuation
+ self?.connect()
}
}
- private func reset() {
- self.maxDataLength = 0
+ @MainActor func close() {
+ self.socket.close()
+ }
+
+ // MARK: -
+
+ @MainActor private func reset() {
+ self.expectedDataLength = 0
self.serverCount = 0
self.servers = []
}
- private func connect(_ callback: (() -> Void)? = nil) {
- self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp)
- self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
- switch newState {
- case .ready:
- self?.connectionStatus = .connected
- self?.sendMagic()
- case .cancelled:
- self?.connectionStatus = .disconnected
- DispatchQueue.main.async {
- callback?()
- }
- case .failed(let err):
- print("HotlineTrackerClient: Connection error \(err)")
- self?.connectionStatus = .disconnected
- DispatchQueue.main.async {
- callback?()
- }
- default:
- return
- }
- }
+ @MainActor private func connect() {
+ self.socket.close()
self.connectionStatus = .connecting
- self.connection?.start(queue: .global())
- }
-
- func disconnect() {
- self.connection?.cancel()
- self.connection = nil
+ self.socket.connect(host: self.serverAddress, port: self.serverPort)
+ self.socket.write(HotlineTrackerClient.MagicPacket)
}
- private func sendMagic() {
- guard let c = connection else {
- print("HotlineTrackerClient: invalid connection to send magic.")
+ @MainActor private func receiveMagic() {
+ guard self.stage == .magic, self.socket.available >= HotlineTrackerClient.MagicPacket.count else {
return
}
- // let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion]
+ let magic: [UInt8] = self.socket.read(count: HotlineTrackerClient.MagicPacket.count)
- c.send(content: HotlineTrackerClient.magicPacket, completion: .contentProcessed { [weak self] (error) in
- if let err = error {
- print("HotlineTrackerClient: sending magic failed \(err)")
- return
- }
-
- self?.receiveMagic()
- })
- }
-
- private func receiveMagic() {
- guard let c = connection else {
- print("HotlineTrackerClient: invalid connection to receive magic.")
+ if magic != HotlineTrackerClient.MagicPacket {
+ self.socket.close()
return
}
- 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(HotlineTrackerClient.magicPacket) {
- print("HotlineTrackerClient: invalid magic response")
- self.disconnect()
- return
- }
-
- if let error = error {
- print("HotlineTrackerClient: receive error \(error)")
- }
- else {
- self.receiveHeader()
- }
- }
+ self.stage = .header
+ self.receiveHeader()
}
- private func receiveHeader() {
- guard let c = connection else {
- print("HotlineTrackerClient: invalid connection to receive header.")
+ @MainActor private func receiveHeader() {
+ guard self.stage == .header, self.socket.available >= 8 else {
return
}
- c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in
- guard let self = self else {
- return
- }
-
- if let error = error {
- print("HotlineTrackerClient: receive error \(error)")
- self.disconnect()
- return
- }
-
- if let data = data, !data.isEmpty {
- self.maxDataLength = Int(data[2]) * 0xFF + Int(data[3])
- self.maxDataLength -= 4
- self.serverCount = Int(data[4]) * 256 + Int(data[5])
- }
-
- if let error = error {
- print("HotlineTrackerClient: receive error \(error)")
- }
- else {
- self.receiveListing()
- }
+ let header: [UInt8] = self.socket.read(count: 8)
+ if header.count != 8 {
+ return
}
+
+ self.expectedDataLength = Int(header[2]) * 0xFF + Int(header[3])
+ self.expectedDataLength -= 4
+ self.serverCount = Int(header[4]) * 256 + Int(header[5])
+
+ self.stage = .listing
+ self.receiveListing()
}
- private func receiveListing() {
- guard let c = connection else {
- print("HotlineTrackerClient: invalid connection to receive data.")
+ @MainActor private func receiveListing() {
+ guard self.stage == .listing, self.socket.available >= self.expectedDataLength else {
return
}
- 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 {
- self.bytes.append(contentsOf: data)
-
- if bytes.count >= maxDataLength {
- self.parseListing()
- print("HotlineTrackerClient: Found \(self.servers.count) servers on tracker \(self.serverAddress):\(self.serverPort)")
- self.disconnect()
- return
- }
- }
-
- if let error = error {
- print("HotlineTrackerClient: receive error \(error)")
- self.disconnect()
- }
- else {
- print("HotlineTrackerClient: not complete")
- self.receiveListing()
- }
- }
+ self.parseListing(self.socket.read(count: self.expectedDataLength))
}
- private func parseListing() {
+ @MainActor private func parseListing(_ listingBytes: [UInt8]) {
// IP address (4 bytes)
// Port number (2 bytes)
// Number of users (2 bytes)
@@ -222,41 +148,77 @@ class HotlineTrackerClient {
// Description size (1 byte)
// Description (description size)
+ var bytes: [UInt8] = listingBytes
let trackerSeparatorRegex = /^[-]+$/
var foundServers: [HotlineServer] = []
- var cursor = 0
for _ in 1...self.serverCount {
- if self.bytes.count < cursor + 12 {
- print("HotlineTrackerClient: 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 (serverName, nameByteCount) = self.bytes.readPString(at: cursor + 10)
- let (serverDescription, descByteCount) = self.bytes.readPString(at: cursor + 10 + nameByteCount)
+ let ip_1 = bytes.consumeUInt8(),
+ let ip_2 = bytes.consumeUInt8(),
+ let ip_3 = bytes.consumeUInt8(),
+ let ip_4 = bytes.consumeUInt8(),
+ let port = bytes.consumeUInt16(),
+ let _ = bytes.consumeBytes(2),
+ let userCount = bytes.consumeUInt16(),
+ let serverName = bytes.consumePString(),
+ let serverDescription = bytes.consumePString() {
- if let name = serverName,
- let desc = serverDescription {
- // Ignore servers that are just used as dividers in the tracker listing.
- let validName = try? trackerSeparatorRegex.prefixMatch(in: name)
- if validName == nil {
- let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
- foundServers.append(server)
- }
+ // Ignore servers that are just used as dividers in the tracker listing.
+ let validName = try? trackerSeparatorRegex.prefixMatch(in: serverName)
+ if validName == nil {
+ let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: serverName, description: serverDescription)
+ foundServers.append(server)
}
-
- cursor += 10 + nameByteCount + descByteCount
+ }
+ else {
+ print("HotlineTrackerClient: Data isn't long enough for next server")
+ break
}
}
self.servers = foundServers
+
+ self.stage = .done
+ self.socket.close()
+ }
+}
+
+// MARK: -
+
+extension HotlineTrackerClient: NetSocketDelegate {
+ @MainActor func netsocketConnected(socket: NetSocket) {
+ self.connectionStatus = .connected
+ }
+
+ @MainActor func netsocketDisconnected(socket: NetSocket, error: Error?) {
+ self.stage = .magic
+ self.connectionStatus = .disconnected
+
+ let servers = self.servers
+ self.reset()
+
+ if let continuation = self.fetchContinuation {
+ self.fetchContinuation = nil
+ if let err = error {
+ continuation.resume(throwing: err)
+ }
+ else {
+ continuation.resume(returning: servers)
+ }
+ }
+ }
+
+ @MainActor func netsocketReceived(socket: NetSocket, bytes: [UInt8]) {
+ switch self.stage {
+ case .magic:
+ self.receiveMagic()
+ case .header:
+ self.receiveHeader()
+ case .listing:
+ self.receiveListing()
+ case .done:
+ break
+ }
}
}
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index e62ac15..445d09e 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -223,13 +223,13 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
// MARK: -
@MainActor func getServerList(tracker: String, port: Int = HotlinePorts.DefaultTrackerPort) async -> [Server] {
- let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: tracker, port: port)
-
var servers: [Server] = []
- for s in fetchedServers {
- if let serverName = s.name {
- servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)))
+ if let fetchedServers: [HotlineServer] = try? await self.trackerClient.fetchServers(address: tracker, port: port) {
+ for s in fetchedServers {
+ if let serverName = s.name {
+ servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)))
+ }
}
}
@@ -237,7 +237,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
}
@MainActor func disconnectTracker() {
- self.trackerClient.disconnect()
+ self.trackerClient.close()
}
@MainActor func login(server: Server, username: String, iconID: Int, callback: ((Bool) -> Void)? = nil) {
diff --git a/Hotline/Shared/NetSocket.swift b/Hotline/Shared/NetSocket.swift
new file mode 100644
index 0000000..e474f5a
--- /dev/null
+++ b/Hotline/Shared/NetSocket.swift
@@ -0,0 +1,227 @@
+
+// NetSocket.swift
+// A simple delegate based buffered read/write TCP socket.
+// Created by Dustin Mierau
+
+import Foundation
+
+protocol NetSocketDelegate: AnyObject {
+ func netsocketConnected(socket: NetSocket)
+ func netsocketDisconnected(socket: NetSocket, error: Error?)
+ func netsocketReceived(socket: NetSocket, bytes: [UInt8])
+ func netsocketSent(socket: NetSocket, count: Int)
+}
+
+extension NetSocketDelegate {
+ func netsocketConnected(socket: NetSocket) {}
+ func netsocketDisconnected(socket: NetSocket, error: Error?) {}
+ func netsocketReceived(socket: NetSocket, bytes: [UInt8]) {}
+ func netsocketSent(socket: NetSocket, count: Int) {}
+}
+
+enum NetSocketStatus {
+ case disconnected
+ case connecting
+ case connected
+}
+
+final class NetSocket: NSObject, StreamDelegate {
+ weak var delegate: NetSocketDelegate? = nil
+
+ private var output: OutputStream? = nil
+ private var input: InputStream? = nil
+
+ private var outputBuffer: [UInt8] = []
+ private var inputBuffer: [UInt8] = []
+
+ private var readBuffer: [UInt8] = Array(repeating: 0, count: 4 * 1024)
+
+ public func peek() -> [UInt8] { self.inputBuffer }
+ public var available: Int { self.inputBuffer.count }
+
+ private var status: NetSocketStatus = .disconnected
+
+ override init() {}
+
+ @MainActor public func connect(host: String, port: Int) {
+ self.close()
+
+ var outputStream: OutputStream? = nil
+ var inputStream: InputStream? = nil
+
+ self.status = .connecting
+
+ Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream)
+
+ self.input = inputStream
+ self.output = outputStream
+
+ inputStream?.delegate = self
+ outputStream?.delegate = self
+
+ inputStream?.schedule(in: .current, forMode: .common)
+ outputStream?.schedule(in: .current, forMode: .common)
+
+ inputStream?.open()
+ outputStream?.open()
+ }
+
+ @MainActor public func close(_ err: Error? = nil) {
+ let disconnected = (self.status != .disconnected)
+
+ self.status = .disconnected
+
+ self.input?.delegate = nil
+ self.output?.delegate = nil
+ self.input?.close()
+ self.output?.close()
+ self.input?.remove(from: .current, forMode: .common)
+ self.output?.remove(from: .current, forMode: .common)
+ self.input = nil
+ self.output = nil
+ self.inputBuffer = []
+ self.outputBuffer = []
+
+ if disconnected {
+ self.delegate?.netsocketDisconnected(socket: self, error: err)
+ }
+
+ print("NetSocket: Closed")
+ }
+
+ @MainActor public func write(_ data: Data) {
+ guard let output = self.output else {
+ return
+ }
+
+ self.outputBuffer.append(contentsOf: data)
+
+ if output.hasSpaceAvailable {
+ self.writeBufferToStream()
+ }
+ }
+
+ @MainActor public func write(_ data: [UInt8]) {
+ guard let output = self.output else {
+ return
+ }
+
+ self.outputBuffer.append(contentsOf: data)
+
+ if output.hasSpaceAvailable {
+ self.writeBufferToStream()
+ }
+ }
+
+ @MainActor public func read(count: Int) -> [UInt8] {
+ guard self.inputBuffer.count > 0, count > 0 else {
+ return []
+ }
+
+ let amountToRead = min(count, self.inputBuffer.count)
+ let dataRead: [UInt8] = Array(self.inputBuffer[0..<amountToRead])
+ self.inputBuffer.removeFirst(amountToRead)
+
+ return dataRead
+ }
+
+ @MainActor public func read(count: Int) -> Data {
+ guard self.inputBuffer.count > 0, count > 0 else {
+ return Data()
+ }
+
+ let amountToRead = min(count, self.inputBuffer.count)
+
+ let dataRead: Data = Data(self.inputBuffer[0..<amountToRead])
+ self.inputBuffer.removeFirst(amountToRead)
+
+ return dataRead
+ }
+
+ @MainActor public func readAll() -> [UInt8] {
+ guard self.inputBuffer.count > 0 else {
+ return []
+ }
+
+ let dataRead: [UInt8] = Array(self.inputBuffer)
+ self.inputBuffer = []
+
+ return dataRead
+ }
+
+ @MainActor public func readAll() -> Data {
+ guard self.inputBuffer.count > 0 else {
+ return Data()
+ }
+
+ let dataRead: Data = Data(self.inputBuffer)
+ self.inputBuffer = []
+
+ return dataRead
+ }
+
+ @MainActor private func writeBufferToStream() {
+ guard let output = self.output, self.outputBuffer.count > 0 else {
+ return
+ }
+
+ let bytesWritten = output.write(self.outputBuffer, maxLength: self.outputBuffer.count)
+ if bytesWritten > 0 {
+ self.outputBuffer.removeFirst(bytesWritten)
+ self.delegate?.netsocketSent(socket: self, count: bytesWritten)
+ }
+ else if bytesWritten == -1 {
+ self.close(output.streamError)
+ }
+ }
+
+ @MainActor private func readStreamToBuffer() {
+ guard let input = self.input else {
+ return
+ }
+
+ let bytesRead = input.read(&self.readBuffer, maxLength: self.readBuffer.capacity)
+ if bytesRead > 0 {
+ self.inputBuffer.append(contentsOf: readBuffer[0..<bytesRead])
+ self.delegate?.netsocketReceived(socket: self, bytes: self.inputBuffer)
+ }
+ else if bytesRead == -1 {
+ self.close(input.streamError)
+ }
+ }
+
+ // MARK: -
+
+ @MainActor func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
+ guard let input = self.input, let output = self.output else {
+ return
+ }
+
+ switch eventCode {
+ case .openCompleted:
+ if input.streamStatus == .open && output.streamStatus == .open {
+ if self.status == .connecting {
+ print("NetSocket: Connected")
+ self.status = .connected
+ self.delegate?.netsocketConnected(socket: self)
+ }
+ }
+ break
+ case .hasBytesAvailable:
+ print("NetSocket: Data available")
+ self.readStreamToBuffer()
+ case .hasSpaceAvailable:
+ print("NetSocket: Space available")
+ self.writeBufferToStream()
+ case .endEncountered:
+ print("NetSocket: End encounterd")
+ self.close()
+ case .errorOccurred:
+ print("NetSocket: Error encounterd", input.streamError.debugDescription, output.streamError.debugDescription)
+ let err = input.streamError ?? output.streamError
+ self.close(err)
+ default:
+ break
+ }
+ }
+}
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 3183b39..edb8bee 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -5,6 +5,267 @@ enum Endianness {
case little
}
+extension Array where Element == UInt8 {
+ init(_ val: UInt8) {
+ self.init()
+ self.appendUInt8(val)
+ }
+
+ init(_ val: UInt16) {
+ self.init()
+ self.appendUInt16(val)
+ }
+
+ init(_ val: UInt32) {
+ self.init()
+ self.appendUInt32(val)
+ }
+
+ init(_ val: UInt64) {
+ self.init()
+ self.appendUInt64(val)
+ }
+
+ mutating func consumeUInt8() -> UInt8? {
+ guard let val = self.readUInt8(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(1)
+ return val
+ }
+
+ mutating func consumeUInt16() -> UInt16? {
+ guard let val = self.readUInt16(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(2)
+ return val
+ }
+
+ mutating func consumeUInt32() -> UInt32? {
+ guard let val = self.readUInt32(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(4)
+ return val
+ }
+
+ mutating func consumeUInt64() -> UInt64? {
+ guard let val = self.readUInt64(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(8)
+ return val
+ }
+
+ mutating func consumeBytes(_ length: Int) -> Data? {
+ guard let val = self.readData(at: 0, length: length) else {
+ return nil
+ }
+
+ self.removeFirst(length)
+ return val
+ }
+
+ mutating func consumeDate() -> Date? {
+ guard let date = self.readDate(at: 0) else {
+ return nil
+ }
+
+ self.removeFirst(2 + 2 + 4)
+ return date
+ }
+
+ mutating func consumePString() -> String? {
+ let (str, len) = self.readPString(at: 0)
+ guard let str = str else {
+ return nil
+ }
+ if len == 0 {
+ return ""
+ }
+
+ self.removeFirst(len)
+ return str
+ }
+
+ mutating func consumeString(_ length: Int) -> String? {
+ guard let val = self.readString(at: 0, length: length) else {
+ return nil
+ }
+
+ self.removeFirst(length)
+ return val
+ }
+
+ func readUInt8(at offset: Int) -> UInt8? {
+ guard offset >= 0, offset + 1 <= self.count else {
+ return nil
+ }
+ return self[offset]
+ }
+
+ func readUInt16(at offset: Int) -> UInt16? {
+ guard offset >= 0, offset + 2 <= self.count else {
+ return nil
+ }
+
+ return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1])
+ }
+
+ func readUInt32(at offset: Int) -> UInt32? {
+ guard offset >= 0, offset + 4 <= self.count else {
+ return nil
+ }
+
+ return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3])
+ }
+
+ func readUInt64(at offset: Int) -> UInt64? {
+ guard offset >= 0, offset + 8 <= self.count else {
+ return nil
+ }
+
+ let leftSide: UInt64 = (UInt64(self[offset]) << 56) +
+ (UInt64(self[offset + 1]) << 48) +
+ (UInt64(self[offset + 2]) << 40) +
+ (UInt64(self[offset + 3]) << 32)
+
+ let rightSide: UInt64 = (UInt64(self[offset + 4]) << 24) +
+ (UInt64(self[offset + 5]) << 16) +
+ (UInt64(self[offset + 6]) << 8) +
+ UInt64(self[offset + 7])
+
+ return leftSide + rightSide
+ }
+
+ func readDate(at offset: Int) -> Date? {
+ guard offset >= 0, offset + 2 + 2 + 4 <= self.count else {
+ return nil
+ }
+
+ if
+ let year = self.readUInt16(at: offset),
+ let ms = self.readUInt16(at: offset + 2),
+ let secs = self.readUInt32(at: offset + 2 + 2) {
+ return convertHotlineDate(year: year, seconds: secs, milliseconds: ms)
+ }
+
+ return nil
+ }
+
+ func readData(at offset: Int, length: Int) -> Data? {
+ guard offset >= 0, offset + length <= self.count else {
+ return nil
+ }
+ return Data(self[offset..<(offset + length)])
+ }
+
+ func readString(at offset: Int, length: Int) -> String? {
+ guard let subdata = self.readData(at: offset, length: length) else {
+ return nil
+ }
+
+ if subdata.count == 0 {
+ return ""
+ }
+
+ let allowedEncodings = [
+ NSUTF8StringEncoding,
+ NSShiftJISStringEncoding,
+ NSUnicodeStringEncoding,
+ NSWindowsCP1251StringEncoding
+ ]
+
+ var decodedNSString: NSString?
+ let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
+
+ if allowedEncodings.contains(rawValue) {
+ return decodedNSString as? String
+ }
+
+ else if rawValue > 1 {
+ print("ENCODING FOUND \(rawValue)")
+ }
+
+ var macStr = String(data: subdata, encoding: .macOSRoman)
+ if macStr == nil {
+ macStr = String(data: subdata, encoding: .nonLossyASCII)
+ }
+
+ return macStr
+ }
+
+ func readPString(at offset: Int) -> (String?, Int) {
+ guard offset >= 0, offset + 1 <= self.count else {
+ return (nil, 0)
+ }
+ let len = Int(self.readUInt8(at: offset)!)
+ guard offset + 1 + len <= self.count else {
+ return (nil, 0)
+ }
+ return (self.readString(at: offset+1, length: len), 1 + len)
+ }
+
+ func readLongPString(at offset: Int) -> (String?, Int) {
+ guard offset >= 0, offset + 2 <= self.count else {
+ return (nil, 0)
+ }
+ let len = Int(self.readUInt16(at: offset)!)
+ guard len > 0 else {
+ return ("", 0)
+ }
+ guard offset + 2 + len <= self.count else {
+ return (nil, 0)
+ }
+ return (self.readString(at: offset+2, length: len), len)
+ }
+
+ mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
+ let val = endianness == .big ? value.bigEndian : value.littleEndian
+ self.append(val)
+ }
+
+ mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) {
+ let val = endianness == .big ? value.bigEndian : value.littleEndian
+ let bytes: [UInt8] = [
+ UInt8((val >> 8) & 0xFF),
+ UInt8(val & 0xFF)
+ ]
+ self.append(contentsOf: bytes)
+ }
+
+ mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) {
+ let val = endianness == .big ? value.bigEndian : value.littleEndian
+ let bytes: [UInt8] = [
+ UInt8((val >> 24) & 0xFF),
+ UInt8((val >> 16) & 0xFF),
+ UInt8((val >> 8) & 0xFF),
+ UInt8(val & 0xFF)
+ ]
+ self.append(contentsOf: bytes)
+ }
+
+ mutating func appendUInt64(_ value: UInt64, endianness: Endianness = .big) {
+ let val: UInt64 = endianness == .big ? value.bigEndian : value.littleEndian
+ let bytes: [UInt8] = [
+ UInt8((val >> 56) & 0xFF),
+ UInt8((val >> 48) & 0xFF),
+ UInt8((val >> 40) & 0xFF),
+ UInt8((val >> 32) & 0xFF),
+ UInt8((val >> 24) & 0xFF),
+ UInt8((val >> 16) & 0xFF),
+ UInt8((val >> 8) & 0xFF),
+ UInt8(val & 0xFF)
+ ]
+ self.append(contentsOf: bytes)
+ }
+}
+
extension Data {
init(_ val: UInt8) {
self.init()
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index d53b73e..6b8f330 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -81,27 +81,20 @@ class TrackerItem: Identifiable, Hashable {
let client = HotlineTrackerClient()
-
self.loading = true
-// self.servers = []
-
- let fetchedServers: [HotlineServer] = await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort)
-
- client.disconnect()
- var newItems: [TrackerItem] = []
+ if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort) {
+ var newItems: [TrackerItem] = []
- for s in fetchedServers {
- if let serverName = s.name {
-
- let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))
-// let item = TrackerItem(server: server)
-
- newItems.append(TrackerItem(server: server))
+ for s in fetchedServers {
+ if let serverName = s.name {
+ let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))
+ newItems.append(TrackerItem(server: server))
+ }
}
- }
- self.servers = newItems
+ self.servers = newItems
+ }
self.loading = false
}