API in Swift

From MikroTik Wiki
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
    }
}