Initial
This commit is contained in:
commit
63f74c67ba
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- You may freely edit this file. See commented blocks below for -->
|
||||||
|
<!-- some examples of how to customize the build. -->
|
||||||
|
<!-- (If you delete it and reopen the project it will be recreated.) -->
|
||||||
|
<!-- By default, only the Clean and Build commands use this build script. -->
|
||||||
|
<!-- Commands such as Run, Debug, and Test only use this build script if -->
|
||||||
|
<!-- the Compile on Save feature is turned off for the project. -->
|
||||||
|
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
|
||||||
|
<!-- in the project's Project Properties dialog box.-->
|
||||||
|
<project name="MikroTikAPI" default="default" basedir=".">
|
||||||
|
<description>Builds, tests, and runs the project MikroTikAPI.</description>
|
||||||
|
<import file="nbproject/build-impl.xml"/>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
There exist several targets which are by default empty and which can be
|
||||||
|
used for execution of your tasks. These targets are usually executed
|
||||||
|
before and after some main targets. They are:
|
||||||
|
|
||||||
|
-pre-init: called before initialization of project properties
|
||||||
|
-post-init: called after initialization of project properties
|
||||||
|
-pre-compile: called before javac compilation
|
||||||
|
-post-compile: called after javac compilation
|
||||||
|
-pre-compile-single: called before javac compilation of single file
|
||||||
|
-post-compile-single: called after javac compilation of single file
|
||||||
|
-pre-compile-test: called before javac compilation of JUnit tests
|
||||||
|
-post-compile-test: called after javac compilation of JUnit tests
|
||||||
|
-pre-compile-test-single: called before javac compilation of single JUnit test
|
||||||
|
-post-compile-test-single: called after javac compilation of single JUunit test
|
||||||
|
-pre-jar: called before JAR building
|
||||||
|
-post-jar: called after JAR building
|
||||||
|
-post-clean: called after cleaning build products
|
||||||
|
|
||||||
|
(Targets beginning with '-' are not intended to be called on their own.)
|
||||||
|
|
||||||
|
Example of inserting an obfuscator after compilation could look like this:
|
||||||
|
|
||||||
|
<target name="-post-compile">
|
||||||
|
<obfuscate>
|
||||||
|
<fileset dir="${build.classes.dir}"/>
|
||||||
|
</obfuscate>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
For list of available properties check the imported
|
||||||
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
|
|
||||||
|
Another way to customize the build is by overriding existing main targets.
|
||||||
|
The targets of interest are:
|
||||||
|
|
||||||
|
-init-macrodef-javac: defines macro for javac compilation
|
||||||
|
-init-macrodef-junit: defines macro for junit execution
|
||||||
|
-init-macrodef-debug: defines macro for class debugging
|
||||||
|
-init-macrodef-java: defines macro for class execution
|
||||||
|
-do-jar: JAR building
|
||||||
|
run: execution of project
|
||||||
|
-javadoc-build: Javadoc generation
|
||||||
|
test-report: JUnit report generation
|
||||||
|
|
||||||
|
An example of overriding the target for project execution could look like this:
|
||||||
|
|
||||||
|
<target name="run" depends="MikroTikAPI-impl.jar">
|
||||||
|
<exec dir="bin" executable="launcher.exe">
|
||||||
|
<arg file="${dist.jar}"/>
|
||||||
|
</exec>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
Notice that the overridden target depends on the jar target and not only on
|
||||||
|
the compile target as the regular run target does. Again, for a list of available
|
||||||
|
properties which you can use, check the target you are overriding in the
|
||||||
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
|
-->
|
||||||
|
</project>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
||||||
|
build.xml.data.CRC32=4d236977
|
||||||
|
build.xml.script.CRC32=a2932288
|
||||||
|
build.xml.stylesheet.CRC32=8064a381@1.75.2.48
|
||||||
|
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
|
||||||
|
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
|
||||||
|
nbproject/build-impl.xml.data.CRC32=4d236977
|
||||||
|
nbproject/build-impl.xml.script.CRC32=06e81169
|
||||||
|
nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
compile.on.save=true
|
||||||
|
do.depend=false
|
||||||
|
do.jar=true
|
||||||
|
javac.debug=true
|
||||||
|
javadoc.preview=true
|
||||||
|
user.properties.file=/home/mh/.netbeans/8.0.2/build.properties
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project-private xmlns="http://www.netbeans.org/ns/project-private/1">
|
||||||
|
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/>
|
||||||
|
<open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2">
|
||||||
|
<group>
|
||||||
|
<file>file:/home/mh/Dvp/prj/MtUtils/MikroTikAPI/src/com/eightOceans/mikroTik/ops/Wireless.java</file>
|
||||||
|
<file>file:/home/mh/Dvp/prj/MtUtils/MikroTikAPI/src/com/eightOceans/mikroTik/sample/TestApp.java</file>
|
||||||
|
<file>file:/home/mh/Dvp/prj/MtUtils/MikroTikAPI/src/com/eightOceans/mikroTik/ops/OPs.java</file>
|
||||||
|
</group>
|
||||||
|
</open-files>
|
||||||
|
</project-private>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
annotation.processing.enabled=true
|
||||||
|
annotation.processing.enabled.in.editor=false
|
||||||
|
annotation.processing.processors.list=
|
||||||
|
annotation.processing.run.all.processors=true
|
||||||
|
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
|
||||||
|
application.title=MikroTikAPI
|
||||||
|
application.vendor=mh
|
||||||
|
build.classes.dir=${build.dir}/classes
|
||||||
|
build.classes.excludes=**/*.java,**/*.form
|
||||||
|
# This directory is removed when the project is cleaned:
|
||||||
|
build.dir=build
|
||||||
|
build.generated.dir=${build.dir}/generated
|
||||||
|
build.generated.sources.dir=${build.dir}/generated-sources
|
||||||
|
# Only compile against the classpath explicitly listed here:
|
||||||
|
build.sysclasspath=ignore
|
||||||
|
build.test.classes.dir=${build.dir}/test/classes
|
||||||
|
build.test.results.dir=${build.dir}/test/results
|
||||||
|
# Uncomment to specify the preferred debugger connection transport:
|
||||||
|
#debug.transport=dt_socket
|
||||||
|
debug.classpath=\
|
||||||
|
${run.classpath}
|
||||||
|
debug.test.classpath=\
|
||||||
|
${run.test.classpath}
|
||||||
|
# Files in build.classes.dir which should be excluded from distribution jar
|
||||||
|
dist.archive.excludes=
|
||||||
|
# This directory is removed when the project is cleaned:
|
||||||
|
dist.dir=dist
|
||||||
|
dist.jar=${dist.dir}/MikroTikAPI.jar
|
||||||
|
dist.javadoc.dir=${dist.dir}/javadoc
|
||||||
|
endorsed.classpath=
|
||||||
|
excludes=
|
||||||
|
includes=**
|
||||||
|
jar.compress=false
|
||||||
|
javac.classpath=
|
||||||
|
# Space-separated list of extra javac options
|
||||||
|
javac.compilerargs=
|
||||||
|
javac.deprecation=false
|
||||||
|
javac.processorpath=\
|
||||||
|
${javac.classpath}
|
||||||
|
javac.source=1.8
|
||||||
|
javac.target=1.8
|
||||||
|
javac.test.classpath=\
|
||||||
|
${javac.classpath}:\
|
||||||
|
${build.classes.dir}
|
||||||
|
javac.test.processorpath=\
|
||||||
|
${javac.test.classpath}
|
||||||
|
javadoc.additionalparam=
|
||||||
|
javadoc.author=false
|
||||||
|
javadoc.encoding=${source.encoding}
|
||||||
|
javadoc.noindex=false
|
||||||
|
javadoc.nonavbar=false
|
||||||
|
javadoc.notree=false
|
||||||
|
javadoc.private=false
|
||||||
|
javadoc.splitindex=true
|
||||||
|
javadoc.use=true
|
||||||
|
javadoc.version=false
|
||||||
|
javadoc.windowtitle=
|
||||||
|
main.class=com.eightOceans.mikroTik.sample.TestApp
|
||||||
|
meta.inf.dir=${src.dir}/META-INF
|
||||||
|
mkdist.disabled=true
|
||||||
|
platform.active=default_platform
|
||||||
|
run.classpath=\
|
||||||
|
${javac.classpath}:\
|
||||||
|
${build.classes.dir}
|
||||||
|
# Space-separated list of JVM arguments used when running the project.
|
||||||
|
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
|
||||||
|
# To set system properties for unit tests define test-sys-prop.name=value:
|
||||||
|
run.jvmargs=
|
||||||
|
run.test.classpath=\
|
||||||
|
${javac.test.classpath}:\
|
||||||
|
${build.test.classes.dir}
|
||||||
|
source.encoding=UTF-8
|
||||||
|
src.dir=src
|
||||||
|
test.src.dir=test
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||||
|
<type>org.netbeans.modules.java.j2seproject</type>
|
||||||
|
<configuration>
|
||||||
|
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||||
|
<name>MikroTikAPI</name>
|
||||||
|
<source-roots>
|
||||||
|
<root id="src.dir"/>
|
||||||
|
</source-roots>
|
||||||
|
<test-roots>
|
||||||
|
<root id="test.src.dir"/>
|
||||||
|
</test-roots>
|
||||||
|
</data>
|
||||||
|
</configuration>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
This lib contains two APIs
|
||||||
|
|
||||||
|
- com.mikroTik.wiki -> This is the java lib from the
|
||||||
|
mikrotik wiki by "janisk". I have extended it for support
|
||||||
|
with SSL-Support (weak and fully validated) and changed the
|
||||||
|
class/visiblity a bit for use in the second api
|
||||||
|
|
||||||
|
- com.eightOceans.mikroTik -> This a API-wrapper around the wiki-api
|
||||||
|
which utilied Futures. There is also a TestApp, which uses a state
|
||||||
|
based App-Flow, which I uses as template for several private utilities.
|
||||||
|
|
||||||
|
* Work in progress *
|
||||||
|
|
||||||
|
MH
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
package com.eightOceans.mikroTik;
|
||||||
|
|
||||||
|
import com.mikroTik.wiki.ApiConn;
|
||||||
|
import com.mikroTik.wiki.Hasher;
|
||||||
|
import com.mikroTik.wiki.ReadCommand;
|
||||||
|
import com.mikroTik.wiki.WriteCommand;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh TODO: Improve listen/getData/readin
|
||||||
|
*/
|
||||||
|
public class ApiConnection {
|
||||||
|
|
||||||
|
|
||||||
|
private Socket sock;
|
||||||
|
private boolean isAuthenticated;
|
||||||
|
|
||||||
|
// Stuff from old API
|
||||||
|
private DataOutputStream out = null;
|
||||||
|
private DataInputStream in = null;
|
||||||
|
private String ipAddress;
|
||||||
|
private ReadCommand readCommand = null;
|
||||||
|
private WriteCommand writeCommand = null;
|
||||||
|
private Thread listener = null;
|
||||||
|
private LinkedBlockingQueue queue = new LinkedBlockingQueue(40);
|
||||||
|
|
||||||
|
private final ExecutorService pool = Executors.newCachedThreadPool();//newFixedThreadPool(10);
|
||||||
|
private static final TrustManager[] ALL_TRUSTING_TRUST_MANAGER = new TrustManager[]{
|
||||||
|
new X509TrustManager() {
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// private static final HostnameVerifier ALL_TRUSTING_HOSTNAME_VERIFIER = new HostnameVerifier() {
|
||||||
|
// public boolean verify(String hostname, SSLSession session) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
private void ensureSockClosed() {
|
||||||
|
try {
|
||||||
|
if (sock != null) {
|
||||||
|
try {
|
||||||
|
sock.close();
|
||||||
|
} catch (IOException ioex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sock = null;
|
||||||
|
isAuthenticated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String getData() throws InterruptedException {
|
||||||
|
String s = (String) queue.take();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void listen() {
|
||||||
|
if (this.isConnected()) {
|
||||||
|
if (readCommand == null) {
|
||||||
|
readCommand = new ReadCommand(in, queue);
|
||||||
|
}
|
||||||
|
listener = new Thread(readCommand);
|
||||||
|
listener.setDaemon(true);
|
||||||
|
listener.setName("listener");
|
||||||
|
listener.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Future<Result> close() {
|
||||||
|
return pool.submit(new Callable<Result>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result call() throws Exception {
|
||||||
|
ensureSockClosed();
|
||||||
|
Result res = new Result();
|
||||||
|
res.Success = true;
|
||||||
|
res.ResultMessage = "OK";
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isAuthenticated() {
|
||||||
|
return isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return sock != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Future<Result> login(String user, String password) {
|
||||||
|
return pool.submit(new Callable<Result>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result call() throws Exception {
|
||||||
|
|
||||||
|
String errMsg = null;
|
||||||
|
String r = sendCommand("/login");
|
||||||
|
String s = "a";
|
||||||
|
try {
|
||||||
|
s = getData();
|
||||||
|
|
||||||
|
if ((!s.contains("!trap")) && (s.length() > 4)) {
|
||||||
|
String[] tmp = s.trim().split("\n");
|
||||||
|
if (tmp.length > 1) {
|
||||||
|
tmp = tmp[1].split("=ret=");
|
||||||
|
s = "";
|
||||||
|
String transition = tmp[tmp.length - 1];
|
||||||
|
String chal = "";
|
||||||
|
chal = Hasher.hexStrToStr("00") + new String(password) + Hasher.hexStrToStr(transition);
|
||||||
|
chal = Hasher.hashMD5(chal);
|
||||||
|
String m = "/login\n=name=" + user + "\n=response=00" + chal;
|
||||||
|
s = sendCommand(m);
|
||||||
|
s = getData();
|
||||||
|
if (s.contains("!done")) {
|
||||||
|
if (s.contains("!trap")) {
|
||||||
|
errMsg = "Error during login";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errMsg = "Error during login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errMsg = "Unexpected response from server";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (/*InterruptedException*/RuntimeException ex) {
|
||||||
|
Logger.getLogger(ApiConn.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
errMsg = "Communication interrupted";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errMsg == null) {
|
||||||
|
isAuthenticated = true;
|
||||||
|
Result res = new Result();
|
||||||
|
res.Success = true;
|
||||||
|
res.ResultMessage = "Login successful";
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
isAuthenticated = false;
|
||||||
|
Result res = new Result();
|
||||||
|
res.Success = false;
|
||||||
|
res.ResultMessage = errMsg;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Future<Result> open(String host, int port, SSLMode sslMode) {
|
||||||
|
return pool.submit(new Callable<Result>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result call() throws Exception {
|
||||||
|
|
||||||
|
ensureSockClosed();
|
||||||
|
|
||||||
|
String message = "(none)";
|
||||||
|
try {
|
||||||
|
InetAddress ia = InetAddress.getByName(host);
|
||||||
|
if (ia.isReachable(1000)) {
|
||||||
|
|
||||||
|
SSLSocketFactory sslSockFact = null;
|
||||||
|
if (sslMode == SSLMode.None) {
|
||||||
|
sock = new Socket(ia, port);
|
||||||
|
} else if (sslMode == SSLMode.Weak) {
|
||||||
|
try {
|
||||||
|
SSLContext sc = SSLContext.getInstance("SSL");
|
||||||
|
sc.init(null, ALL_TRUSTING_TRUST_MANAGER, new java.security.SecureRandom());
|
||||||
|
sslSockFact = sc.getSocketFactory();
|
||||||
|
} catch (NoSuchAlgorithmException | KeyManagementException nex) {
|
||||||
|
message = "Internal error in SSL-Module: " + nex.getMessage();
|
||||||
|
}
|
||||||
|
} else {// if (sslMode == SSLMode.Strict) {
|
||||||
|
sslSockFact = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||||
|
|
||||||
|
}
|
||||||
|
if (sslSockFact != null) {
|
||||||
|
SSLSocket socket = (SSLSocket) sslSockFact.createSocket(ia, port);
|
||||||
|
sock = socket;
|
||||||
|
}
|
||||||
|
message = "Connected";
|
||||||
|
|
||||||
|
in = new DataInputStream(sock.getInputStream());
|
||||||
|
out = new DataOutputStream(sock.getOutputStream());
|
||||||
|
readCommand = new ReadCommand(in, queue);
|
||||||
|
writeCommand = new WriteCommand(out);
|
||||||
|
listen();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
message = "Not reachable";
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException ex) {
|
||||||
|
ensureSockClosed();
|
||||||
|
message = ex.getMessage();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ensureSockClosed();
|
||||||
|
message = ex.getMessage();
|
||||||
|
}
|
||||||
|
Result res = new Result();
|
||||||
|
res.Success = (sock != null);
|
||||||
|
res.ResultMessage = message;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets and exectues command (sends it to RouterOS host connected)
|
||||||
|
*
|
||||||
|
* @param s - command will be sent to RouterOS for example
|
||||||
|
* "/ip/address/print\n=follow="
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String sendCommand(String s) {
|
||||||
|
return writeCommand.setCommand(s).runCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Future<ReadDataResult> readData() {
|
||||||
|
return pool.submit(new Callable<ReadDataResult>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadDataResult call() throws Exception {
|
||||||
|
return readDataSync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ReadDataResult readDataSync() {
|
||||||
|
try {
|
||||||
|
String g = getData();
|
||||||
|
ReadDataResult res = new ReadDataResult();
|
||||||
|
res.Success = true;
|
||||||
|
res.Data = g;
|
||||||
|
return res;
|
||||||
|
} catch (Throwable th) {
|
||||||
|
ReadDataResult res = new ReadDataResult();
|
||||||
|
res.Success = false;
|
||||||
|
res.ResultMessage = "Read failed: " + th.getMessage();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.eightOceans.mikroTik;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class MtSentence {
|
||||||
|
|
||||||
|
private boolean isDone;
|
||||||
|
private boolean isTrap;
|
||||||
|
private Map<String, String> words;
|
||||||
|
|
||||||
|
|
||||||
|
public static MtSentence from(String rawString) {
|
||||||
|
MtSentence s = new MtSentence();
|
||||||
|
String[] words = rawString.split("\n");
|
||||||
|
|
||||||
|
Map<String, String> newWords = new HashMap<>();
|
||||||
|
for (String w : words) {
|
||||||
|
if (w.contains("!done")) {
|
||||||
|
s.isDone = true;
|
||||||
|
break;
|
||||||
|
} else if (w.contains("!trap")) {
|
||||||
|
s.isTrap = true;
|
||||||
|
break;
|
||||||
|
} else if (w.contains("!re")) {
|
||||||
|
|
||||||
|
} else if (w.startsWith("=")) {
|
||||||
|
String[] p = w.substring(1).split("=");
|
||||||
|
if (p.length == 2) {
|
||||||
|
newWords.put(p[0], p[1]);
|
||||||
|
} else {
|
||||||
|
newWords.put(w, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("Unsupported word in rawString: " + w);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
if ((!s.isDone) && (!s.isTrap)) {
|
||||||
|
s.words = newWords;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected MtSentence() {
|
||||||
|
words = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isDone() {
|
||||||
|
return isDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isTrap() {
|
||||||
|
return isTrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return [KEY]=[VALUE] if word was key=value style or [KEY]=null if word does not contain a "="
|
||||||
|
*/
|
||||||
|
public Map<String, String> getWordAsMap() {
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.eightOceans.mikroTik;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class ReadDataResult extends Result {
|
||||||
|
|
||||||
|
public String Data;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.eightOceans.mikroTik;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class Result<T> {
|
||||||
|
|
||||||
|
public boolean Success;
|
||||||
|
public String ResultMessage;
|
||||||
|
public T Result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.eightOceans.mikroTik;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public enum SSLMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No encryption
|
||||||
|
*/
|
||||||
|
None,
|
||||||
|
/**
|
||||||
|
* SSL without certificate check
|
||||||
|
*/
|
||||||
|
Weak,
|
||||||
|
/**
|
||||||
|
* SSL verifiying for valid certificate
|
||||||
|
*/
|
||||||
|
Strict
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.eightOceans.mikroTik;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class Utils {
|
||||||
|
|
||||||
|
public static String createPrivateWifiKey() {
|
||||||
|
String dk = "23a6c7f8abadffdFF01cd02DFfd022dacbdf"; // Sample for length
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Random rnd = new Random();
|
||||||
|
for (int i = 0; i < dk.length() / 2; i++) {
|
||||||
|
int r = rnd.nextInt(255);
|
||||||
|
sb.append(String.format("%02x", r));
|
||||||
|
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String createPresharedWifiKey(int minLen, int maxLen, int minSpecial, int maxSpecial) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Random rnd = new Random();
|
||||||
|
int special = 0;
|
||||||
|
boolean firstPass = true;
|
||||||
|
while (special < minSpecial) {
|
||||||
|
|
||||||
|
if (firstPass) {
|
||||||
|
for (int i = 0; i < maxLen; i++) {
|
||||||
|
int r = 0;
|
||||||
|
char c = ' ';
|
||||||
|
while (true) {
|
||||||
|
c = getPasswordChar(rnd);
|
||||||
|
if (!Character.isAlphabetic(c)) {
|
||||||
|
special++;
|
||||||
|
if (special > maxSpecial) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sb.append(c);
|
||||||
|
if (sb.length() >= minLen) {
|
||||||
|
r = rnd.nextInt(maxLen - minLen+1);
|
||||||
|
if (r == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Second or more passes, merge required num of special chars
|
||||||
|
char c = ' ';
|
||||||
|
for (int j = 0; j < sb.length(); j++) {
|
||||||
|
if (!Character.isAlphabetic(sb.charAt(j))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
c = getPasswordChar(rnd);
|
||||||
|
if (Character.isAlphabetic(c)) {
|
||||||
|
sb.setCharAt(j, c);
|
||||||
|
special++;
|
||||||
|
}
|
||||||
|
if (special > maxSpecial) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firstPass = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user typeable password char. Excludes some hard to enter
|
||||||
|
* characters
|
||||||
|
*
|
||||||
|
* @param rnd
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static Character getPasswordChar(Random rnd) {
|
||||||
|
char c = ' ';
|
||||||
|
|
||||||
|
do {
|
||||||
|
int r = 33 + rnd.nextInt(126 - 33);
|
||||||
|
c = String.format("%c", r).charAt(0);
|
||||||
|
} while ((c == '\'') || (c == '`') || (c == '|') || (c == '\\'));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.eightOceans.mikroTik.ops;
|
||||||
|
|
||||||
|
import com.eightOceans.mikroTik.Utils;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class AccessListEntry {
|
||||||
|
|
||||||
|
public static AccessListEntry createWithDefault() {
|
||||||
|
AccessListEntry e = new AccessListEntry();
|
||||||
|
e.Authentication = true;
|
||||||
|
e.Forwarding = true;
|
||||||
|
e.MinSigStrength = -120;
|
||||||
|
e.MaxSigStrength = 120;
|
||||||
|
e.Algo = "aes-ccm";
|
||||||
|
e.PrivateKey = Utils.createPrivateWifiKey();
|
||||||
|
e.PreSharedKey = Utils.createPresharedWifiKey(12,14,1,2);
|
||||||
|
return e;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String Id;
|
||||||
|
public String MacAddress;
|
||||||
|
public int MinSigStrength;
|
||||||
|
public int MaxSigStrength;
|
||||||
|
public boolean Forwarding;
|
||||||
|
public boolean Authentication;
|
||||||
|
public String Algo;
|
||||||
|
public String PrivateKey;
|
||||||
|
public String PreSharedKey;
|
||||||
|
public String ManagementProtectionKey;
|
||||||
|
public boolean Disabled;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.eightOceans.mikroTik.ops;
|
||||||
|
|
||||||
|
import com.eightOceans.mikroTik.ApiConnection;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class OPs {
|
||||||
|
|
||||||
|
private Wireless wireless;
|
||||||
|
private ApiConnection conn;
|
||||||
|
private final ExecutorService pool = Executors.newCachedThreadPool();//
|
||||||
|
|
||||||
|
|
||||||
|
public OPs(ApiConnection conn) {
|
||||||
|
this.conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Wireless getWireless() {
|
||||||
|
if (wireless == null) {
|
||||||
|
wireless = new Wireless(pool, conn);
|
||||||
|
}
|
||||||
|
return wireless;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package com.eightOceans.mikroTik.ops;
|
||||||
|
|
||||||
|
import com.eightOceans.mikroTik.ApiConnection;
|
||||||
|
import com.eightOceans.mikroTik.MtSentence;
|
||||||
|
import com.eightOceans.mikroTik.ReadDataResult;
|
||||||
|
import com.eightOceans.mikroTik.Result;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class Wireless {
|
||||||
|
|
||||||
|
private ApiConnection conn;
|
||||||
|
private ExecutorService pool;
|
||||||
|
|
||||||
|
|
||||||
|
protected Wireless(ExecutorService pool, ApiConnection conn) {
|
||||||
|
this.conn = conn;
|
||||||
|
this.pool = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Future<Result> createOrUpdate(AccessListEntry e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Future<Result<List<AccessListEntry>>> getAccessListEntries() {
|
||||||
|
return pool.submit(new Callable<Result<List<AccessListEntry>>>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<AccessListEntry>> call() throws Exception {
|
||||||
|
Result<List<AccessListEntry>> res = new Result<>();
|
||||||
|
List<AccessListEntry> resLst = new ArrayList<>();
|
||||||
|
|
||||||
|
conn.sendCommand("/interface/wireless/access-list/getall");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ReadDataResult r = conn.readDataSync();
|
||||||
|
if (!r.Success) {
|
||||||
|
res.Success = false;
|
||||||
|
res.ResultMessage = "Data read failed: " + r.ResultMessage;
|
||||||
|
resLst = null; // Indicate failures
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
MtSentence ms = MtSentence.from(r.Data);
|
||||||
|
if (ms.isTrap()) {
|
||||||
|
res.Success = false;
|
||||||
|
res.ResultMessage = "Data read failed, remote sent trap";
|
||||||
|
resLst = null; // Indicate failures
|
||||||
|
break;
|
||||||
|
} else if (ms.isDone()) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
.id=*1B
|
||||||
|
=mac-address=00:00:00:00:00:01
|
||||||
|
=interface=all
|
||||||
|
=signal-range=-120..120
|
||||||
|
=authentication=true
|
||||||
|
=forwarding=true
|
||||||
|
=ap-tx-limit=0
|
||||||
|
=client-tx-limit=0
|
||||||
|
=private-algo=aes-ccm
|
||||||
|
=private-key=23a6c7f8abadffdFF01cd02DFfd022dacbdf
|
||||||
|
=private-pre-shared-key=asdasdadsasdasdasdasdasd
|
||||||
|
=management-protection-key=
|
||||||
|
=disabled=false
|
||||||
|
=comment=#GEND0001# Vla
|
||||||
|
*/
|
||||||
|
AccessListEntry ale = AccessListEntry.createWithDefault();
|
||||||
|
ale.Algo = ms.getWordAsMap().get("private-algo");
|
||||||
|
ale.Authentication = "true".equals(ms.getWordAsMap().get("authentication"));
|
||||||
|
ale.Disabled = "true".equals(ms.getWordAsMap().get("disabled"));
|
||||||
|
ale.Forwarding = "true".equals(ms.getWordAsMap().get("forwarding"));
|
||||||
|
ale.Id = ms.getWordAsMap().get(".id");
|
||||||
|
ale.MacAddress = ms.getWordAsMap().get("mac-address");
|
||||||
|
ale.ManagementProtectionKey = ms.getWordAsMap().get("management-protection-key");
|
||||||
|
String sigRange = ms.getWordAsMap().get("signal-range");
|
||||||
|
if (sigRange != null) {
|
||||||
|
String[] parts = sigRange.split("\\.\\.");
|
||||||
|
try {
|
||||||
|
if (parts.length == 2) {
|
||||||
|
int min = 0;
|
||||||
|
int max = 0;
|
||||||
|
min = Integer.parseInt(parts[0]);
|
||||||
|
max = Integer.parseInt(parts[1]);
|
||||||
|
ale.MaxSigStrength = max;
|
||||||
|
ale.MinSigStrength = min;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException nex) {
|
||||||
|
res.Success = false;
|
||||||
|
res.ResultMessage = "Remote returns invalid singal sength";
|
||||||
|
resLst = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ale.PreSharedKey = ms.getWordAsMap().get("private-pre-shared-key");
|
||||||
|
ale.PrivateKey = ms.getWordAsMap().get("private-key");
|
||||||
|
resLst.add(ale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resLst != null) { // != null == no failure
|
||||||
|
resLst.add(AccessListEntry.createWithDefault());
|
||||||
|
res.Success = true;
|
||||||
|
res.Result = resLst;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package com.eightOceans.mikroTik.sample;
|
||||||
|
|
||||||
|
import com.eightOceans.mikroTik.ApiConnection;
|
||||||
|
import com.eightOceans.mikroTik.ReadDataResult;
|
||||||
|
import com.eightOceans.mikroTik.Result;
|
||||||
|
import com.eightOceans.mikroTik.SSLMode;
|
||||||
|
import com.eightOceans.mikroTik.ops.AccessListEntry;
|
||||||
|
import com.eightOceans.mikroTik.ops.OPs;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class TestApp {
|
||||||
|
|
||||||
|
private static String user = "admin";
|
||||||
|
private static String pass = "";
|
||||||
|
private static String host = "192.168.61.254";
|
||||||
|
private static int port = 8729;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* @param f
|
||||||
|
* @param timeout
|
||||||
|
* @param startOp
|
||||||
|
* @param resOp
|
||||||
|
* @return On sucess the Result of the Future, on failure Result of the
|
||||||
|
* Future or a basic self generated Result-instance.
|
||||||
|
*/
|
||||||
|
private static <T extends Result> T run(Future<T> f, long timeout, String opInProgressDescr) {
|
||||||
|
if (opInProgressDescr != null) {
|
||||||
|
System.out.println("* " + opInProgressDescr + " ...");
|
||||||
|
}
|
||||||
|
boolean succ;
|
||||||
|
String msg;
|
||||||
|
T res = null;
|
||||||
|
try {
|
||||||
|
res = f.get(timeout, TimeUnit.SECONDS);
|
||||||
|
msg = res.ResultMessage;
|
||||||
|
succ = res.Success;
|
||||||
|
} catch (InterruptedException | ExecutionException | TimeoutException iex) {
|
||||||
|
succ = false;
|
||||||
|
msg = "Operation interrupted/timeout:" + iex.getMessage();
|
||||||
|
}
|
||||||
|
if (msg != null) {
|
||||||
|
if (succ) {
|
||||||
|
System.out.println("- Operation succeeded: " + msg + ".");
|
||||||
|
} else {
|
||||||
|
System.out.println("! Operation FAILED: " + msg + "!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (res == null) {
|
||||||
|
res = (T) new Result();
|
||||||
|
res.Success = succ;
|
||||||
|
res.ResultMessage = msg;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum NextOp {
|
||||||
|
|
||||||
|
CONNECT,
|
||||||
|
LOGIN,
|
||||||
|
TEST1,
|
||||||
|
TEST2,
|
||||||
|
EXIT,
|
||||||
|
UNDEF
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("- Starting test app");
|
||||||
|
ApiConnection conn = new ApiConnection();
|
||||||
|
|
||||||
|
NextOp op = NextOp.CONNECT;
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
NextOp nextOp = NextOp.EXIT;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case CONNECT:
|
||||||
|
if (run(conn.open(host, port, SSLMode.Weak), 7, "Establishing connection").Success) {
|
||||||
|
nextOp = NextOp.LOGIN;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LOGIN:
|
||||||
|
if (run(conn.login(user, pass), 7, "Loggin in").Success) {
|
||||||
|
nextOp = NextOp.TEST2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TEST1:
|
||||||
|
conn.sendCommand("/interface/getall");
|
||||||
|
String d = "";
|
||||||
|
while ((d == null || (d.indexOf("!done") == -1))) {
|
||||||
|
Result res = run(conn.readData(), 7, null);
|
||||||
|
if (res.Success) {
|
||||||
|
d = ((ReadDataResult) res).Data;
|
||||||
|
System.out.println(" Got: " + d);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(" Reply complete");
|
||||||
|
nextOp = NextOp.TEST2;
|
||||||
|
break;
|
||||||
|
case TEST2:
|
||||||
|
try {
|
||||||
|
OPs ops = new OPs(conn);
|
||||||
|
Result<List<AccessListEntry>> r = ops.getWireless().getAccessListEntries().get();
|
||||||
|
System.out.println(r.Result.get(0).PreSharedKey);
|
||||||
|
System.out.println(r.Result.get(0).PrivateKey);
|
||||||
|
System.out.println("XXX");
|
||||||
|
} catch (Throwable th) {
|
||||||
|
th.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case EXIT:
|
||||||
|
run(conn.close(), 7, "Closing connection");
|
||||||
|
System.out.println("Test app done");
|
||||||
|
//
|
||||||
|
// for (int i = 0; i < 100; i++) {
|
||||||
|
// System.out.println(Utils.createPresharedWifiKey(12, 13, 1, 2));
|
||||||
|
// }
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
op = nextOp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
package com.mikroTik.wiki;
|
||||||
|
|
||||||
|
// *** Based on Sources from the MikroTik wiki - Free to use license
|
||||||
|
// *** Orig author apparently "janisk"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This contains connection. Everything should be here,
|
||||||
|
* should operate with this class only
|
||||||
|
*/
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author janisk
|
||||||
|
*/
|
||||||
|
public class ApiConn extends Thread {
|
||||||
|
|
||||||
|
private Socket sock = null;
|
||||||
|
private DataOutputStream out = null;
|
||||||
|
private DataInputStream in = null;
|
||||||
|
private String ipAddress;
|
||||||
|
private int ipPort;
|
||||||
|
private boolean connected = false;
|
||||||
|
private String message = "Not connected";
|
||||||
|
private ReadCommand readCommand = null;
|
||||||
|
private WriteCommand writeCommand = null;
|
||||||
|
private Thread listener = null;
|
||||||
|
LinkedBlockingQueue queue = new LinkedBlockingQueue(40);
|
||||||
|
private SSLMode sslMode;
|
||||||
|
|
||||||
|
public enum SSLMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No encryption
|
||||||
|
*/
|
||||||
|
None,
|
||||||
|
/**
|
||||||
|
* SSL without certificate check
|
||||||
|
*/
|
||||||
|
Weak,
|
||||||
|
/**
|
||||||
|
* SSL verifiying for valid certificate
|
||||||
|
*/
|
||||||
|
Strict
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor of the connection class
|
||||||
|
*
|
||||||
|
* @param ipAddress - IP address of the router you want to conenct to
|
||||||
|
* @param ipPort - port used for connection, ROS default is 8728
|
||||||
|
*/
|
||||||
|
public ApiConn(String ipAddress, int ipPort, SSLMode sslMode) {
|
||||||
|
this.sslMode = sslMode;
|
||||||
|
this.ipAddress = ipAddress;
|
||||||
|
this.ipPort = ipPort;
|
||||||
|
this.setName("settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of connection
|
||||||
|
*
|
||||||
|
* @return - if connection is established to router it returns true.
|
||||||
|
*/
|
||||||
|
public boolean isConnected() {
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnect() throws IOException {
|
||||||
|
listener.interrupt();
|
||||||
|
sock.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listen() {
|
||||||
|
if (this.isConnected()) {
|
||||||
|
if (readCommand == null) {
|
||||||
|
readCommand = new ReadCommand(in, queue);
|
||||||
|
}
|
||||||
|
listener = new Thread(readCommand);
|
||||||
|
listener.setDaemon(true);
|
||||||
|
listener.setName("listener");
|
||||||
|
listener.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to get IP address of the connection. Reads data from socket created.
|
||||||
|
*
|
||||||
|
* @return InetAddress
|
||||||
|
*/
|
||||||
|
public InetAddress getIpAddress() {
|
||||||
|
return sock == null ? null : sock.getInetAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns ip address that socket is asociated with.
|
||||||
|
*
|
||||||
|
* @return InetAddress
|
||||||
|
*/
|
||||||
|
public InetAddress getLocalIpAddress() {
|
||||||
|
return sock == null ? null : sock.getLocalAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket remote port number
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public int getPort() {
|
||||||
|
return sock == null ? null : sock.getPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return local prot used by socket
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public int getLocalPort() {
|
||||||
|
return sock == null ? null : sock.getLocalPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns status message set up bu class.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets and exectues command (sends it to RouterOS host connected)
|
||||||
|
*
|
||||||
|
* @param s - command will be sent to RouterOS for example
|
||||||
|
* "/ip/address/print\n=follow="
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String sendCommand(String s) {
|
||||||
|
return writeCommand.setCommand(s).runCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* exeecutes already set command.
|
||||||
|
*
|
||||||
|
* @return returns status of the command sent
|
||||||
|
*/
|
||||||
|
public String runCommand() {
|
||||||
|
return writeCommand.runCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to fech data that is repllied to commands sent. It will wait till
|
||||||
|
* it can return something.
|
||||||
|
*
|
||||||
|
* @return returns data sent by RouterOS
|
||||||
|
* @throws java.lang.InterruptedException
|
||||||
|
*/
|
||||||
|
public String getData() throws InterruptedException {
|
||||||
|
String s = (String) queue.take();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns command that is set at this moment. And will be exectued if
|
||||||
|
* runCommand is exectued.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getCommand() {
|
||||||
|
return writeCommand.getCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 String login(String name, char[] password) {
|
||||||
|
this.sendCommand("/login");
|
||||||
|
String s = "a";
|
||||||
|
try {
|
||||||
|
s = this.getData();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Logger.getLogger(ApiConn.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return "failed read #1";
|
||||||
|
}
|
||||||
|
if (!s.contains("!trap") && s.length() > 4) {
|
||||||
|
String[] tmp = s.trim().split("\n");
|
||||||
|
if (tmp.length > 1) {
|
||||||
|
tmp = tmp[1].split("=ret=");
|
||||||
|
s = "";
|
||||||
|
String transition = tmp[tmp.length - 1];
|
||||||
|
String chal = "";
|
||||||
|
chal = Hasher.hexStrToStr("00") + new String(password) + Hasher.hexStrToStr(transition);
|
||||||
|
chal = Hasher.hashMD5(chal);
|
||||||
|
String m = "/login\n=name=" + name + "\n=response=00" + chal;
|
||||||
|
s = this.sendCommand(m);
|
||||||
|
try {
|
||||||
|
s = this.getData();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Logger.getLogger(ApiConn.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return "failed read #2";
|
||||||
|
}
|
||||||
|
if (s.contains("!done")) {
|
||||||
|
if (!s.contains("!trap")) {
|
||||||
|
return "Login successful";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "Login failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final TrustManager[] ALL_TRUSTING_TRUST_MANAGER = new TrustManager[]{
|
||||||
|
new X509TrustManager() {
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// private static final HostnameVerifier ALL_TRUSTING_HOSTNAME_VERIFIER = new HostnameVerifier() {
|
||||||
|
// public boolean verify(String hostname, SSLSession session) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
InetAddress ia = InetAddress.getByName(ipAddress);
|
||||||
|
if (ia.isReachable(1000)) {
|
||||||
|
|
||||||
|
SSLSocketFactory sslSockFact = null;
|
||||||
|
if (sslMode == SSLMode.None) {
|
||||||
|
sock = new Socket(ipAddress, ipPort);
|
||||||
|
} else if (sslMode == SSLMode.Weak) {
|
||||||
|
try {
|
||||||
|
SSLContext sc = SSLContext.getInstance("SSL");
|
||||||
|
sc.init(null, ALL_TRUSTING_TRUST_MANAGER, new java.security.SecureRandom());
|
||||||
|
sslSockFact = sc.getSocketFactory();
|
||||||
|
} catch (NoSuchAlgorithmException | KeyManagementException nex) {
|
||||||
|
throw new RuntimeException(nex);
|
||||||
|
}
|
||||||
|
} else {// if (sslMode == SSLMode.Strict) {
|
||||||
|
sslSockFact = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||||
|
|
||||||
|
}
|
||||||
|
if (sslSockFact != null) {
|
||||||
|
SSLSocket socket = (SSLSocket) sslSockFact.createSocket(ipAddress, ipPort);
|
||||||
|
sock = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
//sock = new Socket(ipAddress, ipPort);
|
||||||
|
in = new DataInputStream(sock.getInputStream());
|
||||||
|
out = new DataOutputStream(sock.getOutputStream());
|
||||||
|
connected = true;
|
||||||
|
readCommand = new ReadCommand(in, queue);
|
||||||
|
writeCommand = new WriteCommand(out);
|
||||||
|
this.listen();
|
||||||
|
message = "Connected";
|
||||||
|
} else {
|
||||||
|
message = "Not reachable";
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException ex) {
|
||||||
|
connected = false;
|
||||||
|
message = ex.getMessage();
|
||||||
|
ex.printStackTrace();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
connected = false;
|
||||||
|
message = ex.getMessage();
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.mikroTik.wiki;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author janisk
|
||||||
|
*/
|
||||||
|
public class DataReceiver extends Thread {
|
||||||
|
|
||||||
|
private ApiConn aConn = null;
|
||||||
|
|
||||||
|
public DataReceiver(ApiConn aConn) {
|
||||||
|
this.aConn = aConn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String s = "";
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
s = aConn.getData();
|
||||||
|
if (s != null) {
|
||||||
|
System.out.println(s);
|
||||||
|
if (s.contains("!done")) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Logger.getGlobal().log(Level.SEVERE, null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.mikroTik.wiki;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author janisk
|
||||||
|
*/
|
||||||
|
public class Hasher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* makes MD5 hash of string for use with RouterOS API
|
||||||
|
*
|
||||||
|
* @param s - variable to make hacsh from
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
static public String hashMD5(String s) {
|
||||||
|
String md5val = "";
|
||||||
|
MessageDigest algorithm = null;
|
||||||
|
try {
|
||||||
|
algorithm = MessageDigest.getInstance("MD5");
|
||||||
|
} catch (NoSuchAlgorithmException nsae) {
|
||||||
|
System.out.println("Cannot find digest algorithm");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
StringBuffer hexString = new StringBuffer();
|
||||||
|
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 public 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,139 @@
|
||||||
|
package com.mikroTik.wiki;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CommandRead.java
|
||||||
|
*
|
||||||
|
* Created on 19 June 2007, 10:29
|
||||||
|
*
|
||||||
|
* To change this template, choose Tools | Template Manager
|
||||||
|
* and open the template in the editor.
|
||||||
|
*/
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author janisk
|
||||||
|
*/
|
||||||
|
public class ReadCommand implements Runnable {
|
||||||
|
|
||||||
|
private DataInputStream in = null;
|
||||||
|
LinkedBlockingQueue queue = null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of CommandRead
|
||||||
|
*
|
||||||
|
* @param in - Data input stream of socket
|
||||||
|
* @param queue - data output inteface
|
||||||
|
*/
|
||||||
|
public ReadCommand(DataInputStream in, LinkedBlockingQueue queue) {
|
||||||
|
this.in = in;
|
||||||
|
this.queue = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
byte b = 0;
|
||||||
|
String s = "";
|
||||||
|
char ch;
|
||||||
|
int a = 0;
|
||||||
|
while (true) {
|
||||||
|
int sk = 0;
|
||||||
|
try {
|
||||||
|
a = in.read();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (a != 0 && a > 0) {
|
||||||
|
if (a < 0x80) {
|
||||||
|
sk = a;
|
||||||
|
} else {
|
||||||
|
if (a < 0xC0) {
|
||||||
|
a = a << 8;
|
||||||
|
try {
|
||||||
|
a += in.read();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.getLogger(ReadCommand.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sk = a ^ 0x8000;
|
||||||
|
} else {
|
||||||
|
if (a < 0xE0) {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
a = a << 8;
|
||||||
|
a += in.read();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.getLogger(ReadCommand.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sk = a ^ 0xC00000;
|
||||||
|
} else {
|
||||||
|
if (a < 0xF0) {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
a = a << 8;
|
||||||
|
a += in.read();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.getLogger(ReadCommand.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sk = a ^ 0xE0000000;
|
||||||
|
} else {
|
||||||
|
if (a < 0xF8) {
|
||||||
|
try {
|
||||||
|
a = 0;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
a = a << 8;
|
||||||
|
a += in.read();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.getLogger(ReadCommand.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//s += "\n"; // MH: Dont'add newline BEFORE word
|
||||||
|
byte[] bb = new byte[sk];
|
||||||
|
// MH: Use while around in.read -> otherwise messasges bay be garbled by partial reads
|
||||||
|
int got = 0;
|
||||||
|
while (got < sk) {
|
||||||
|
try {
|
||||||
|
got += in.read(bb, got, sk - got);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
got = 0;
|
||||||
|
ex.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (got > 0) {
|
||||||
|
s += new String(bb);
|
||||||
|
s += "\n"; // MH: Add newline AFTER word (here!)
|
||||||
|
}
|
||||||
|
} else if (b == -1) {
|
||||||
|
System.out.println("Error, it should not happen ever, or connected to wrong port");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
queue.put(s + "\n");
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
System.out.println("exiting reader");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package com.mikroTik.wiki;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author janisk
|
||||||
|
*/
|
||||||
|
public class WriteCommand {
|
||||||
|
|
||||||
|
private byte[] len = {0};
|
||||||
|
private DataOutputStream out = null;
|
||||||
|
private String command = "";
|
||||||
|
|
||||||
|
|
||||||
|
WriteCommand(DataOutputStream out, String command) {
|
||||||
|
this.out = out;
|
||||||
|
this.command = command.replaceAll("\n", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public WriteCommand(DataOutputStream out) {
|
||||||
|
this.out = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public WriteCommand setCommand(String command) {
|
||||||
|
this.command = command.trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String getCommand() {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private byte[] writeLen(String command) {
|
||||||
|
Integer i = null;
|
||||||
|
String s = "";
|
||||||
|
String ret = "";
|
||||||
|
if (command.length() < 0x80) {
|
||||||
|
i = command.length();
|
||||||
|
} else if (command.length() < 0x4000) {
|
||||||
|
i = Integer.reverseBytes(command.length() | 0x8000);
|
||||||
|
} else if (command.length() < 0x20000) {
|
||||||
|
i = Integer.reverseBytes(command.length() | 0xC00000);
|
||||||
|
} else if (command.length() < 10000000) {
|
||||||
|
i = Integer.reverseBytes(command.length() | 0xE0000000);
|
||||||
|
} else {
|
||||||
|
i = Integer.reverseBytes(command.length());
|
||||||
|
}
|
||||||
|
s = Integer.toHexString(i);
|
||||||
|
if (s.length() < 2) {
|
||||||
|
return new byte[]{i.byteValue()};
|
||||||
|
} else {
|
||||||
|
for (int j = 0; j < s.length(); j += 2) {
|
||||||
|
ret += (char) Integer.parseInt(s.substring(j, j + 2), 16) != 0 ? (char) Integer.parseInt(s.substring(j, j + 2), 16) : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char[] ch = ret.toCharArray();
|
||||||
|
return ret.getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String runCommand() {
|
||||||
|
try {
|
||||||
|
byte[] ret = new byte[0];
|
||||||
|
if (!command.contains("\n")) {
|
||||||
|
int i = 0;
|
||||||
|
byte[] b = writeLen(command);
|
||||||
|
int retLen = b.length + command.length() + 1;
|
||||||
|
ret = new byte[retLen];
|
||||||
|
for (i = 0; i < b.length; i++) {
|
||||||
|
ret[i] = b[i];
|
||||||
|
}
|
||||||
|
for (byte c : command.getBytes("US-ASCII")) {
|
||||||
|
ret[i++] = c;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String[] str = command.split("\n");
|
||||||
|
int i = 1;
|
||||||
|
int[] iTmp = new int[str.length];
|
||||||
|
for (int a = 0; a < str.length; a++) {
|
||||||
|
iTmp[a] = writeLen(str[a]).length + str[a].length();
|
||||||
|
}
|
||||||
|
for (int b : iTmp) {
|
||||||
|
i += b;
|
||||||
|
}
|
||||||
|
ret = new byte[i];
|
||||||
|
int counter = 0;
|
||||||
|
for (int a = 0; a < str.length; a++) {
|
||||||
|
int j = 0;
|
||||||
|
byte[] b = writeLen(str[a]);
|
||||||
|
for (j = 0; j < b.length; j++) {
|
||||||
|
ret[counter++] = b[j];
|
||||||
|
}
|
||||||
|
for (byte c : str[a].getBytes("US-ASCII")) {
|
||||||
|
ret[counter++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write(ret);
|
||||||
|
return "Sent successfully";
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.getLogger(WriteCommand.class.getName()).log(Level.SEVERE, null, ex);
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.mikroTik.wiki.sample;
|
||||||
|
|
||||||
|
import com.mikroTik.wiki.DataReceiver;
|
||||||
|
import com.mikroTik.wiki.ApiConn;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class TestApp {
|
||||||
|
|
||||||
|
static String password = "XXXX";
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
ApiConn conn = new ApiConn("192.168.61.254", 8729, ApiConn.SSLMode.Weak);
|
||||||
|
if (!conn.isConnected()) {
|
||||||
|
conn.start();
|
||||||
|
try {
|
||||||
|
conn.join(2000);
|
||||||
|
if (conn.isConnected()) {
|
||||||
|
conn.login("admin", new String(password).toCharArray());
|
||||||
|
conn.sendCommand("/ip/address/print");
|
||||||
|
DataReceiver dataRec = new DataReceiver(conn);
|
||||||
|
dataRec.start();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Logger.getGlobal().log(Level.SEVERE, null, ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("Done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[Dolphin]
|
||||||
|
Timestamp=2015,1,10,10,15,48
|
||||||
|
Version=3
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- You may freely edit this file. See commented blocks below for -->
|
||||||
|
<!-- some examples of how to customize the build. -->
|
||||||
|
<!-- (If you delete it and reopen the project it will be recreated.) -->
|
||||||
|
<!-- By default, only the Clean and Build commands use this build script. -->
|
||||||
|
<!-- Commands such as Run, Debug, and Test only use this build script if -->
|
||||||
|
<!-- the Compile on Save feature is turned off for the project. -->
|
||||||
|
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
|
||||||
|
<!-- in the project's Project Properties dialog box.-->
|
||||||
|
<project name="MtWifiUserConfig" default="default" basedir=".">
|
||||||
|
<description>Builds, tests, and runs the project MtWifiUserConfig.</description>
|
||||||
|
<import file="nbproject/build-impl.xml"/>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
There exist several targets which are by default empty and which can be
|
||||||
|
used for execution of your tasks. These targets are usually executed
|
||||||
|
before and after some main targets. They are:
|
||||||
|
|
||||||
|
-pre-init: called before initialization of project properties
|
||||||
|
-post-init: called after initialization of project properties
|
||||||
|
-pre-compile: called before javac compilation
|
||||||
|
-post-compile: called after javac compilation
|
||||||
|
-pre-compile-single: called before javac compilation of single file
|
||||||
|
-post-compile-single: called after javac compilation of single file
|
||||||
|
-pre-compile-test: called before javac compilation of JUnit tests
|
||||||
|
-post-compile-test: called after javac compilation of JUnit tests
|
||||||
|
-pre-compile-test-single: called before javac compilation of single JUnit test
|
||||||
|
-post-compile-test-single: called after javac compilation of single JUunit test
|
||||||
|
-pre-jar: called before JAR building
|
||||||
|
-post-jar: called after JAR building
|
||||||
|
-post-clean: called after cleaning build products
|
||||||
|
|
||||||
|
(Targets beginning with '-' are not intended to be called on their own.)
|
||||||
|
|
||||||
|
Example of inserting an obfuscator after compilation could look like this:
|
||||||
|
|
||||||
|
<target name="-post-compile">
|
||||||
|
<obfuscate>
|
||||||
|
<fileset dir="${build.classes.dir}"/>
|
||||||
|
</obfuscate>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
For list of available properties check the imported
|
||||||
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
|
|
||||||
|
Another way to customize the build is by overriding existing main targets.
|
||||||
|
The targets of interest are:
|
||||||
|
|
||||||
|
-init-macrodef-javac: defines macro for javac compilation
|
||||||
|
-init-macrodef-junit: defines macro for junit execution
|
||||||
|
-init-macrodef-debug: defines macro for class debugging
|
||||||
|
-init-macrodef-java: defines macro for class execution
|
||||||
|
-do-jar: JAR building
|
||||||
|
run: execution of project
|
||||||
|
-javadoc-build: Javadoc generation
|
||||||
|
test-report: JUnit report generation
|
||||||
|
|
||||||
|
An example of overriding the target for project execution could look like this:
|
||||||
|
|
||||||
|
<target name="run" depends="MtWifiUserConfig-impl.jar">
|
||||||
|
<exec dir="bin" executable="launcher.exe">
|
||||||
|
<arg file="${dist.jar}"/>
|
||||||
|
</exec>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
Notice that the overridden target depends on the jar target and not only on
|
||||||
|
the compile target as the regular run target does. Again, for a list of available
|
||||||
|
properties which you can use, check the target you are overriding in the
|
||||||
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
|
-->
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
AP_1=192.168.61.220
|
||||||
|
AP_1_PORT=8729
|
||||||
|
AP_1_USER=rcfg
|
||||||
|
AP_1_PWPREF=0a
|
||||||
|
|
||||||
|
DEV1=MK Iphone 7
|
||||||
|
DEV1_KEY=saldk9012LLada
|
||||||
|
DEV1_MINDB=-70
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
Manifest-Version: 1.0
|
||||||
|
X-COMMENT: Main-Class will be added automatically by build
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
||||||
|
build.xml.data.CRC32=d3fd163a
|
||||||
|
build.xml.script.CRC32=d782f8ff
|
||||||
|
build.xml.stylesheet.CRC32=8064a381@1.75.2.48
|
||||||
|
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
|
||||||
|
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
|
||||||
|
nbproject/build-impl.xml.data.CRC32=d3fd163a
|
||||||
|
nbproject/build-impl.xml.script.CRC32=0d613bb8
|
||||||
|
nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
application.args=0a0a
|
||||||
|
compile.on.save=true
|
||||||
|
do.depend=false
|
||||||
|
do.jar=true
|
||||||
|
javac.debug=true
|
||||||
|
javadoc.preview=true
|
||||||
|
user.properties.file=/home/mh/.netbeans/8.0.2/build.properties
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project-private xmlns="http://www.netbeans.org/ns/project-private/1">
|
||||||
|
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/>
|
||||||
|
<open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2">
|
||||||
|
<group>
|
||||||
|
<file>file:/home/mh/Dvp/prj/MtUtils/MtWifiUserConfig/config.properties</file>
|
||||||
|
<file>file:/home/mh/Dvp/prj/MtUtils/MtWifiUserConfig/src/com/eightOceans/mtWifiUserConfig/Main.java</file>
|
||||||
|
<file>file:/home/mh/Dvp/prj/MtUtils/MtWifiUserConfig/src/com/eightOceans/mtWifiUserConfig/ApDef.java</file>
|
||||||
|
</group>
|
||||||
|
</open-files>
|
||||||
|
</project-private>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
annotation.processing.enabled=true
|
||||||
|
annotation.processing.enabled.in.editor=false
|
||||||
|
annotation.processing.processors.list=
|
||||||
|
annotation.processing.run.all.processors=true
|
||||||
|
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
|
||||||
|
application.title=MtWifiUserConfig
|
||||||
|
application.vendor=mh
|
||||||
|
build.classes.dir=${build.dir}/classes
|
||||||
|
build.classes.excludes=**/*.java,**/*.form
|
||||||
|
# This directory is removed when the project is cleaned:
|
||||||
|
build.dir=build
|
||||||
|
build.generated.dir=${build.dir}/generated
|
||||||
|
build.generated.sources.dir=${build.dir}/generated-sources
|
||||||
|
# Only compile against the classpath explicitly listed here:
|
||||||
|
build.sysclasspath=ignore
|
||||||
|
build.test.classes.dir=${build.dir}/test/classes
|
||||||
|
build.test.results.dir=${build.dir}/test/results
|
||||||
|
# Uncomment to specify the preferred debugger connection transport:
|
||||||
|
#debug.transport=dt_socket
|
||||||
|
debug.classpath=\
|
||||||
|
${run.classpath}
|
||||||
|
debug.test.classpath=\
|
||||||
|
${run.test.classpath}
|
||||||
|
# Files in build.classes.dir which should be excluded from distribution jar
|
||||||
|
dist.archive.excludes=
|
||||||
|
# This directory is removed when the project is cleaned:
|
||||||
|
dist.dir=dist
|
||||||
|
dist.jar=${dist.dir}/MtWifiUserConfig.jar
|
||||||
|
dist.javadoc.dir=${dist.dir}/javadoc
|
||||||
|
endorsed.classpath=
|
||||||
|
excludes=
|
||||||
|
includes=**
|
||||||
|
jar.archive.disabled=${jnlp.enabled}
|
||||||
|
jar.compress=false
|
||||||
|
jar.index=${jnlp.enabled}
|
||||||
|
javac.classpath=\
|
||||||
|
${reference.MikroTikAPI.jar}
|
||||||
|
# Space-separated list of extra javac options
|
||||||
|
javac.compilerargs=
|
||||||
|
javac.deprecation=false
|
||||||
|
javac.processorpath=\
|
||||||
|
${javac.classpath}
|
||||||
|
javac.source=1.8
|
||||||
|
javac.target=1.8
|
||||||
|
javac.test.classpath=\
|
||||||
|
${javac.classpath}:\
|
||||||
|
${build.classes.dir}
|
||||||
|
javac.test.processorpath=\
|
||||||
|
${javac.test.classpath}
|
||||||
|
javadoc.additionalparam=
|
||||||
|
javadoc.author=false
|
||||||
|
javadoc.encoding=${source.encoding}
|
||||||
|
javadoc.noindex=false
|
||||||
|
javadoc.nonavbar=false
|
||||||
|
javadoc.notree=false
|
||||||
|
javadoc.private=false
|
||||||
|
javadoc.splitindex=true
|
||||||
|
javadoc.use=true
|
||||||
|
javadoc.version=false
|
||||||
|
javadoc.windowtitle=
|
||||||
|
jnlp.codebase.type=no.codebase
|
||||||
|
jnlp.descriptor=application
|
||||||
|
jnlp.enabled=false
|
||||||
|
jnlp.mixed.code=default
|
||||||
|
jnlp.offline-allowed=false
|
||||||
|
jnlp.signed=false
|
||||||
|
jnlp.signing=
|
||||||
|
jnlp.signing.alias=
|
||||||
|
jnlp.signing.keystore=
|
||||||
|
main.class=com.eightOceans.mtWifiUserConfig.Main
|
||||||
|
# Optional override of default Codebase manifest attribute, use to prevent RIAs from being repurposed
|
||||||
|
manifest.custom.codebase=
|
||||||
|
# Optional override of default Permissions manifest attribute (supported values: sandbox, all-permissions)
|
||||||
|
manifest.custom.permissions=
|
||||||
|
manifest.file=manifest.mf
|
||||||
|
meta.inf.dir=${src.dir}/META-INF
|
||||||
|
mkdist.disabled=false
|
||||||
|
platform.active=default_platform
|
||||||
|
project.MikroTikAPI=../MikroTikAPI
|
||||||
|
reference.MikroTikAPI.jar=${project.MikroTikAPI}/dist/MikroTikAPI.jar
|
||||||
|
run.classpath=\
|
||||||
|
${javac.classpath}:\
|
||||||
|
${build.classes.dir}
|
||||||
|
# Space-separated list of JVM arguments used when running the project.
|
||||||
|
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
|
||||||
|
# To set system properties for unit tests define test-sys-prop.name=value:
|
||||||
|
run.jvmargs=
|
||||||
|
run.test.classpath=\
|
||||||
|
${javac.test.classpath}:\
|
||||||
|
${build.test.classes.dir}
|
||||||
|
source.encoding=UTF-8
|
||||||
|
src.dir=src
|
||||||
|
test.src.dir=test
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||||
|
<type>org.netbeans.modules.java.j2seproject</type>
|
||||||
|
<configuration>
|
||||||
|
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||||
|
<name>MtWifiUserConfig</name>
|
||||||
|
<source-roots>
|
||||||
|
<root id="src.dir"/>
|
||||||
|
</source-roots>
|
||||||
|
<test-roots>
|
||||||
|
<root id="test.src.dir"/>
|
||||||
|
</test-roots>
|
||||||
|
</data>
|
||||||
|
<references xmlns="http://www.netbeans.org/ns/ant-project-references/1">
|
||||||
|
<reference>
|
||||||
|
<foreign-project>MikroTikAPI</foreign-project>
|
||||||
|
<artifact-type>jar</artifact-type>
|
||||||
|
<script>build.xml</script>
|
||||||
|
<target>jar</target>
|
||||||
|
<clean-target>clean</clean-target>
|
||||||
|
<id>jar</id>
|
||||||
|
</reference>
|
||||||
|
</references>
|
||||||
|
</configuration>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.eightOceans.mtWifiUserConfig;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class ApDef {
|
||||||
|
|
||||||
|
public int index;
|
||||||
|
public String host;
|
||||||
|
public Integer port;
|
||||||
|
public String user;
|
||||||
|
public String pwPrefix;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
package com.eightOceans.mtWifiUserConfig;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class DevDef {
|
||||||
|
public int index;
|
||||||
|
public String name;
|
||||||
|
public String preshareKey;
|
||||||
|
public int minDbAllowed;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
package com.eightOceans.mtWifiUserConfig;
|
||||||
|
|
||||||
|
import com.eightOceans.mikroTik.ApiConnection;
|
||||||
|
import com.eightOceans.mikroTik.Result;
|
||||||
|
import com.eightOceans.mikroTik.SSLMode;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author mh
|
||||||
|
*/
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
private final static String VERS = "0.0.1";
|
||||||
|
|
||||||
|
private static String _currOp = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param args the command line arguments
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
List<ApDef> aps = new ArrayList<>();
|
||||||
|
List<DevDef> devs = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
System.out.println("MTWifiUserConfig " + VERS);
|
||||||
|
_currOp = "Loading config";
|
||||||
|
Properties props = loadProps();
|
||||||
|
|
||||||
|
// load APs
|
||||||
|
int i = 0;
|
||||||
|
while (true) {
|
||||||
|
i++;
|
||||||
|
_currOp = "Checking AP " + i + " config";
|
||||||
|
ApDef ap = new ApDef();
|
||||||
|
ap.index = i;
|
||||||
|
ap.host = props.getProperty("AP_" + i, null);
|
||||||
|
if (ap.host == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ap.port = Integer.parseInt(props.getProperty("AP_" + i + "_PORT", null));
|
||||||
|
ap.user = props.getProperty("AP_" + i + "_USER", null);
|
||||||
|
ap.pwPrefix = props.getProperty("AP_" + i + "_PWPREF", null);
|
||||||
|
aps.add(ap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load
|
||||||
|
i = 0;
|
||||||
|
while (true) {
|
||||||
|
i++;
|
||||||
|
_currOp = "Checking Device " + i + " config";
|
||||||
|
DevDef dev = new DevDef();
|
||||||
|
dev.index = i;
|
||||||
|
dev.name = props.getProperty("DEV_" + i, null);
|
||||||
|
if (dev.name == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dev.preshareKey = props.getProperty("DEV_" + i + "_KEY", null);
|
||||||
|
if ((dev.preshareKey == null) || (dev.preshareKey.length() < 8)) {
|
||||||
|
throw new RuntimeException("Preshared key too short!");
|
||||||
|
}
|
||||||
|
String db = props.getProperty("DEV_" + i + "_MINDB", null);
|
||||||
|
if (db != null) {
|
||||||
|
dev.minDbAllowed = Integer.parseInt(db);
|
||||||
|
} else {
|
||||||
|
dev.minDbAllowed = -87;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currOp = "Getting pw";
|
||||||
|
String pw = null;
|
||||||
|
|
||||||
|
System.out.print("Enter Password: ");
|
||||||
|
if (System.console() != null) {
|
||||||
|
char[] pwChars = System.console().readPassword();
|
||||||
|
pw = new String(pwChars);
|
||||||
|
} else {
|
||||||
|
pw = args[0];
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
System.out.println("- Sending config to APs ...");
|
||||||
|
for (ApDef ap : aps) {
|
||||||
|
sendDevsToAp(ap, devs, pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
System.out.println("! While " + _currOp + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
_currOp = "Done";
|
||||||
|
System.out.println("* Done");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties loadProps() {
|
||||||
|
try {
|
||||||
|
FileInputStream fis = new FileInputStream("config.properties");
|
||||||
|
Properties p = new Properties();
|
||||||
|
p.load(fis);
|
||||||
|
return p;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("Property load failed:" + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CommState {
|
||||||
|
|
||||||
|
CONNECT,
|
||||||
|
LOGIN,
|
||||||
|
FAILED,
|
||||||
|
EXIT,
|
||||||
|
DONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setCurrOp(String msg) {
|
||||||
|
_currOp = msg;
|
||||||
|
System.out.println(" ... " + _currOp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendDevsToAp(ApDef ap, List<DevDef> devs, String remPw) {
|
||||||
|
|
||||||
|
int to = 10;
|
||||||
|
String errMsg = null;
|
||||||
|
ApiConnection conn = new ApiConnection();
|
||||||
|
|
||||||
|
CommState cs = CommState.CONNECT;
|
||||||
|
|
||||||
|
while (cs != CommState.DONE) {
|
||||||
|
try {
|
||||||
|
Result r;
|
||||||
|
switch (cs) {
|
||||||
|
case CONNECT:
|
||||||
|
setCurrOp("Connecting to ap " + ap.host);
|
||||||
|
r = conn.open(ap.host, ap.port, SSLMode.Weak).get(to, TimeUnit.SECONDS);
|
||||||
|
if (r.Success) {
|
||||||
|
cs = CommState.LOGIN;
|
||||||
|
} else {
|
||||||
|
errMsg = "Failed: " + r.ResultMessage;
|
||||||
|
cs = CommState.FAILED;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LOGIN:
|
||||||
|
setCurrOp("Login to " + ap.host);
|
||||||
|
r = conn.login(ap.user, ap.pwPrefix + remPw).get(to, TimeUnit.SECONDS);
|
||||||
|
if (r.Success) {
|
||||||
|
cs = cs.EXIT;
|
||||||
|
} else {
|
||||||
|
errMsg = "Login Failed: " + r.ResultMessage;
|
||||||
|
cs = CommState.FAILED;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FAILED:
|
||||||
|
System.out.println(" ! " + (errMsg != null ? errMsg : (_currOp + " Failed")) + "! Skipping this AP");
|
||||||
|
cs = CommState.EXIT;
|
||||||
|
break;
|
||||||
|
case EXIT:
|
||||||
|
default:
|
||||||
|
setCurrOp("Closing conn to " + ap.host);
|
||||||
|
cs = CommState.DONE;
|
||||||
|
conn.close().get(to, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
} catch (TimeoutException tex) {
|
||||||
|
errMsg = "Timeout occurred";
|
||||||
|
cs = CommState.FAILED;
|
||||||
|
} catch (ExecutionException ee) {
|
||||||
|
throw new RuntimeException(ee);
|
||||||
|
} catch (InterruptedException iex) {
|
||||||
|
errMsg = "Interruption occuroed";
|
||||||
|
cs = CommState.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DevDef dev : devs) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue