From afec06658626a5c5b62d19acdc30b1cc88a569fd Mon Sep 17 00:00:00 2001 From: GideonLeGrange Date: Thu, 11 Jul 2013 15:32:18 +0200 Subject: [PATCH] Added to GitHub --- .gitignore | 36 ++ pom.xml | 25 + .../mikrotik/ApiCommandException.java | 31 ++ .../me/legrange/mikrotik/ApiConnection.java | 430 ++++++++++++++++++ .../mikrotik/ApiConnectionException.java | 19 + .../legrange/mikrotik/ApiDataException.java | 19 + .../legrange/mikrotik/ApiLoginException.java | 18 + .../java/me/legrange/mikrotik/Command.java | 81 ++++ src/main/java/me/legrange/mikrotik/Done.java | 23 + src/main/java/me/legrange/mikrotik/Error.java | 28 ++ .../mikrotik/MikrotikApiException.java | 17 + .../java/me/legrange/mikrotik/Parameter.java | 41 ++ .../me/legrange/mikrotik/ParseException.java | 19 + .../java/me/legrange/mikrotik/Parser.java | 208 +++++++++ .../java/me/legrange/mikrotik/Response.java | 28 ++ .../legrange/mikrotik/ResponseListener.java | 17 + .../java/me/legrange/mikrotik/Result.java | 78 ++++ .../me/legrange/mikrotik/ResultListener.java | 11 + src/main/java/me/legrange/mikrotik/Util.java | 177 +++++++ src/main/java/me/legrange/mikrotik/bnf.txt | 12 + 20 files changed, 1318 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/me/legrange/mikrotik/ApiCommandException.java create mode 100644 src/main/java/me/legrange/mikrotik/ApiConnection.java create mode 100644 src/main/java/me/legrange/mikrotik/ApiConnectionException.java create mode 100644 src/main/java/me/legrange/mikrotik/ApiDataException.java create mode 100644 src/main/java/me/legrange/mikrotik/ApiLoginException.java create mode 100644 src/main/java/me/legrange/mikrotik/Command.java create mode 100644 src/main/java/me/legrange/mikrotik/Done.java create mode 100644 src/main/java/me/legrange/mikrotik/Error.java create mode 100644 src/main/java/me/legrange/mikrotik/MikrotikApiException.java create mode 100644 src/main/java/me/legrange/mikrotik/Parameter.java create mode 100644 src/main/java/me/legrange/mikrotik/ParseException.java create mode 100644 src/main/java/me/legrange/mikrotik/Parser.java create mode 100644 src/main/java/me/legrange/mikrotik/Response.java create mode 100644 src/main/java/me/legrange/mikrotik/ResponseListener.java create mode 100644 src/main/java/me/legrange/mikrotik/Result.java create mode 100644 src/main/java/me/legrange/mikrotik/ResultListener.java create mode 100644 src/main/java/me/legrange/mikrotik/Util.java create mode 100644 src/main/java/me/legrange/mikrotik/bnf.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8bff6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ + +# Java +*.class +*.jar +*.war +*.ear + +# Scala +*.class +*.log +# sbt specific +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +# Scala-IDE specific +.scala_dependencies + +# Maven +target/ + +# Netbeans +build/ +dist/ +#nbproject/ + +# IntelliJ +.idea/ + +# Eclipse +.settings/ + +# Project +Mtik.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..600b158 --- /dev/null +++ b/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + adept + mikrotik + 1.0-SNAPSHOT + jar + + mikrotik + http://maven.apache.org + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + diff --git a/src/main/java/me/legrange/mikrotik/ApiCommandException.java b/src/main/java/me/legrange/mikrotik/ApiCommandException.java new file mode 100644 index 0000000..1c4a63d --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ApiCommandException.java @@ -0,0 +1,31 @@ +package me.legrange.mikrotik; + +/** + * Thrown when the Mikrotik returns an error when receiving our command. + * @author GideonLeGrange + */ +public class ApiCommandException extends MikrotikApiException { + + + /** return the tag associated with this exception, if there is one */ + public String getTag() { + return tag; + } + + ApiCommandException(String msg) { + super(msg); + } + + ApiCommandException(String msg, Throwable err) { + super(msg, err); + } + + + ApiCommandException(Error err) { + super(err.getMessage()); + tag = err.getTag(); + } + + private String tag = null; + +} diff --git a/src/main/java/me/legrange/mikrotik/ApiConnection.java b/src/main/java/me/legrange/mikrotik/ApiConnection.java new file mode 100644 index 0000000..08b619a --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ApiConnection.java @@ -0,0 +1,430 @@ +package me.legrange.mikrotik; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * The Mikrotik API connection. This is the class used to connect to a remote + * Mikrotik and send commands to it. + * + * @author GideonLeGrange + */ +public class ApiConnection { + + /** + * Create a new API connection to the give device on the supplied port + */ + public static ApiConnection connect(String host, int port) throws ApiConnectionException { + ApiConnection con = new ApiConnection(); + con.open(host, port); + return con; + } + + /** + * Create a new API connection to the give device on the default API port + */ + public static ApiConnection connect(String host) throws ApiConnectionException { + return connect(host, DEFAULT_PORT); + } + + /** + * State of connection + * + * @return - if connection is established to router it returns true. + */ + public boolean isConnected() { + return connected; + } + + /** + * Disconnect from the remote API + */ + public void disconnect() throws ApiConnectionException { + if (!connected) { + throw new ApiConnectionException(("Not/no longer connected to remote Mikrotik")); + } + connected = false; + reader.interrupt(); + try { + sock.close(); + } catch (IOException ex) { + throw new ApiConnectionException(String.format("Error closing socket: %s", ex.getMessage()), ex); + } + } + + /** cancel a command */ + public void cancel(Command can) throws MikrotikApiException { + Command cmd = new Command("/cancel"); + cmd.addParameter("tag", can.getTag()); + execute(cmd); + } + + /** + * set up method that will log you in + * + * @param name - username of the user on the router + * @param password - password for the user + * @return + */ + public void login(String name, String pwd) throws MikrotikApiException, ApiCommandException, InterruptedException { + List list = execute("/login"); + Result res = list.get(0); + String hash = res.get("ret"); + String chal = Util.hexStrToStr("00") + new String(makePass(pwd)) + Util.hexStrToStr(hash); + chal = Util.hashMD5(chal); + execute("/login name=" + name + " response=00" + chal); + } + + private List execute(Command cmd) throws MikrotikApiException { + SyncListener l = new SyncListener(); + execute(cmd, l); + return l.getResults(); + } + + public List execute(String cmd) throws MikrotikApiException { + return execute(Parser.parse(cmd)); + } + + public Command execute(String cmd, ResultListener lis) throws MikrotikApiException { + return execute(Parser.parse(cmd), lis); + } + + private Command execute(Command cmd, ResultListener lis) throws MikrotikApiException { + String tag = nextTag(); + cmd.setTag(tag); + listeners.put(tag, lis); + try { + Util.write(cmd, out); + } catch (UnsupportedEncodingException ex) { + throw new ApiDataException(ex.getMessage(), ex); + } catch (IOException ex) { + throw new ApiConnectionException(ex.getMessage(), ex); + } + return cmd; + } + + private ApiConnection() { + } + + /** + * Start the API. Connects to the Mikrotik + */ + private void open(String host, int port) throws ApiConnectionException { + try { + InetAddress ia = InetAddress.getByName(host); + if (ia.isReachable(1000)) { + sock = new Socket(ia, port); + in = new DataInputStream(sock.getInputStream()); + out = new DataOutputStream(sock.getOutputStream()); + connected = true; + reader = new Reader(); + reader.setDaemon(true); + reader.start(); + processor = new Processor(); + processor.setDaemon(true); + processor.start(); + } else { + throw new ApiConnectionException(String.format("Host '%s' port %d is uncreachable", host, port)); + } + } catch (UnknownHostException ex) { + connected = false; + throw new ApiConnectionException(String.format("Unknown host '%s'", host), ex); + } catch (IOException ex) { + connected = false; + throw new ApiConnectionException(String.format("Error connecting to '%s': %s", host, ex.getMessage()), ex); + } + } + + private char[] makePass(String pass) { + if (true) { + return pass.toCharArray(); + } + char[] res = new char[pass.length() + 1]; + System.arraycopy(pass.toCharArray(), 0, res, 0, pass.length()); + res[pass.length()] = 0x0; + return res; + } + + private synchronized String nextTag() { + _tag++; + return Integer.toHexString(_tag); + } + + private static final int DEFAULT_PORT = 8728; + private Socket sock = null; + private DataOutputStream out = null; + private DataInputStream in = null; + private boolean connected = false; + private Reader reader; + private Processor processor; + private final Map listeners = new HashMap(); + private Integer _tag = 0; + + /** + * thread to read data from the socket and process it into Strings + */ + private class Reader extends Thread { + + private String take() throws ApiConnectionException, ApiDataException { + Object val = null; + try { + val = queue.take(); + } catch (InterruptedException ex) { + throw new ApiConnectionException("Interrupted while reading data from queue.", ex); + } + if (val instanceof ApiConnectionException) { + throw (ApiConnectionException) val; + } else if (val instanceof ApiDataException) { + throw (ApiDataException) val; + } + return (String) val; + } + + private boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public void run() { + while (connected) { + try { + String s = Util.decode(in); + if (s != null) { + queue.put(s); + } + } catch (ApiDataException ex) { + try { + queue.put(ex); + } catch (InterruptedException ex2) { + } + } catch (ApiConnectionException ex) { + } catch (InterruptedException ex1) { + } + } + } + private LinkedBlockingQueue queue = new LinkedBlockingQueue(40); + } + + /** + * Thread to take the received strings and process it into Result objects + */ + private class Processor extends Thread { + + @Override + public void run() { + while (connected) { + Response res; + try { + res = unpack(); + } catch (ApiCommandException ex) { + String tag = ex.getTag(); + if (tag != null) { + res = new Error(tag, ex.getMessage()); + } else { + continue; + } + } catch (MikrotikApiException ex) { + ex.printStackTrace(); + continue; + } + ResultListener l = listeners.get(res.getTag()); + if (l != null) { + if (res instanceof Result) { + l.receive((Result) res); + } else { + if (res instanceof Done) { + listeners.remove(res.getTag()); + } + if (l instanceof ResponseListener) { + ResponseListener rl = (ResponseListener) l; + if (res instanceof Done) { + rl.completed((Done) res); + } else if (res instanceof Error) { + rl.error((Error) res); + } + } + } + } + } + } + + private void nextLine() throws ApiConnectionException, ApiDataException { + if (lines.isEmpty()) { + String block = reader.take(); + String parts[] = block.split("\n"); + lines.addAll(Arrays.asList(parts)); + } + line = lines.remove(0); + } + + private boolean hasNextLine() { + return !lines.isEmpty() || !reader.isEmpty(); + } + + private Response unpack() throws MikrotikApiException { + if (line == null) { + nextLine(); + } + if (line.equals("!re")) { + return unpackRe(); + } else if (line.equals("!done")) { + return unpackDone(); + } else if (line.equals("!trap")) { + return unpackError(); + } else if (line.equals("!halt")) { + return unpackError(); + } else { + throw new ApiDataException(String.format("Unexpected line '%s'", line)); + } + } + + private Result unpackRe() throws ApiDataException, ApiConnectionException { + nextLine(); + int l = 0; + Result res = new Result(); + while (!line.startsWith(("!"))) { + l++; + if (line.startsWith(("="))) { + String parts[] = line.split("=", 3); + if (parts.length == 3) { + res.put(parts[1], parts[2]); + } else { + throw new ApiDataException(String.format("Malformed line '%s'", line)); + } + } else if (line.startsWith(".tag=")) { + String parts[] = line.split("=", 2); + if (parts.length == 2) { + res.setTag(parts[1]); + } + } else { + throw new ApiDataException(String.format("Unexpected line '%s'", line)); + } + if (hasNextLine()) { + nextLine(); + } else { + line = null; + break; + } + } + return res; + } + + private Done unpackDone() throws MikrotikApiException { + Done done = new Done(null); + if (hasNextLine()) { + nextLine(); + + while (!line.startsWith("!")) { + if (line.startsWith(".tag=")) { + String parts[] = line.split("=", 2); + if (parts.length == 2) { + done.setTag(parts[1]); + } + } else if (line.startsWith(("=ret"))) { + String parts[] = line.split("=", 3); + if (parts.length == 3) { + done.setHash(parts[2]); + } else { + throw new ApiDataException(String.format("Malformed line '%s'", line)); + } + } + if (hasNextLine()) { + nextLine(); + } else { + line = null; + break; + } + } + } + return done; + } + + private Error unpackError() throws MikrotikApiException { + nextLine(); + Error err = new Error(); + if (hasNextLine()) { + while (!line.startsWith("!")) { + if (line.startsWith(".tag=")) { + String parts[] = line.split("=", 2); + if (parts.length == 2) { + err.setTag(parts[1]); + } + } else if (line.startsWith("=message=")) { + err.setMessage(line.split("=", 3)[2]); + } + if (hasNextLine()) { + nextLine(); + } else { + line = null; + break; + } + } + } + return err; + } + + private void queue(Response res) { + String tag = res.getTag(); + if (tag != null) { + ResultListener rl = listeners.get(tag); + if (rl != null) { + if (res instanceof Result) { + rl.receive((Result) res); + } else { + // rl.error((Error)res); + } + } + } + } + private List lines = new LinkedList(); + private String line; + } + + private class SyncListener implements ResponseListener { + + public synchronized void error(Error err) { + this.err = err; + notify(); + } + + public synchronized void completed(Done done) { + if (done.getHash() != null) { + Result res = new Result(); + res.put("ret", done.getHash()); + results.add(res); + } + notify(); + } + + public void receive(Result result) { + results.add(result); + } + + private List getResults() throws ApiCommandException, ApiConnectionException { + try { + synchronized (this) { + wait(); + } + } catch (InterruptedException ex) { + throw new ApiConnectionException(ex.getMessage(), ex); + } + if (err != null) { + throw new ApiCommandException(err); + } + return results; + } + private List results = new LinkedList(); + private Error err; + } +} \ No newline at end of file diff --git a/src/main/java/me/legrange/mikrotik/ApiConnectionException.java b/src/main/java/me/legrange/mikrotik/ApiConnectionException.java new file mode 100644 index 0000000..10f4e35 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ApiConnectionException.java @@ -0,0 +1,19 @@ +package me.legrange.mikrotik; + +/** + * Exception thrown if the Api experiences a connection problem + * @author GideonLeGrange + */ +public class ApiConnectionException extends MikrotikApiException { + + ApiConnectionException(String msg) { + super(msg); + } + + ApiConnectionException(String msg, Throwable err) { + super(msg, err); + } + + + +} diff --git a/src/main/java/me/legrange/mikrotik/ApiDataException.java b/src/main/java/me/legrange/mikrotik/ApiDataException.java new file mode 100644 index 0000000..e2b9de7 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ApiDataException.java @@ -0,0 +1,19 @@ +package me.legrange.mikrotik; + +/** + * Thrown if there is a problem unpacking data from the Api. + * @author GideonLeGrange + */ +public class ApiDataException extends MikrotikApiException { + + ApiDataException(String msg) { + super(msg); + } + + ApiDataException(String msg, Throwable err) { + super(msg, err); + } + + + +} diff --git a/src/main/java/me/legrange/mikrotik/ApiLoginException.java b/src/main/java/me/legrange/mikrotik/ApiLoginException.java new file mode 100644 index 0000000..bc21ac2 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ApiLoginException.java @@ -0,0 +1,18 @@ +package me.legrange.mikrotik; + +/** + * Thrown if the API cannot log in + * @author GideonLeGrange + */ +public class ApiLoginException extends MikrotikApiException { + + ApiLoginException(String msg) { + super(msg); + } + + ApiLoginException(String msg, Throwable err) { + super(msg, err); + } + + +} diff --git a/src/main/java/me/legrange/mikrotik/Command.java b/src/main/java/me/legrange/mikrotik/Command.java new file mode 100644 index 0000000..44fc173 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Command.java @@ -0,0 +1,81 @@ +package me.legrange.mikrotik; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * A command sent to a Mikrotik. This internal class is used to build complex commands + * with parameters, queries and property lists. + * + * @author GideonLeGrange + */ +public class Command { + + @Override + public String toString() { + return String.format("cmd[%s] = %s, params = %s, queries = %s, props=%s ", tag, cmd, params, queries, properties); + } + + Command(String cmd) { + if (!cmd.startsWith("/")) { + cmd = "/" + cmd; + } + this.cmd = cmd; + } + + String getCommand() { + return cmd; + } + + /** + * Add a parameter to a command. + */ + void addParameter(String name, String value) { + params.add(new Parameter(name, value)); + } + + /** + * Add a valueless parameter to the command + */ + void addParameter(Parameter param) { + params.add(param); + } + + /** + * Add a property to include in a result + */ + void addProperty(String... names) { + properties.addAll(Arrays.asList(names)); + } + + void addQuery(String... queries) { + this.queries.addAll(Arrays.asList(queries)); + } + + void setTag(String tag) { + this.tag = tag; + } + + List getQueries() { + return queries; + + } + + String getTag() { + return tag; + } + + List getProperties() { + return properties; + } + + List getParameters() { + return params; + } + private String cmd; + private List params = new LinkedList(); + private List queries = new LinkedList(); + private List properties = new LinkedList(); + private String tag; +} diff --git a/src/main/java/me/legrange/mikrotik/Done.java b/src/main/java/me/legrange/mikrotik/Done.java new file mode 100644 index 0000000..e487aa7 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Done.java @@ -0,0 +1,23 @@ +package me.legrange.mikrotik; + +/** + * Internal representation of !done + * @author GideonLeGrange + */ +public class Done extends Response { + + Done(String tag) { + super(tag); + } + + void setHash(String hash) { + this.hash = hash; + } + + String getHash() { + return hash; + } + + private String hash; + +} diff --git a/src/main/java/me/legrange/mikrotik/Error.java b/src/main/java/me/legrange/mikrotik/Error.java new file mode 100644 index 0000000..6fb384f --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Error.java @@ -0,0 +1,28 @@ +package me.legrange.mikrotik; + +/** + * Used to encapsulate API error information. We need to pass both the message and the tag (if one was used). + * @author GideonLeGrange + */ +public class Error extends Response { + + Error(String tag, String message) { + super(tag); + this.message = message; + } + + Error() { + super(null); + } + + String getMessage() { + return message; + } + void setMessage(String message) { + this.message = message; + } + + private String message; + + +} diff --git a/src/main/java/me/legrange/mikrotik/MikrotikApiException.java b/src/main/java/me/legrange/mikrotik/MikrotikApiException.java new file mode 100644 index 0000000..cd39367 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/MikrotikApiException.java @@ -0,0 +1,17 @@ +package me.legrange.mikrotik; + +/** + * Thrown by the Mikrotik API to indicate errors + * + * @author GideonLeGrange + */ +public class MikrotikApiException extends Exception { + + MikrotikApiException(String msg) { + super(msg); + } + + MikrotikApiException(String msg, Throwable err) { + super(msg, err); + } +} diff --git a/src/main/java/me/legrange/mikrotik/Parameter.java b/src/main/java/me/legrange/mikrotik/Parameter.java new file mode 100644 index 0000000..0937e93 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Parameter.java @@ -0,0 +1,41 @@ +package me.legrange.mikrotik; + +/** + * A command parameter + * + * @author GideonLeGrange + */ +class Parameter { + + @Override + public String toString() { + if (hasValue()) { + return String.format("%s=%s", name, value); + } else { + return name; + } + } + + Parameter(String name, String value) { + this.name = name; + this.value = value; + } + + Parameter(String name) { + this(name, null); + } + + boolean hasValue() { + return value != null; + } + + String getName() { + return name; + } + + String getValue() { + return value; + } + private String name; + private String value; +} diff --git a/src/main/java/me/legrange/mikrotik/ParseException.java b/src/main/java/me/legrange/mikrotik/ParseException.java new file mode 100644 index 0000000..5cb8185 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ParseException.java @@ -0,0 +1,19 @@ +package me.legrange.mikrotik; + +/** + * Exception thrown if the parser encounters an error while parsing a command line. + * @author GideonLeGrange + */ +public class ParseException extends MikrotikApiException { + + ParseException(String msg) { + super(msg); + } + + ParseException(String msg, Throwable err) { + super(msg, err); + } + + + +} diff --git a/src/main/java/me/legrange/mikrotik/Parser.java b/src/main/java/me/legrange/mikrotik/Parser.java new file mode 100644 index 0000000..fc95692 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Parser.java @@ -0,0 +1,208 @@ +package me.legrange.mikrotik; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * Parse the pseudo-command line into command objects. + * @author GideonLeGrange + */ +class Parser { + + /** parse the given bit of text into a Command object */ + static Command parse(String text) throws ParseException { + Parser parser = new Parser(text); + return parser.parse(); + } + + /** run parse on the internal data and return the command object */ + private Command parse() throws ParseException { + next(); + expect(Token.COMMAND); + cmd = new Command(text); + next(); + while (!((token == Token.WHERE) || (token == Token.RETURN) || (token == Token.EOL))) { + param(); + } + if (token == Token.WHERE) { + where(); + } + if (token == Token.RETURN) { + returns(); + } + expect(Token.EOL); + return cmd; + } + + private void param() throws ParseException { + String name = text; + next(); + if (token == Token.EQUALS) { + next(); + expect(Token.NAME); + cmd.addParameter(new Parameter(name, text)); + next(); + } + else { + cmd.addParameter(new Parameter(name)); + } + } + + private void where() throws ParseException { + next(); // swallow the word "where" + expr(); + } + + private void expr() throws ParseException { + expect(Token.NOT, Token.NAME); + switch (token) { + case NOT: + notExpr(); + break; + case NAME: { + String name = text; + next(); + expect(Token.EQUALS, Token.LESS, Token.MORE); + switch (token) { + case EQUALS: + eqExpr(name); + break; + case LESS: + lessExpr(name); + break; + case MORE: + moreExpr(name); + break; + default: + hasExpr(name); + } + } + break; + } + // if you get here, you had a expression, see if you want more. + switch (token) { + case AND : andExpr(); + break; + case OR : orExpr(); + } + } + + private void andExpr() throws ParseException { + next(); // eat and + expr(); + cmd.addQuery("?#&"); + } + + private void orExpr() throws ParseException { + next(); // eat or + expr(); + cmd.addQuery("?#|"); + } + + + private void notExpr() throws ParseException { + next(); // eat not + expr(); + cmd.addQuery("?#!"); + } + + private void eqExpr(String name) { + next(); // eat = + cmd.addQuery(String.format("?%s=%s", name, text)); + next(); + } + + private void lessExpr(String name) { + next(); // eat < + cmd.addQuery(String.format("?<%s=%s", name, text)); + next(); + } + + private void moreExpr(String name) { + next(); // eat > + cmd.addQuery(String.format("?>%s=%s", name, text)); + next(); + } + + private void hasExpr(String name) { + cmd.addQuery(String.format("?%s", name)); + } + + private void returns() throws ParseException { + next(); + expect(Token.NAME); + List props = new LinkedList(); + while (!(token == Token.EOL)) { + if (token != Token.COMMA) { + props.add(text); + } + next(); + } + cmd.addProperty(props.toArray(new String[]{})); + } + + private void expect(Token...tokens) throws ParseException { + for (Token want : tokens) { + if (this.token == want) return; + } + throw new ParseException(String.format("Expected %s but found %s", Arrays.asList(tokens), this.token)); + } + + private void next() { + if (!words.isEmpty()) { + text = words.remove(0); + if (text.startsWith("/")) { + token = Token.COMMAND; + } else { + Token t = lookup.get(text); + if (t != null) { + token = t; + text = ""; + } else { + token = Token.NAME; + } + } + } else { + token = Token.EOL; + text = ""; + } + // System.out.printf("%s: %s\n", token, text); + } + + private Parser(String text) { + text = text.trim(); + StringTokenizer st = new StringTokenizer(text, " \t,=", true); + while (st.hasMoreElements()) { + String t = st.nextToken().trim(); + if (!t.equals("")) { + words.add(t); + } + } + } + private final List words = new LinkedList(); + private String text; + private Token token; + private Command cmd; + private static final Map lookup = new HashMap(); + + private enum Token { + + COMMAND, WHERE, RETURN, EOL, NOT, AND, OR, NAME, EQUALS, MORE, LESS, COMMA; + } + + static { + lookup.put("where", Token.WHERE); + lookup.put("return", Token.RETURN); + lookup.put("not", Token.NOT); + lookup.put("and", Token.AND); + lookup.put("or", Token.OR); + lookup.put("=", Token.EQUALS); + lookup.put(">", Token.MORE); + lookup.put("<", Token.LESS); + lookup.put(",", Token.COMMA); + } +} diff --git a/src/main/java/me/legrange/mikrotik/Response.java b/src/main/java/me/legrange/mikrotik/Response.java new file mode 100644 index 0000000..5e05a02 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Response.java @@ -0,0 +1,28 @@ +package me.legrange.mikrotik; + +/** + * Super type of possible API responses + * + * @author GideonLeGrange + */ +abstract class Response { + + public String getTag() { + return tag; + } + + @Override + public String toString() { + return String.format("%s: tag=%s", getClass().getSimpleName(), tag); + } + + void setTag(String tag) { + this.tag = tag; + } + + protected Response(String tag) { + this.tag = tag; + } + + private String tag; +} diff --git a/src/main/java/me/legrange/mikrotik/ResponseListener.java b/src/main/java/me/legrange/mikrotik/ResponseListener.java new file mode 100644 index 0000000..2c2c8bd --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ResponseListener.java @@ -0,0 +1,17 @@ +package me.legrange.mikrotik; + +/** + * A listener that receives life cycle command events from the Mikrotik API, + * and not just results. + * @author GideonLeGrange + */ +public interface ResponseListener extends ResultListener { + + /** called if the command associated with this listener experiences a trap */ + void error(Error err); + + /** called when the command associated with this listener is done */ + void completed(Done done); + + +} diff --git a/src/main/java/me/legrange/mikrotik/Result.java b/src/main/java/me/legrange/mikrotik/Result.java new file mode 100644 index 0000000..1d42f17 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Result.java @@ -0,0 +1,78 @@ +package me.legrange.mikrotik; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A result from an API command. + * @author GideonLeGrange + */ +public class Result extends Response implements Map { + + public Result() { + super(null); + this.map = new HashMap(); + } + + public String get(String key) { + return map.get(key); + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public String toString() { + return String.format("tag=%s, data=%s", getTag(), map); + } + + public int size() { + return map.size(); + } + + public boolean containsKey(Object o) { + return map.containsKey(o); + } + + public boolean containsValue(Object o) { + return map.containsValue(o); + } + + public String get(Object o) { + return map.get(o); + } + + public String put(String k, String v) { + return map.put(k, v); + } + + public String remove(Object o) { + return map.remove(o); + } + + public void putAll(Map map) { + this.map.putAll(map); + } + + public void clear() { + map.clear(); + } + + public Set keySet() { + return map.keySet(); + } + + public Collection values() { + return map.values(); + } + + public Set> entrySet() { + return map.entrySet(); + } + + private final Map map; + +} diff --git a/src/main/java/me/legrange/mikrotik/ResultListener.java b/src/main/java/me/legrange/mikrotik/ResultListener.java new file mode 100644 index 0000000..ab0b25a --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/ResultListener.java @@ -0,0 +1,11 @@ +package me.legrange.mikrotik; + +/** + * Implement this interface to receive command results from the Mikrotik Api. + * @author GideonLeGrange + */ +public interface ResultListener { + + void receive(Result result); + +} diff --git a/src/main/java/me/legrange/mikrotik/Util.java b/src/main/java/me/legrange/mikrotik/Util.java new file mode 100644 index 0000000..40392fe --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/Util.java @@ -0,0 +1,177 @@ +package me.legrange.mikrotik; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +/** + * Utility library that handles the low level encoding required by the Mikrotik API. + * @author GideonLeGrange. Possibly some code by janisk left. + */ +class Util { + + static void write(Command cmd, OutputStream out) throws UnsupportedEncodingException, IOException { + System.out.println("cmd = '" + cmd + "'"); + encode(cmd.getCommand(), out); + for (Parameter param : cmd.getParameters()) { + encode(String.format("=%s=%s", param.getName(), param.hasValue() ? param.getValue() : "" ), out); + } + String tag = cmd.getTag(); + if ((tag != null) && !tag.equals("")) { + encode(String.format(".tag=%s", tag), out); + } + List props = cmd.getProperties(); + if (!props.isEmpty()) { + StringBuilder buf = new StringBuilder("=.proplist="); + for (int i = 0; i < props.size(); ++i) { + if (i > 0) + buf.append(","); + buf.append(props.get(i)); + } + encode(buf.toString(), out); + } + for (String query : cmd.getQueries()) { + encode(query, out); + } + out.write(0); + } + + /** encode text using Mikrotik's encoding scheme and write it to an output stream. */ + private static void encode(String word, OutputStream out) throws UnsupportedEncodingException, IOException { + byte bytes[] = word.getBytes("US-ASCII"); + int len = bytes.length; + if (len < 0x80) { + out.write(len); + } else if (len < 0x4000) { + len = len | 0x8000; + out.write(len >> 8); + out.write(len); + } else if (len < 0x20000) { + len = len | 0xC00000; + out.write(len >> 16); + out.write(len >> 8); + out.write(len); + } else if (len < 0x10000000) { + len = len | 0xE0000000; + out.write(len >> 24); + out.write(len >> 16); + out.write(len >> 8); + out.write(len); + } else { + out.write(0xF0); + out.write(len >> 24); + out.write(len >> 16); + out.write(len >> 8); + out.write(len); + } + out.write(bytes); + } + + /** decode bytes from an input stream of Mikrotik protocol sentences into text */ + static String decode(InputStream in) throws ApiDataException, ApiConnectionException { + StringBuilder res = new StringBuilder(); + decode(in, res); + return res.toString(); + } + + /** decode bytes from an input stream into Mikrotik protocol sentences */ + private static void decode(InputStream in, StringBuilder result) throws ApiDataException, ApiConnectionException { + try { + int len = readLen(in); + if (len > 0) { + byte buf[] = new byte[len]; + for (int i = 0; i < len; ++i) { + int c = in.read(); + if (c < 0) { + throw new ApiDataException("Truncated data. Expected to read more bytes"); + } + buf[i] = (byte) (c & 0xFF); + } + String res = new String(buf); + if (result.length() > 0) + result.append("\n"); + result.append(res); + decode(in, result); + } + } catch (IOException ex) { + throw new ApiConnectionException(ex.getMessage(), ex); + } + } + + /** read length bytes from stream and return length of coming word */ + static private int readLen(InputStream in) throws IOException { + int c = in.read(); + if (c > 0) { + if ((c & 0x80) == 0) { + } else if ((c & 0xC0) == 0x80) { + c = c & ~0xC0; + c = (c << 8) | in.read(); + } else if ((c & 0xE0) == 0xC0) { + c = c & ~0xE0; + c = (c << 8) | in.read(); + c = (c << 8) | in.read(); + } else if ((c & 0xF0) == 0xE0) { + c = c & ~0xF0; + c = (c << 8) | in.read(); + c = (c << 8) | in.read(); + c = (c << 8) | in.read(); + } else if ((c & 0xF8) == 0xF0) { + c = in.read(); + c = (c << 8) | in.read(); + c = (c << 8) | in.read(); + c = (c << 8) | in.read(); + c = (c << 8) | in.read(); + } + } + return c; + } + + /** + * makes MD5 hash of string for use with RouterOS API + * + * @param s - variable to make hash from + * @return - the md5 hash + */ + static String hashMD5(String s) throws ApiDataException { + MessageDigest algorithm = null; + try { + algorithm = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException nsae) { + throw new ApiDataException("Cannot find MD5 digest algorithm"); + } + byte[] defaultBytes = new byte[s.length()]; + for (int i = 0; i < s.length(); i++) { + defaultBytes[i] = (byte) (0xFF & s.charAt(i)); + } + algorithm.reset(); + algorithm.update(defaultBytes); + byte messageDigest[] = algorithm.digest(); + StringBuilder hexString = new StringBuilder(); + for (int i = 0; i < messageDigest.length; i++) { + String hex = Integer.toHexString(0xFF & messageDigest[i]); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + /** + * converts hex value string to normal strint for use with RouterOS API + * + * @param s - hex string to convert to + * @return - converted string. + */ + static String hexStrToStr(String s) { + String ret = ""; + for (int i = 0; i < s.length(); i += 2) { + ret += (char) Integer.parseInt(s.substring(i, i + 2), 16); + } + return ret; + } +} \ No newline at end of file diff --git a/src/main/java/me/legrange/mikrotik/bnf.txt b/src/main/java/me/legrange/mikrotik/bnf.txt new file mode 100644 index 0000000..9868188 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/bnf.txt @@ -0,0 +1,12 @@ +command = action [ query ] [ return ] +action = ("/" word)+ +query = "where" expr +expr = expr "and" expr | expr "or" expr | "not" expr | hasExpr | eqExpr | lessExpr | moreExpr +hasExpr = name +eqExpr = name "=" value +lessExpr = name "<" value +moreExpr = name ">" value +return = "return" (name)+ + + +