API in Swift
Jump to navigation
Jump to search
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 } }