diff --git a/src/main/java/me/legrange/mikrotik/ApiLoginException.java b/src/main/java/me/legrange/mikrotik/ApiLoginException.java deleted file mode 100644 index bc21ac2..0000000 --- a/src/main/java/me/legrange/mikrotik/ApiLoginException.java +++ /dev/null @@ -1,18 +0,0 @@ -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/bnf.txt b/src/main/java/me/legrange/mikrotik/grammar.txt similarity index 100% rename from src/main/java/me/legrange/mikrotik/bnf.txt rename to src/main/java/me/legrange/mikrotik/grammar.txt diff --git a/src/main/java/me/legrange/mikrotik/impl/ApiConnectionImpl.java b/src/main/java/me/legrange/mikrotik/impl/ApiConnectionImpl.java new file mode 100644 index 0000000..d2c2d67 --- /dev/null +++ b/src/main/java/me/legrange/mikrotik/impl/ApiConnectionImpl.java @@ -0,0 +1,456 @@ +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 + * @param host The host to which to connect. + * @param port The TCP port to use. + * @return The ApiConnection + */ + 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.. + * @param host The host to which to connect. + * @return The ApiConnection + */ + public static ApiConnection connect(String host) throws ApiConnectionException { + return connect(host, DEFAULT_PORT); + } + + /** + * Check the 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); + } + } + + /** + * Log in to the remote router. + * + * @param username - username of the user on the router + * @param password - password for the user + */ + public void login(String username, String password) throws MikrotikApiException, ApiCommandException, InterruptedException { + List> list = execute("/login"); + Map res = list.get(0); + String hash = res.get("ret"); + String chal = Util.hexStrToStr("00") + new String(makePass(password)) + Util.hexStrToStr(hash); + chal = Util.hashMD5(chal); + execute("/login name=" + username + " response=00" + chal); + } + + /** execute a command and return a list of results. + * @param cmd Command to execute + * @return The list of results + */ + public List> execute(String cmd) throws MikrotikApiException { + return execute(Parser.parse(cmd)); + } + + /** execute a command and attach a result listener to receive it's results. + * + * @param cmd Command to execute + * @param lis ResultListener that will receive the results + * @return A command object that can be used to cancel the command. + * @throws MikrotikApiException + */ + public String execute(String cmd, ResultListener lis) throws MikrotikApiException { + return execute(Parser.parse(cmd), lis); + } + + /** cancel a command */ + public void cancel(String tag) throws MikrotikApiException { + execute(String.format("/cancel tag=%s", tag)) ; + } + + private List> execute(Command cmd) throws MikrotikApiException { + SyncListener l = new SyncListener(); + execute(cmd, l); + return l.getResults(); + } + + + private String 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 tag; + } + + 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) { + if (rl instanceof SyncListener) { + ((SyncListener)rl).completed((Done)res); + } + else { + rl.completed(); + } + } else if (res instanceof Error) { + rl.error(new ApiCommandException((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(MikrotikApiException ex) { + this.err = ex; + notify(); + } + + public synchronized void completed() { + notify(); + } + + 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(Map result) { + results.add(result); + } + + private List> getResults() throws MikrotikApiException { + try { + synchronized (this) { // don't wait if we already have a result. + if ((err == null) && results.isEmpty()) { + wait(); + } + } + } catch (InterruptedException ex) { + throw new ApiConnectionException(ex.getMessage(), ex); + } + if (err != null) { + throw err; + } + return results; + } + + private List> results = new LinkedList>(); + private MikrotikApiException err; + } +} \ No newline at end of file diff --git a/src/main/java/me/legrange/mikrotik/ApiDataException.java b/src/main/java/me/legrange/mikrotik/impl/ApiDataException.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/ApiDataException.java rename to src/main/java/me/legrange/mikrotik/impl/ApiDataException.java diff --git a/src/main/java/me/legrange/mikrotik/Command.java b/src/main/java/me/legrange/mikrotik/impl/Command.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Command.java rename to src/main/java/me/legrange/mikrotik/impl/Command.java diff --git a/src/main/java/me/legrange/mikrotik/Done.java b/src/main/java/me/legrange/mikrotik/impl/Done.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Done.java rename to src/main/java/me/legrange/mikrotik/impl/Done.java diff --git a/src/main/java/me/legrange/mikrotik/Error.java b/src/main/java/me/legrange/mikrotik/impl/Error.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Error.java rename to src/main/java/me/legrange/mikrotik/impl/Error.java diff --git a/src/main/java/me/legrange/mikrotik/Parameter.java b/src/main/java/me/legrange/mikrotik/impl/Parameter.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Parameter.java rename to src/main/java/me/legrange/mikrotik/impl/Parameter.java diff --git a/src/main/java/me/legrange/mikrotik/ParseException.java b/src/main/java/me/legrange/mikrotik/impl/ParseException.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/ParseException.java rename to src/main/java/me/legrange/mikrotik/impl/ParseException.java diff --git a/src/main/java/me/legrange/mikrotik/Parser.java b/src/main/java/me/legrange/mikrotik/impl/Parser.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Parser.java rename to src/main/java/me/legrange/mikrotik/impl/Parser.java diff --git a/src/main/java/me/legrange/mikrotik/Response.java b/src/main/java/me/legrange/mikrotik/impl/Response.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Response.java rename to src/main/java/me/legrange/mikrotik/impl/Response.java diff --git a/src/main/java/me/legrange/mikrotik/Result.java b/src/main/java/me/legrange/mikrotik/impl/Result.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Result.java rename to src/main/java/me/legrange/mikrotik/impl/Result.java diff --git a/src/main/java/me/legrange/mikrotik/Util.java b/src/main/java/me/legrange/mikrotik/impl/Util.java similarity index 100% rename from src/main/java/me/legrange/mikrotik/Util.java rename to src/main/java/me/legrange/mikrotik/impl/Util.java