API in Swift: Difference between revisions

From MikroTik Wiki
Jump to navigation Jump to search
(Created page with "==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...")
(No difference)

Revision as of 10:34, 2 May 2018

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
   }

}