Refactored to split public API from implementation

This commit is contained in:
GideonLeGrange 2013-12-23 11:37:14 +02:00
parent 93e5eda09d
commit daf1ff9518
15 changed files with 132 additions and 496 deletions

View File

@ -1,4 +1,7 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import me.legrange.mikrotik.MikrotikApiException;
import me.legrange.mikrotik.impl.Error;
/**
* Thrown when the Mikrotik returns an error when receiving our command.

View File

@ -1,18 +1,8 @@
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;
import me.legrange.mikrotik.impl.ApiConnectionImpl;
/**
* The Mikrotik API connection. This is the class used to connect to a remote
@ -20,7 +10,10 @@ import java.util.concurrent.LinkedBlockingQueue;
*
* @author GideonLeGrange
*/
public class ApiConnection {
public abstract class ApiConnection {
public static final int DEFAULT_PORT = 8728;
/**
* Create a new API connection to the give device on the supplied port
@ -28,10 +21,8 @@ public class ApiConnection {
* @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;
public static ApiConnection connect(String host, int port) throws MikrotikApiException {
return ApiConnectionImpl.connect(host, port);
}
/**
@ -39,7 +30,7 @@ public class ApiConnection {
* @param host The host to which to connect.
* @return The ApiConnection
*/
public static ApiConnection connect(String host) throws ApiConnectionException {
public static ApiConnection connect(String host) throws MikrotikApiException {
return connect(host, DEFAULT_PORT);
}
@ -48,25 +39,12 @@ public class ApiConnection {
*
* @return if connection is established to router it returns true.
*/
public boolean isConnected() {
return connected;
}
public abstract boolean isConnected();
/**
* 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);
}
}
public abstract void disconnect() throws MikrotikApiException;
/**
* Log in to the remote router.
@ -74,22 +52,13 @@ public class ApiConnection {
* @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<Map<String, String>> list = execute("/login");
Map<String, String> 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);
}
public abstract void login(String username, String password) throws MikrotikApiException, InterruptedException;
/** execute a command and return a list of results.
* @param cmd Command to execute
* @return The list of results
*/
public List<Map<String, String>> execute(String cmd) throws MikrotikApiException {
return execute(Parser.parse(cmd));
}
public abstract List<Map<String, String>> execute(String cmd) throws MikrotikApiException;
/** execute a command and attach a result listener to receive it's results.
*
@ -98,359 +67,9 @@ public class ApiConnection {
* @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);
}
public abstract String execute(String cmd, ResultListener lis) throws MikrotikApiException;
/** cancel a command */
public void cancel(String tag) throws MikrotikApiException {
execute(String.format("/cancel tag=%s", tag)) ;
}
public abstract void cancel(String tag) throws MikrotikApiException;
private List<Map<String, String>> 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<String, ResultListener> listeners = new HashMap<String, ResultListener>();
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<String> lines = new LinkedList<String>();
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<String, String> result) {
results.add(result);
}
private List<Map<String, String>> 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<Map<String, String>> results = new LinkedList<Map<String, String>>();
private MikrotikApiException err;
}
}

View File

@ -6,11 +6,11 @@ package me.legrange.mikrotik;
*/
public class ApiConnectionException extends MikrotikApiException {
ApiConnectionException(String msg) {
public ApiConnectionException(String msg) {
super(msg);
}
ApiConnectionException(String msg, Throwable err) {
public ApiConnectionException(String msg, Throwable err) {
super(msg, err);
}

View File

@ -7,11 +7,11 @@ package me.legrange.mikrotik;
*/
public class MikrotikApiException extends Exception {
MikrotikApiException(String msg) {
public MikrotikApiException(String msg) {
super(msg);
}
MikrotikApiException(String msg, Throwable err) {
public MikrotikApiException(String msg, Throwable err) {
super(msg, err);
}
}

View File

@ -1,5 +1,6 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import me.legrange.mikrotik.*;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@ -15,12 +16,12 @@ import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
/**
* The Mikrotik API connection. This is the class used to connect to a remote
* The Mikrotik API connection implementation. This is the class used to connect to a remote
* Mikrotik and send commands to it.
*
* @author GideonLeGrange
*/
public class ApiConnection {
public final class ApiConnectionImpl extends ApiConnection {
/**
* Create a new API connection to the give device on the supplied port
@ -29,20 +30,11 @@ public class ApiConnection {
* @return The ApiConnection
*/
public static ApiConnection connect(String host, int port) throws ApiConnectionException {
ApiConnection con = new ApiConnection();
ApiConnectionImpl con = new ApiConnectionImpl();
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.
*
@ -74,7 +66,7 @@ public class ApiConnection {
* @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 {
public void login(String username, String password) throws MikrotikApiException, InterruptedException {
List<Map<String, String>> list = execute("/login");
Map<String, String> res = list.get(0);
String hash = res.get("ret");
@ -128,7 +120,7 @@ public class ApiConnection {
return tag;
}
private ApiConnection() {
private ApiConnectionImpl() {
}
/**

View File

@ -1,4 +1,6 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import me.legrange.mikrotik.MikrotikApiException;
/**
* Thrown if there is a problem unpacking data from the Api.

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import java.util.Arrays;
import java.util.LinkedList;

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
/**
* Internal representation of !done

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
/**
* Used to encapsulate API error information. We need to pass both the message and the tag (if one was used).

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
/**
* A command parameter

View File

@ -1,4 +1,6 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import me.legrange.mikrotik.MikrotikApiException;
/**
* Exception thrown if the parser encounters an error while parsing a command line.

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import java.util.Arrays;
import java.util.HashMap;

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
/**
* Super type of possible API responses

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import java.util.Collection;
import java.util.HashMap;

View File

@ -1,4 +1,4 @@
package me.legrange.mikrotik;
package me.legrange.mikrotik.impl;
import java.io.IOException;
import java.io.InputStream;
@ -7,13 +7,19 @@ import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import me.legrange.mikrotik.ApiConnectionException;
/**
* Utility library that handles the low level encoding required by the Mikrotik API.
* Utility library that handles the low level encoding required by the Mikrotik
* API.
*
* @author GideonLeGrange. Possibly some code by janisk left.
*/
class Util {
final class Util {
/**
* write a command to the output stream
*/
static void write(Command cmd, OutputStream out) throws UnsupportedEncodingException, IOException {
encode(cmd.getCommand(), out);
for (Parameter param : cmd.getParameters()) {
@ -27,8 +33,9 @@ class Util {
if (!props.isEmpty()) {
StringBuilder buf = new StringBuilder("=.proplist=");
for (int i = 0; i < props.size(); ++i) {
if (i > 0)
if (i > 0) {
buf.append(",");
}
buf.append(props.get(i));
}
encode(buf.toString(), out);
@ -39,45 +46,19 @@ class Util {
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 */
/**
* 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 */
/**
* 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);
@ -91,8 +72,9 @@ class Util {
buf[i] = (byte) (c & 0xFF);
}
String res = new String(buf);
if (result.length() > 0)
if (result.length() > 0) {
result.append("\n");
}
result.append(res);
decode(in, result);
}
@ -101,34 +83,6 @@ class Util {
}
}
/** 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
*
@ -173,4 +127,68 @@ class Util {
}
return ret;
}
/**
* 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);
}
/**
* read length bytes from stream and return length of coming word
*/
private static 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;
}
}