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
}
}