API in Swift
Summary
RouterOS API access library written in Swift 3.
Licensing
Code is provided as is and can be freely used freely. I, as a writer of code, am not responsible for anything that may arise from use of this code.
Usage
Simple example how this can be used.
api = ApiConn(address: "192.168.88.1") api!.connect() api!.login(username: "admin", password: "") api!.sendCommand(command: "/ip/address/print"); while(true) { let s = api!.getData() if(!s.isEmpty) { print(s) if(s.contains("!done")) { break } } }
Code
To use this library, start by adding
#import <CommonCrypto/CommonCrypto.h>
to Bridging Header. This is requred for login hashing.
Connection library uses SwiftSocket library. Add this to projcect Podfile or Carthage.
ApiConn.swift
Main file of API class. Consists of 5 classes:
- ApiConn
- Main API class. This is class that will be used by user
- WriteCommand
- Handles message sending to router
- ReadCommand
- Handles message receiving from router
- Queue
- Queue for received messages
- Hasher
- Login message hasher
import Foundation import SwiftSocket class ApiConn { private var socket : TCPClient? private var address = "192.168.88.1" private var port : Int32 = 8728 private var connected = false private var readCommand : ReadCommand? private var writeCommand : WriteCommand? private var running = false /** Constructor of the connection class - parameter address: IP address of the router You want to conenct to - parameter port: port used for connection, ROS default is 8728 */ init (address: String, port: Int32) { self.address = address self.port = port } /** Constructor of the connection class. Uses default ROS port 8728 for connection - parameter address: IP address of the router You want to conenct to */ init (address: String) { self.address = address } /** State of conenction - returns: if connection is established to router it returns true */ func isConnected() -> Bool { return connected } func disconnect() { running = false connected = false readCommand?.stop() socket?.close() } private func listen() { if(connected) { if(readCommand == nil) { readCommand = ReadCommand(socket: socket!) } readCommand?.run() } } /** Sets and exectues command (sends it to RouterOS host connected) - parameter command: command will be sent to RouterOS for example "/ip/address/print\n=follow=" - returns: */ func sendCommand(command : String) -> String { return writeCommand!.setCommand(command).runCommand() } /** Exeecutes already set command. - returns: Status of the command sent */ func runCommand() -> String { return writeCommand?.runCommand() ?? "failed" } /** Tries to fech data that is repllied to commands sent. It will timeout in ~1 second - returns: returns data sent by RouterOS or empty response on timeout */ func getData() -> String { var retry = 100 while((readCommand?.queue.isEmpty)! && retry > 10) { usleep(10000) retry -= 1 } let s = readCommand?.queue.dequeue() return s ?? "" } /** - returns: command that is set at this moment. And will be exectued if runCommand is exectued */ func getCommand() -> String { return (writeCommand?.getCommand())! } /** Set up method that will log you in - parameter name: username of the user on the router - parameter password: password for the user - returns: login status */ func login(username: String, password: String) -> String { sendCommand(command: "/login") var s = getData() if(s.isEmpty) { return "failed read #1" } if(!s.contains("!trap") && s.count > 4) { var tmp = s.components(separatedBy: "\n") if(tmp.count > 1) { tmp = tmp[tmp.count - 1].components(separatedBy: "=ret=") s = "" var transition = tmp[tmp.count - 1] var chal = Data() chal.append(0x00) chal.append(password.data(using: .ascii)!) chal.append(Data(hexStringToBytes(transition)!)) chal = Hasher.MD5(string: chal)! let m = "/login\n=name=\(username)\n=response=00\(bytesToHexString(data: chal))" s = sendCommand(command: m) s = getData() if(s.isEmpty) { return "failed read #2" } if(s.contains("!done")) { if(!s.contains("!trap")) { return "Login successful" } } } } return "Login failed" } /** Open connection socket - returns: connection status */ func connect() -> String { var status = "unknown" socket = TCPClient(address: address, port: port) if(socket?.connect(timeout: 3).isSuccess)! { status = "success" print("c: connected") } else { connected = false running = false print("c: failed") return "failure" } running = true connected = true writeCommand = WriteCommand(socket: socket!) readCommand = ReadCommand(socket: socket!) listen() return status } /** Converts hex string to byte array - parameter string: hex string to convert to - returns: converted string */ func hexStringToBytes(_ string: String) -> [UInt8]? { let length = string.characters.count if length & 1 != 0 { return nil } var bytes = [UInt8]() bytes.reserveCapacity(length/2) var index = string.startIndex for _ in 0..<length/2 { let nextIndex = string.index(index, offsetBy: 2) if let b = UInt8(string[index..<nextIndex], radix: 16) { bytes.append(b) } else { return nil } index = nextIndex } return bytes } /** Converts hex value string to normal string for use with RouterOS API - parameter data: hex string to convert to - parameter separator: string to separate bytes (default is none) - returns: converted string */ func bytesToHexString(data : Data, separator : String = "") -> String { return (data.map { String(format: "%02X", $0) }).joined(separator: separator) } } fileprivate class WriteCommand { var len : [UInt8] = [0] var socket : TCPClient var command = "" /** Creates a new instance of WriteCommand - parameter socket: API socket */ init(socket: TCPClient) { self.socket = socket } func setCommand(_ command: String) -> WriteCommand { self.command = command return self } func getCommand() -> String { return command } private func writeLen(_ command : String) -> Data { let data = command.data(using: String.Encoding.utf8) var len = data?.count ?? 0 var dat = Data() if len < 0x80 { dat.append([UInt8(len)], count: 1) }else if len < 0x4000 { len = len | 0x8000; dat.append(Data(bytes: [UInt8(len >> 8)])) dat.append(Data(bytes: [UInt8(len)])) }else if len < 0x20000 { len = len | 0xC00000; dat.append(Data(bytes: [UInt8(len >> 16)])) dat.append(Data(bytes: [UInt8(len >> 8)])) dat.append(Data(bytes: [UInt8(len)])) } else if len < 0x10000000 { len = len | 0xE0000000; dat.append(Data(bytes: [UInt8(len >> 24)])) dat.append(Data(bytes: [UInt8(len >> 16)])) dat.append(Data(bytes: [UInt8(len >> 8)])) dat.append(Data(bytes: [UInt8(len)])) }else{ dat.append(Data(bytes: [0xF0])) dat.append(Data(bytes: [UInt8(len >> 24)])) dat.append(Data(bytes: [UInt8(len >> 16)])) dat.append(Data(bytes: [UInt8(len >> 8)])) dat.append(Data(bytes: [UInt8(len)])) } return dat } func runCommand() -> String { var ret = Data() if(!command.contains("\n")) { var i = 0 var b = writeLen(command) var retLen = b.count + command.count + 1 ret.append(b) ret.append(command.data(using: String.Encoding.ascii)!) } else { var str = command.components(separatedBy: "\n") var i = 0 var iTmp : [Int] = [] for s in str { let len = writeLen(s).count + s.count iTmp.append(len) } for b in iTmp { i += b } for s in str { var j = 0 var b = writeLen(s) for bb in b { ret.append(bb) } let dat = s.data(using: .utf8) ret.append(dat!) } } ret.append(0) socket.send(data: ret) return "OK" } } fileprivate class ReadCommand { var socket : TCPClient var queue : Queue<String> = Queue() var running = false /** Creates a new instance of ReadCommand - parameter socket: API socket */ init (socket: TCPClient) { self.socket = socket } func stop() { running = false } func run() { running = true DispatchQueue.global(qos: .utility).async { var b : UInt8 = 0 var s = "" var ch : Character? var a : UInt32 = 0 while(self.running) { var sk : UInt32 = 0 let res = self.read() if(!res.1) { continue } a = res.0 if(a != 0 && a > 0) { if(a < 0x80) { sk = UInt32(a) } else { if(a < 0xC0) { a = a << 8 a += self.read().0 sk = UInt32(a) ^ 0x8000 } else { if(a < 0xE0) { for i in 0...1 { a = a << 8 a += self.read().0 } sk = UInt32(a ^ 0xC00000) } else { if (a < 0xF0) { for i in 0...2 { a = a << 8; a += self.read().0 } sk = UInt32(a ^ 0xE0000000); } else { if (a < 0xF8) { a = 0; for i in 0...4 { a = a << 8; a += self.read().0 } } else { } } } } } s += "\n" let len = Int(sk) var rx = self.socket.read(len, timeout: 500) if(rx == nil) { a = 0 } else { let rxd = Data(rx!) a = UInt32(rx!.count) } if(a > 0) { s += String(data: Data(rx!), encoding: String.Encoding.ascii)! } } else if (b == 0xFF) { print("Error. Wrong port?") } else { self.queue.enqueue(s) s = "" } } } } private func read() -> (UInt32,Bool) { let rx = socket.read(1, timeout: 1) if(rx != nil) { return (UInt32(rx![0]),true) } return (0xFF,false) } } /* First-in first-out queue (FIFO) New elements are added to the end of the queue. Dequeuing pulls elements from the front of the queue. Enqueuing and dequeuing are O(1) operations. */ fileprivate class Queue<T> { fileprivate var array = [T?]() fileprivate var head = 0 public var isEmpty: Bool { return count == 0 } public var count: Int { return array.count - head } public func enqueue(_ element: T) { array.append(element) } public func dequeue() -> T? { guard head < array.count, let element = array[head] else { return nil } array[head] = nil head += 1 let percentage = Double(head)/Double(array.count) if array.count > 50 && percentage > 0.25 { array.removeFirst(head) head = 0 } return element } public var front: T? { if isEmpty { return nil } else { return array[head] } } } fileprivate class Hasher { /** Makes MD5 hash of string for use with RouterOS API - parameter string: variable to make hacsh from - returns: hashed string */ static func MD5(string: Data) -> Data? { let messageData = string var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH)) _ = digestData.withUnsafeMutableBytes {digestBytes in messageData.withUnsafeBytes {messageBytes in CC_MD5(messageBytes, CC_LONG(messageData.count), digestBytes) } } return digestData } }