Added to GitHub
This commit is contained in:
parent
a57aecc05b
commit
afec066586
|
|
@ -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
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>adept</groupId>
|
||||
<artifactId>mikrotik</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>mikrotik</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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<Result> 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<Result> execute(Command cmd) throws MikrotikApiException {
|
||||
SyncListener l = new SyncListener();
|
||||
execute(cmd, l);
|
||||
return l.getResults();
|
||||
}
|
||||
|
||||
public List<Result> 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<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) {
|
||||
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<String> lines = new LinkedList<String>();
|
||||
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<Result> 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<Result> results = new LinkedList<Result>();
|
||||
private Error err;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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<String> getQueries() {
|
||||
return queries;
|
||||
|
||||
}
|
||||
|
||||
String getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
List<String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
List<Parameter> getParameters() {
|
||||
return params;
|
||||
}
|
||||
private String cmd;
|
||||
private List<Parameter> params = new LinkedList<Parameter>();
|
||||
private List<String> queries = new LinkedList<String>();
|
||||
private List<String> properties = new LinkedList<String>();
|
||||
private String tag;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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<String> props = new LinkedList<String>();
|
||||
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<String> words = new LinkedList<String>();
|
||||
private String text;
|
||||
private Token token;
|
||||
private Command cmd;
|
||||
private static final Map<String, Token> lookup = new HashMap<String, Token>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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<String, String> {
|
||||
|
||||
public Result() {
|
||||
super(null);
|
||||
this.map = new HashMap<String, String>();
|
||||
}
|
||||
|
||||
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<? extends String, ? extends String> map) {
|
||||
this.map.putAll(map);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public Set<String> keySet() {
|
||||
return map.keySet();
|
||||
}
|
||||
|
||||
public Collection<String> values() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
public Set<Entry<String, String>> entrySet() {
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
private final Map<String, String> map;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)+
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue