Compare commits

..

No commits in common. "master" and "v3.0.5" have entirely different histories.

12 changed files with 88 additions and 224 deletions

View File

@ -1,24 +0,0 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Maven
run: mvn -B package --file pom.xml

View File

@ -6,11 +6,12 @@ This project provides a Java client to manipulate Mikrotik routers using the rem
## Versions
![Java CI with Maven](https://github.com/GideonLeGrange/mikrotik-java/workflows/Java%20CI%20with%20Maven/badge.svg)
[![Build Status](https://travis-ci.org/GideonLeGrange/mikrotik-java.svg?branch=master)](https://travis-ci.org/GideonLeGrange/mikrotik-java)
**The current stable version is 3.0.8**
**The current stable version is 3.0.5**
Version 3.0.5 implements a change in the way `login()` is implemented by RouterOS from version 6.43 onward. See pull request #60 for details.
Version 3.0.8 fixes a null pointer error
### Changes in version 3.0:
@ -35,7 +36,7 @@ Maven users can use the artifact from Maven Central with this dependency:
<dependency>
<groupId>me.legrange</groupId>
<artifactId>mikrotik</artifactId>
<version>3.0.8</version>
<version>3.0.5</version>
</dependency>
```
@ -167,14 +168,6 @@ Change the IP address in the object created by the above example:
con.execute("/interface/gre/set .id=gre1 remote-address=10.0.1.1");
```
### Remove an existing object
And now remove the object:
```java
con.execute("/interface/gre/remove .id=gre1");
```
### Un-setting a variable on an object
Un-setting a variable is a bit different, and you need to use a parameter called `value-name`. This isn't well documented. Let's say you have a firewall rule that was set up like this:
@ -188,6 +181,14 @@ Assuming the rule can be accessed as `.id=*1`, you un-set it by using `value-nam
con.execute("/ip/firewall/filter/unset .id=*1 value-name=time");
```
### Remove an existing object
And now remove the object:
```java
con.execute("/interface/gre/remove .id=gre1");
```
## Asynchronous commands
We can run some commands asynchronously in order to continue receiving updates:
@ -225,6 +226,8 @@ The above command will run and send results asynchronously as they become availa
con.cancel(tag);
```
From version 2.0.0 of the API the error() and completed() methods are part of the ResultListener interface.
## Command timeouts
Command timeouts can be used to make sure that synchronous commands either return or fail within a specific time. Command timeouts are separate from the connection timeout used in ```connect()```, and can be set using ```setTimeout()```. Here is an example:

10
pom.xml
View File

@ -9,7 +9,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>me.legrange</groupId>
<artifactId>mikrotik</artifactId>
<version>3.0.8.1</version>
<version>3.0.5</version>
<packaging>jar</packaging>
<name>Mikrotik API Java Client Library</name>
<url>https://github.com/GideonLeGrange/mikrotik-java</url>
@ -33,8 +33,8 @@
<version>2.3.2</version>
<configuration>
<showDeprecation>true</showDeprecation>
<source>8</source>
<target>8</target>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
@ -73,7 +73,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.7.0</version>
<version>2.9.1</version>
<configuration>
<excludePackageNames>example;*.impl</excludePackageNames>
</configuration>
@ -161,7 +161,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -7,8 +7,8 @@ package examples;
*/
public class Config {
public static final String HOST = "ce2.ter.cpt.adept.za.net";
public static final String USERNAME = "adept";
public static final String PASSWORD = "34ffp9";
public static final String HOST = "192.168.1.1";
public static final String USERNAME = "admin";
public static final String PASSWORD = "";
}

View File

@ -1,33 +0,0 @@
package examples;
import me.legrange.mikrotik.MikrotikApiException;
import java.util.List;
import java.util.Map;
/**
* Example 2: A command that returns results. Print all interfaces
*
* @author gideon
*/
public class NestedExpressions extends Example {
public static void main(String... args) throws Exception {
NestedExpressions ex = new NestedExpressions();
ex.connect();
ex.test("/ip/firewall/nat/print where (src-address=\"192.168.15.52\" or src-address=\"192.168.15.53\")");
ex.test("/ip/firewall/nat/print where chain=api_test and (src-address=192.168.15.52) and action=log ");
ex.test("/ip/firewall/nat/print where chain=api_test and (src-address=192.168.15.53 or src-address=192.168.15.52) and action=log ");
ex.test("/ip/firewall/nat/print where chain=api_test and (src-address=\"192.168.15.53\" or src-address=\"192.168.15.52\") and action=log ");
ex.disconnect();
}
private void test(String cmd) throws MikrotikApiException {
System.out.println("Command: " + cmd);
List<Map<String, String>> results = con.execute(cmd);
for (Map<String, String> result : results) {
System.out.println(result);
}
System.out.println();
}
}

View File

@ -1,28 +0,0 @@
package examples;
import me.legrange.mikrotik.MikrotikApiException;
/**
* Example 1: A very simple command: Reboot the remote router
* @author gideon
*/
public class ScriptCommand extends Example {
public static void main(String...args) throws Exception {
ScriptCommand ex = new ScriptCommand();
ex.connect();
ex.test();
ex.disconnect();
}
private void test() throws MikrotikApiException {
con.execute("/ip/hotspot/user/profile/add name=\"cahyo-cek-2\" idle-timeout=none keepalive-timeout=2m status-autorefresh=1m shared-users=1 add-mac-cookie=true mac-cookie-timeout=3d parent-queue=none rate-limit=512k/512k on-login=:put(\",re,3000,12d,3000,,Enable,\")");
}
///
/*
/ip/hotspot/user/profile/add name="cahyo-cek-2" idle-timeout=none keepalive-timeout=2m status-autorefresh=1m shared-users=1 add-mac-cookie=true mac-cookie-timeout=3d parent-queue=none rate-limit=512k/512k on-login=:put (",re,3000,12d,3000,,Enable,");
/system scheduler add name="usercahyo-cek-2-$user" interval=12:00:00 on-even="/ip hotspot active remove [find user=\"$user\"] ; /ip hotspot user set \"$user\" disable=yes ; /system scheduler remove [find name=usercahyo-cek-2-$user]"
*/
}

View File

@ -1,13 +1,12 @@
command = action [ query ] [ return ]
action = ("/" word)+
query = "where" expr
expr = expr "and" expr | expr "or" expr | "not" expr | hasExpr | eqExpr | lessExpr | moreExpr | notExpr | nestedExpr
expr = expr "and" expr | expr "or" expr | "not" expr | hasExpr | eqExpr | lessExpr | moreExpr | notExpr
hasExpr = name
eqExpr = name "=" value
lessExpr = name "<" value
moreExpr = name ">" value
notExpr = name "!=" value
nestedExpr = "(" expr ")"
return = "return" (name)+

View File

@ -4,40 +4,31 @@ import me.legrange.mikrotik.MikrotikApiException;
/**
* Thrown when the Mikrotik returns an error when receiving our command.
*
* @author GideonLeGrange
*/
public class ApiCommandException extends MikrotikApiException {
private String tag = null;
private int category = 0;
/**
* return the tag associated with this exception, if there is one
*
* @return the tag associated with this exception. Null if there is no tag
*/
/** return the tag associated with this exception, if there is one
* @return the tag associated with this exception. Null if there is no tag*/
public String getTag() {
return tag;
}
ApiCommandException(String msg) {
ApiCommandException(String msg) {
super(msg);
}
ApiCommandException(String msg, Throwable err) {
ApiCommandException(String msg, Throwable err) {
super(msg, err);
}
public int getCategory() {
return category;
}
ApiCommandException(Error err) {
super(err.getMessage());
tag = err.getTag();
category = err.getCategory();
}
private String tag = null;
}

View File

@ -14,7 +14,6 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.SocketFactory;
import me.legrange.mikrotik.ApiConnection;
import me.legrange.mikrotik.ApiConnectionException;
@ -56,10 +55,7 @@ public final class ApiConnectionImpl extends ApiConnection {
if (username.trim().isEmpty()) {
throw new ApiConnectionException("API username cannot be empty");
}
Command cmd = new Command("/login");
cmd.addParameter("name", username);
cmd.addParameter("password", password);
List<Map<String, String>> list = execute(cmd, timeout);
List<Map<String, String>> list = execute("/login name=" + username + " password=" + password);
if (!list.isEmpty()) {
Map<String, String> res = list.get(0);
if (res.containsKey("ret")) {
@ -163,7 +159,8 @@ public final class ApiConnectionImpl extends ApiConnection {
}
private synchronized String nextTag() {
return Integer.toHexString(_tag.incrementAndGet());
_tag++;
return Integer.toHexString(_tag);
}
private Socket sock = null;
@ -173,7 +170,7 @@ public final class ApiConnectionImpl extends ApiConnection {
private Reader reader;
private Processor processor;
private final Map<String, ResultListener> listeners;
private final AtomicInteger _tag = new AtomicInteger(0);
private Integer _tag = 0;
private int timeout = ApiConnection.DEFAULT_COMMAND_TIMEOUT;
/**
@ -186,7 +183,7 @@ public final class ApiConnectionImpl extends ApiConnection {
}
private String take() throws ApiConnectionException, ApiDataException {
Object val;
Object val = null;
try {
val = queue.take();
} catch (InterruptedException ex) {
@ -223,7 +220,7 @@ public final class ApiConnectionImpl extends ApiConnection {
private void put(Object data) {
try {
queue.put(data);
} catch (InterruptedException ignored) {
} catch (InterruptedException ex) {
}
}
@ -248,31 +245,27 @@ public final class ApiConnectionImpl extends ApiConnection {
} catch (ApiCommandException ex) {
String tag = ex.getTag();
if (tag != null) {
res = new Error(tag, ex.getMessage(), ex.getCategory());
res = new Error(tag, ex.getMessage());
} else {
continue;
}
} catch (MikrotikApiException ex) {
continue;
}
if (res.getTag() != null) {
ResultListener l = listeners.get(res.getTag());
if (l != null) {
if (res instanceof Result) {
l.receive((Result) res);
} else if (res instanceof Done) {
if (l instanceof SyncListener) {
((SyncListener) l).completed((Done) res);
} else {
l.completed();
}
listeners.remove(res.getTag());
} else if (res instanceof Error) {
l.error(new ApiCommandException((Error) res));
ResultListener l = listeners.get(res.getTag());
if (l != null) {
if (res instanceof Result) {
l.receive((Result) res);
} else if (res instanceof Done) {
if (l instanceof SyncListener) {
((SyncListener) l).completed((Done) res);
} else {
l.completed();
}
listeners.remove(res.getTag());
} else if (res instanceof Error) {
l.error(new ApiCommandException((Error) res));
}
} else {
nextTag();
}
}
}
@ -280,7 +273,7 @@ public final class ApiConnectionImpl extends ApiConnection {
private void nextLine() throws ApiConnectionException, ApiDataException {
if (lines.isEmpty()) {
String block = reader.take();
String[] parts = block.split("\n");
String parts[] = block.split("\n");
lines.addAll(Arrays.asList(parts));
}
line = lines.remove(0);
@ -293,7 +286,7 @@ public final class ApiConnectionImpl extends ApiConnection {
private String peekLine() throws ApiConnectionException, ApiDataException {
if (lines.isEmpty()) {
String block = reader.take();
String[] parts = block.split("\n");
String parts[] = block.split("\n");
lines.addAll(Arrays.asList(parts));
}
return lines.get(0);
@ -306,10 +299,10 @@ public final class ApiConnectionImpl extends ApiConnection {
switch (line) {
case "!re":
return unpackRe();
case "!empty":
case "!done":
return unpackDone();
case "!trap":
return unpackError();
case "!halt":
return unpackError();
case "":
@ -320,10 +313,12 @@ public final class ApiConnectionImpl extends ApiConnection {
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);
String parts[] = line.split("=", 3);
if (parts.length == 3) {
if (!parts[2].endsWith("\r")) {
res.put(parts[1], unpackResult(parts[2]));
@ -340,7 +335,7 @@ public final class ApiConnectionImpl extends ApiConnection {
throw new ApiDataException(String.format("Malformed line '%s'", line));
}
} else if (line.startsWith(".tag=")) {
String[] parts = line.split("=", 2);
String parts[] = line.split("=", 2);
if (parts.length == 2) {
res.setTag(parts[1]);
}
@ -381,12 +376,12 @@ public final class ApiConnectionImpl extends ApiConnection {
while (!line.startsWith("!")) {
if (line.startsWith(".tag=")) {
String[] parts = line.split("=", 2);
String parts[] = line.split("=", 2);
if (parts.length == 2) {
done.setTag(parts[1]);
}
} else if (line.startsWith(("=ret"))) {
String[] parts = line.split("=", 3);
String parts[] = line.split("=", 3);
if (parts.length == 3) {
done.setHash(parts[2]);
} else {
@ -410,16 +405,13 @@ public final class ApiConnectionImpl extends ApiConnection {
if (hasNextLine()) {
while (!line.startsWith("!")) {
if (line.startsWith(".tag=")) {
String[] parts = line.split("=", 2);
String parts[] = line.split("=", 2);
if (parts.length == 2) {
err.setTag(parts[1]);
}
} else if (line.startsWith("=message=")) {
err.setMessage(line.split("=", 3)[2]);
}
else if (line.startsWith("=category=")) {
err.setCategory(Integer.parseInt(line.split("=", 3)[2]));
}
if (hasNextLine()) {
nextLine();
} else {

View File

@ -2,15 +2,11 @@ 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).
*
* @author GideonLeGrange
*/
class Error extends Response {
private String message;
private int category;
Error(String tag, String message, int category) {
Error(String tag, String message) {
super(tag);
this.message = message;
}
@ -22,16 +18,11 @@ class Error extends Response {
String getMessage() {
return message;
}
void setMessage(String message) {
this.message = message;
}
int getCategory() {
return category;
}
private String message;
void setCategory(int category) {
this.category = category;
}
}

View File

@ -3,27 +3,21 @@ package me.legrange.mikrotik.impl;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import me.legrange.mikrotik.impl.Scanner.Token;
/**
* Parse the pseudo-command line into command objects.
*
* @author GideonLeGrange
*/
class Parser {
/**
* parse the given bit of text into a Command object
*/
/** 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
*/
/** run parse on the internal data and return the command object */
private Command parse() throws ParseException {
command();
while (!is(Token.WHERE, Token.RETURN, Token.EOL)) {
@ -73,7 +67,8 @@ class Parser {
next();
}
cmd.addParameter(new Parameter(name, val.toString()));
} else {
}
else {
cmd.addParameter(new Parameter(name));
}
}
@ -84,7 +79,7 @@ class Parser {
}
private void expr() throws ParseException {
expect(Token.NOT, Token.TEXT, Token.LEFT_BRACKET);
expect(Token.NOT, Token.TEXT);
switch (token) {
case NOT:
notExpr();
@ -97,7 +92,7 @@ class Parser {
case EQUALS:
eqExpr(name);
break;
case NOT_EQUALS:
case NOT_EQUALS :
notExpr(name);
break;
case LESS:
@ -111,36 +106,23 @@ class Parser {
}
}
break;
case LEFT_BRACKET:
nestedExpr();
break;
}
// if you get here, you had a expression, see if you want more.
switch (token) {
case AND:
andExpr();
case AND : andExpr();
break;
case OR:
orExpr();
case OR : orExpr();
break;
}
}
private void nestedExpr() throws ParseException {
expect(Token.LEFT_BRACKET);
next();
expr();
expect(Token.RIGHT_BRACKET);
next();
}
private void andExpr() throws ParseException {
next(); // eat and
expr();
cmd.addQuery("?#&");
}
private void orExpr() throws ParseException {
private void orExpr() throws ParseException {
next(); // eat or
expr();
cmd.addQuery("?#|");
@ -153,7 +135,7 @@ class Parser {
cmd.addQuery("?#!");
}
private void eqExpr(String name) throws ParseException {
private void eqExpr(String name) throws ParseException {
next(); // eat =
expect(Token.TEXT);
cmd.addQuery(String.format("?%s=%s", name, text));
@ -196,21 +178,19 @@ class Parser {
cmd.addProperty(props.toArray(new String[props.size()]));
}
private void expect(Token... tokens) throws ParseException {
private void expect(Token...tokens) throws ParseException {
if (!is(tokens))
throw new ParseException(String.format("Expected %s but found %s at position %d", Arrays.asList(tokens), this.token, scanner.pos()));
}
private boolean is(Token... tokens) {
private boolean is(Token...tokens) {
for (Token want : tokens) {
if (this.token == want) return true;
}
return false;
}
/**
* move to the next token returned by the scanner
*/
/** move to the next token returned by the scanner */
private void next() throws ScanException {
token = scanner.next();
while (token == Token.WS) {

View File

@ -28,7 +28,6 @@ class Scanner {
SLASH("/"), COMMA(","), EOL(), WS, TEXT,
LESS("<"), MORE(">"), EQUALS("="), NOT_EQUALS("!="), PIPE("!"),
LEFT_BRACKET("("), RIGHT_BRACKET(")"),
WHERE, NOT, AND, OR, RETURN;
@Override
@ -81,12 +80,6 @@ class Scanner {
case '=':
nextChar();
return EQUALS;
case '(' :
nextChar();
return LEFT_BRACKET;
case ')' :
nextChar();
return RIGHT_BRACKET;
case '!':
return pipe();
case '"':
@ -121,7 +114,7 @@ class Scanner {
*/
private Token name() throws ScanException {
text = new StringBuilder();
while (!in(c, "[ \t\r\n=<>!)]")) {
while (!in(c, "[ \t\r\n=<>!]")) {
text.append(c);
nextChar();
}