From 00279a113cbc536b37b2340dccca9f8cc21cf2b3 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Tue, 19 Apr 2016 17:21:21 -0400 Subject: [PATCH 01/24] Some cleaning up of AdminApi --- .../java/berlin/meshnet/cjdns/AdminApi.java | 190 +++++++++++++++--- .../java/berlin/meshnet/cjdns/Cjdroute.java | 2 + .../berlin/meshnet/cjdns/CjdrouteConf.java | 2 +- 3 files changed, 161 insertions(+), 33 deletions(-) diff --git a/src/main/java/berlin/meshnet/cjdns/AdminApi.java b/src/main/java/berlin/meshnet/cjdns/AdminApi.java index 0160122..f8ef1d9 100644 --- a/src/main/java/berlin/meshnet/cjdns/AdminApi.java +++ b/src/main/java/berlin/meshnet/cjdns/AdminApi.java @@ -17,16 +17,113 @@ import java.util.HashMap; import java.util.Map; -import berlin.meshnet.cjdns.model.Node; - -public class AdminApi { - public static final int TIMEOUT = 5000; - public static final int DGRAM_LENGTH = 4096; - - private InetAddress address; - private int port; - private byte[] password; - +/** + * API for administration of the cjdns node. + */ +class AdminApi { + + /* + AdminLog_logMany(count) + AdminLog_subscribe(line='', file=0, level=0) + AdminLog_subscriptions() + AdminLog_unsubscribe(streamId) + Admin_asyncEnabled() + Admin_availableFunctions(page='') + Allocator_bytesAllocated() + Allocator_snapshot(includeAllocations='') + AuthorizedPasswords_add(password, user=0, ipv6=0) + AuthorizedPasswords_list() + AuthorizedPasswords_remove(user) + Core_exit() + Core_initTunnel(desiredTunName=0) + Core_pid() + ETHInterface_beacon(interfaceNumber='', state='') + ETHInterface_beginConnection(publicKey, macAddress, interfaceNumber='', login=0, password=0) + ETHInterface_listDevices() + ETHInterface_new(bindDevice) + InterfaceController_disconnectPeer(pubkey) + InterfaceController_peerStats(page='') + InterfaceController_resetPeering(pubkey=0) + IpTunnel_allowConnection(publicKeyOfAuthorizedNode, ip4Alloc='', ip6Alloc='', ip4Address=0, ip4Prefix='', ip6Address=0, ip6Prefix='') + InterfaceController_resetPeering(pubkey=0) [0/229] + IpTunnel_allowConnection(publicKeyOfAuthorizedNode, ip4Alloc='', ip6Alloc='', ip4Address=0, ip4Prefix='', ip6Address=0, ip6Prefix='') + IpTunnel_connectTo(publicKeyOfNodeToConnectTo) + IpTunnel_listConnections() + IpTunnel_removeConnection(connection) + IpTunnel_showConnection(connection) + Janitor_dumpRumorMill(mill, page) + NodeStore_dumpTable(page) + NodeStore_getLink(linkNum, parent=0) + NodeStore_getRouteLabel(pathParentToChild, pathToParent) + NodeStore_nodeForAddr(ip=0) + RouteGen_addException(route) + RouteGen_addLocalPrefix(route) + RouteGen_addPrefix(route) + RouteGen_commit(tunName) + RouteGen_getExceptions(ip6='', page='') + RouteGen_getGeneratedRoutes(ip6='', page='') + RouteGen_getLocalPrefixes(ip6='', page='') + RouteGen_getPrefixes(ip6='', page='') + RouteGen_removeException(route) + RouteGen_removeLocalPrefix(route) + RouteGen_removePrefix(route) + RouterModule_findNode(nodeToQuery, target, timeout='') + RouterModule_getPeers(path, nearbyPath=0, timeout='') + RouterModule_lookup(address) + RouterModule_nextHop(nodeToQuery, target, timeout='') + RouterModule_pingNode(path, timeout='') + SearchRunner_search(ipv6, maxRequests='') + SearchRunner_showActiveSearch(number) + Security_checkPermissions() + Security_chroot(root) + Security_getUser(user=0) + Security_nofiles() + Security_noforks() + Security_seccomp() + Security_setUser(keepNetAdmin, uid, gid='') + Security_setupComplete() + SessionManager_getHandles(page='') + SessionManager_sessionStats(handle) + SwitchPinger_ping(path, data=0, keyPing='', timeout='') + UDPInterface_beginConnection(publicKey, address, interfaceNumber='', login=0, password=0) + UDPInterface_new(bindAddress=0) + memory() + ping() + */ + + /** + * UDP datagram socket timeout in milliseconds. + */ + public static final int SOCKET_TIMEOUT = 5000; + + /** + * UDP datagram length. + */ + public static final int DATAGRAM_LENGTH = 4096; + + /** + * The local IP address to bind the admin RPC server. + */ + private InetAddress mAddress; + + /** + * The port to bind the admin RPC server. + */ + private int mPort; + + /** + * The password for authenticated requests. + */ + private byte[] mPassword; + + /** + * Creates an {@link AdminApi} object from the + * + * @param cjdrouteConf + * @return + * @throws IOException + * @throws JSONException + */ static AdminApi from(JSONObject cjdrouteConf) throws IOException, JSONException { JSONObject admin = cjdrouteConf.getJSONObject("admin"); String[] bind = admin.getString("bind").split(":"); @@ -38,14 +135,21 @@ static AdminApi from(JSONObject cjdrouteConf) throws IOException, JSONException return new AdminApi(address, port, password); } + /** + * Constructor. + * + * @param address The local IP address to bind the admin RPC server. + * @param port The port to bind the admin RPC server. + * @param password The password for authenticated requests. + */ private AdminApi(InetAddress address, int port, byte[] password) { - this.address = address; - this.port = port; - this.password = password; + mAddress = address; + mPort = port; + mPassword = password; } public String getBind() { - return this.address.getHostAddress() + ":" + this.port; + return mAddress.getHostAddress() + ":" + mPort; } public int corePid() throws IOException { @@ -53,7 +157,7 @@ public int corePid() throws IOException { HashMap request = new HashMap<>(); request.put(ByteBuffer.wrap("q".getBytes()), ByteBuffer.wrap("Core_pid".getBytes())); - Map response = perform(request); + Map response = send(request); Long pid = (Long) response.get(ByteBuffer.wrap("pid".getBytes())); return pid.intValue(); @@ -62,18 +166,21 @@ public int corePid() throws IOException { // } } - public Node NodeStore_nodeForAddr() throws IOException { - return new Node.Peer(0, "Some Peer Node", "foo.k", null); - } - - public Map perform(Map request) throws IOException { + /** + * Sends a request to the {@link AdminApi} socket. + * + * @param request The {@link AdminApi} request. + * @return The response as a map. + * @throws IOException + */ + private Map send(Map request) throws IOException { DatagramSocket socket = newSocket(); byte[] data = serialize(request); - DatagramPacket dgram = new DatagramPacket(data, data.length, this.address, this.port); + DatagramPacket dgram = new DatagramPacket(data, data.length, mAddress, mPort); socket.send(dgram); - DatagramPacket responseDgram = new DatagramPacket(new byte[DGRAM_LENGTH], DGRAM_LENGTH); + DatagramPacket responseDgram = new DatagramPacket(new byte[DATAGRAM_LENGTH], DATAGRAM_LENGTH); socket.receive(responseDgram); socket.close(); @@ -82,25 +189,44 @@ public Map perform(Map request) throws IOException { return response; } - protected byte[] serialize(Map request) throws IOException { + + /** + * Create a new UDP datagram socket. + * + * @return The socket. + * @throws SocketException Thrown if failed to create or bind. + */ + private DatagramSocket newSocket() throws SocketException { + DatagramSocket socket = new DatagramSocket(); + socket.setSoTimeout(SOCKET_TIMEOUT); + return socket; + } + + /** + * Serializes request into bencoded byte array. + * + * @param request The request as a map. + * @return The bencoded byte array. + * @throws IOException + */ + private byte[] serialize(Map request) throws IOException { Bencode serializer = new Bencode(); ByteArrayOutputStream output = new ByteArrayOutputStream(); serializer.setRootElement(request); serializer.print(output); - return output.toByteArray(); } - protected Map parse(byte[] data) throws IOException { + /** + * Parses response from a bencoded byte array. + * + * @param data The bencoded data. + * @return The response as a map. + * @throws IOException + */ + private Map parse(byte[] data) throws IOException { StringReader input = new StringReader(new String(data)); Bencode parser = new Bencode(input); - return (Map) parser.getRootElement(); } - - protected DatagramSocket newSocket() throws SocketException { - DatagramSocket socket = new DatagramSocket(); - socket.setSoTimeout(TIMEOUT); - return socket; - } } diff --git a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java index 1e4b25d..6566c64 100644 --- a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java +++ b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java @@ -246,8 +246,10 @@ public void call(String line) { Log.i(TAG, line); // Find and store cjdroute PID. + // TODO Apply filter operator on the line. if (line.contains(adminLine)) { try { + // TODO Apply corePid as operator. int pid = adminApi.corePid(); // Store PID on disk to persist across java process crashes. diff --git a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java index d31f152..ceb60e7 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java @@ -24,7 +24,7 @@ /** * Configurations for cjdroute. */ -public class CjdrouteConf { +abstract class CjdrouteConf { /** * The filename for the cjdroute configurations. From 89b8ee5d962233e64a5af6d3674350e1eae19931 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Thu, 28 Apr 2016 02:31:21 -0400 Subject: [PATCH 02/24] Make the app work without root via VpnService --- .gitignore | 11 +- build.gradle | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- install_debug | 4 + install_release | 4 + src/main/AndroidManifest.xml | 3 + .../java/berlin/meshnet/cjdns/AdminApi.java | 742 ++++++++++++++---- .../meshnet/cjdns/CjdnsApplication.java | 6 +- .../berlin/meshnet/cjdns/CjdnsService.java | 2 + .../berlin/meshnet/cjdns/CjdnsVpnService.java | 207 +++++ .../java/berlin/meshnet/cjdns/Cjdroute.java | 442 +++++++---- .../berlin/meshnet/cjdns/CjdrouteConf.java | 259 ++++-- .../meshnet/cjdns/FileDescriptorSender.java | 54 ++ .../berlin/meshnet/cjdns/MainActivity.java | 104 ++- .../dialog/ConnectionsDialogFragment.java | 2 +- .../java/berlin/meshnet/cjdns/model/Node.java | 16 +- .../meshnet/cjdns/producer/MeProducer.java | 2 +- .../meshnet/cjdns/producer/PeersProducer.java | 10 +- src/main/jni/Android.mk | 14 + src/main/jni/Application.mk | 12 + src/main/jni/sendfd.cpp | 80 ++ src/main/jni/sendfd.h | 11 + 22 files changed, 1576 insertions(+), 417 deletions(-) create mode 100755 install_debug create mode 100755 install_release create mode 100644 src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java create mode 100644 src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java create mode 100644 src/main/jni/Android.mk create mode 100644 src/main/jni/Application.mk create mode 100644 src/main/jni/sendfd.cpp create mode 100644 src/main/jni/sendfd.h diff --git a/.gitignore b/.gitignore index a129b62..2c38e94 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,13 @@ build/ local.properties *.iml *.class -src/main/assets/x86/ +src/main/libs/ +src/main/obj/ +src/main/assets/armeabi/ src/main/assets/armeabi-v7a/ -src/main/assets/cjdroute.conf +src/main/assets/arm64-v8a/ +src/main/assets/x86/ +src/main/assets/x86_64/ +src/main/assets/mips/ +src/main/assets/mips64/ +src/main/assets/all/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index dc9962f..20f74be 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:2.0.0' } } @@ -27,6 +27,10 @@ android { versionCode 1 versionName "1.0.0-SNAPSHOT" } + sourceSets.main { + jni.srcDirs = [] + jniLibs.srcDir 'src/main/libs' + } signingConfigs { release } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 42cf951..4de0289 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip diff --git a/install_debug b/install_debug new file mode 100755 index 0000000..b23237d --- /dev/null +++ b/install_debug @@ -0,0 +1,4 @@ +#!/bin/sh + +ndk-build NDK_DEBUG=true NDK_PROJECT_PATH=src/main/ +./gradlew installDebug diff --git a/install_release b/install_release new file mode 100755 index 0000000..d297eec --- /dev/null +++ b/install_release @@ -0,0 +1,4 @@ +#!/bin/sh + +ndk-build NDK_DEBUG=false NDK_PROJECT_PATH=src/main/ +./gradlew installRelease diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index b653535..e1d5afc 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ + diff --git a/src/main/java/berlin/meshnet/cjdns/AdminApi.java b/src/main/java/berlin/meshnet/cjdns/AdminApi.java index f8ef1d9..1e032c1 100644 --- a/src/main/java/berlin/meshnet/cjdns/AdminApi.java +++ b/src/main/java/berlin/meshnet/cjdns/AdminApi.java @@ -3,8 +3,6 @@ import android.util.Log; import org.bitlet.wetorrent.bencode.Bencode; -import org.json.JSONException; -import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -13,182 +11,461 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import rx.Observable; +import rx.Subscriber; + /** * API for administration of the cjdns node. */ class AdminApi { - /* - AdminLog_logMany(count) - AdminLog_subscribe(line='', file=0, level=0) - AdminLog_subscriptions() - AdminLog_unsubscribe(streamId) - Admin_asyncEnabled() - Admin_availableFunctions(page='') - Allocator_bytesAllocated() - Allocator_snapshot(includeAllocations='') - AuthorizedPasswords_add(password, user=0, ipv6=0) - AuthorizedPasswords_list() - AuthorizedPasswords_remove(user) - Core_exit() - Core_initTunnel(desiredTunName=0) - Core_pid() - ETHInterface_beacon(interfaceNumber='', state='') - ETHInterface_beginConnection(publicKey, macAddress, interfaceNumber='', login=0, password=0) - ETHInterface_listDevices() - ETHInterface_new(bindDevice) - InterfaceController_disconnectPeer(pubkey) - InterfaceController_peerStats(page='') - InterfaceController_resetPeering(pubkey=0) - IpTunnel_allowConnection(publicKeyOfAuthorizedNode, ip4Alloc='', ip6Alloc='', ip4Address=0, ip4Prefix='', ip6Address=0, ip6Prefix='') - InterfaceController_resetPeering(pubkey=0) [0/229] - IpTunnel_allowConnection(publicKeyOfAuthorizedNode, ip4Alloc='', ip6Alloc='', ip4Address=0, ip4Prefix='', ip6Address=0, ip6Prefix='') - IpTunnel_connectTo(publicKeyOfNodeToConnectTo) - IpTunnel_listConnections() - IpTunnel_removeConnection(connection) - IpTunnel_showConnection(connection) - Janitor_dumpRumorMill(mill, page) - NodeStore_dumpTable(page) - NodeStore_getLink(linkNum, parent=0) - NodeStore_getRouteLabel(pathParentToChild, pathToParent) - NodeStore_nodeForAddr(ip=0) - RouteGen_addException(route) - RouteGen_addLocalPrefix(route) - RouteGen_addPrefix(route) - RouteGen_commit(tunName) - RouteGen_getExceptions(ip6='', page='') - RouteGen_getGeneratedRoutes(ip6='', page='') - RouteGen_getLocalPrefixes(ip6='', page='') - RouteGen_getPrefixes(ip6='', page='') - RouteGen_removeException(route) - RouteGen_removeLocalPrefix(route) - RouteGen_removePrefix(route) - RouterModule_findNode(nodeToQuery, target, timeout='') - RouterModule_getPeers(path, nearbyPath=0, timeout='') - RouterModule_lookup(address) - RouterModule_nextHop(nodeToQuery, target, timeout='') - RouterModule_pingNode(path, timeout='') - SearchRunner_search(ipv6, maxRequests='') - SearchRunner_showActiveSearch(number) - Security_checkPermissions() - Security_chroot(root) - Security_getUser(user=0) - Security_nofiles() - Security_noforks() - Security_seccomp() - Security_setUser(keepNetAdmin, uid, gid='') - Security_setupComplete() - SessionManager_getHandles(page='') - SessionManager_sessionStats(handle) - SwitchPinger_ping(path, data=0, keyPing='', timeout='') - UDPInterface_beginConnection(publicKey, address, interfaceNumber='', login=0, password=0) - UDPInterface_new(bindAddress=0) - memory() - ping() - */ + private static final String TAG = AdminApi.class.getSimpleName(); + + /** + * Name of this class. + */ + private static final String CLASS_NAME = AdminApi.class.getSimpleName(); /** * UDP datagram socket timeout in milliseconds. */ - public static final int SOCKET_TIMEOUT = 5000; + private static final int SOCKET_TIMEOUT = 30000; /** * UDP datagram length. */ - public static final int DATAGRAM_LENGTH = 4096; + private static final int DATAGRAM_LENGTH = 4096; + + /** + * Array used for HEX encoding. + */ + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + + /** + * Request to {@link AdminApi} for authentication cookie. + */ + private static final HashMap REQUEST_COOKIE = new LinkedHashMap() {{ + put(wrapString("q"), wrapString("cookie")); + }}; /** * The local IP address to bind the admin RPC server. */ - private InetAddress mAddress; + private static final String ADMIN_API_ADDRESS = "127.0.0.1"; /** * The port to bind the admin RPC server. */ - private int mPort; + private static final int ADMIN_API_PORT = 11234; /** * The password for authenticated requests. */ - private byte[] mPassword; + private static final byte[] ADMIN_API_PASSWORD = "NONE".getBytes(); /** - * Creates an {@link AdminApi} object from the - * - * @param cjdrouteConf - * @return - * @throws IOException - * @throws JSONException + * The local IP address to bind the admin RPC server, as an {@link InetAddress}. */ - static AdminApi from(JSONObject cjdrouteConf) throws IOException, JSONException { - JSONObject admin = cjdrouteConf.getJSONObject("admin"); - String[] bind = admin.getString("bind").split(":"); - - InetAddress address = InetAddress.getByName(bind[0]); - int port = Integer.parseInt(bind[1]); - byte[] password = admin.getString("password").getBytes(); - - return new AdminApi(address, port, password); - } + private final InetAddress mAdminApiAddress; /** * Constructor. - * - * @param address The local IP address to bind the admin RPC server. - * @param port The port to bind the admin RPC server. - * @param password The password for authenticated requests. */ - private AdminApi(InetAddress address, int port, byte[] password) { - mAddress = address; - mPort = port; - mPassword = password; + public AdminApi() throws UnknownHostException { + mAdminApiAddress = InetAddress.getByName(ADMIN_API_ADDRESS); } - public String getBind() { - return mAddress.getHostAddress() + ":" + mPort; + public static class AdminLog { + + public static Observable logMany(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_logMany is not implemented in " + CLASS_NAME); + } + + public static Observable subscribe(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_subscribe is not implemented in " + CLASS_NAME); + } + + public static Observable subscriptions(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_subscriptions is not implemented in " + CLASS_NAME); + } + + public static Observable unsubscribe(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_unsubscribe is not implemented in " + CLASS_NAME); + } } - public int corePid() throws IOException { - // try { - HashMap request = new HashMap<>(); - request.put(ByteBuffer.wrap("q".getBytes()), ByteBuffer.wrap("Core_pid".getBytes())); + public static class Admin { - Map response = send(request); - Long pid = (Long) response.get(ByteBuffer.wrap("pid".getBytes())); + public static Observable asyncEnabled(final AdminApi api) { + throw new UnsupportedOperationException("Admin_asyncEnabled is not implemented in " + CLASS_NAME); + } - return pid.intValue(); - // } catch (IOException e) { - // return 0; - // } + public static Observable availableFunctions(final AdminApi api) { + throw new UnsupportedOperationException("Admin_availableFunctions is not implemented in " + CLASS_NAME); + } } - /** - * Sends a request to the {@link AdminApi} socket. - * - * @param request The {@link AdminApi} request. - * @return The response as a map. - * @throws IOException - */ - private Map send(Map request) throws IOException { - DatagramSocket socket = newSocket(); + public static class Allocator { - byte[] data = serialize(request); - DatagramPacket dgram = new DatagramPacket(data, data.length, mAddress, mPort); - socket.send(dgram); + public static Observable bytesAllocated(final AdminApi api) { + throw new UnsupportedOperationException("Allocator_bytesAllocated is not implemented in " + CLASS_NAME); + } - DatagramPacket responseDgram = new DatagramPacket(new byte[DATAGRAM_LENGTH], DATAGRAM_LENGTH); - socket.receive(responseDgram); - socket.close(); + public static Observable snapshot(final AdminApi api) { + throw new UnsupportedOperationException("Allocator_snapshot is not implemented in " + CLASS_NAME); + } + } + + public static class AuthorizedPasswords { + + public static Observable add(final AdminApi api) { + throw new UnsupportedOperationException("AuthorizedPasswords_add is not implemented in " + CLASS_NAME); + } + + public static Observable list(final AdminApi api) { + throw new UnsupportedOperationException("AuthorizedPasswords_list is not implemented in " + CLASS_NAME); + } + + public static Observable remove(final AdminApi api) { + throw new UnsupportedOperationException("AuthorizedPasswords_remove is not implemented in " + CLASS_NAME); + } + } + + public static class Core { + + public static Observable exit(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("Core_exit")) { + @Override + protected Boolean parseResult(final Map response) { + return Boolean.TRUE; + } + }); + } + + public static Observable initTunfd(final AdminApi api, final Long tunfd, final Long type) { + return Observable.create(new BaseOnSubscribe(api, new Request("Core_initTunfd", + new LinkedHashMap() {{ + put(wrapString("tunfd"), tunfd); + put(wrapString("type"), type); + }})) { + @Override + protected Boolean parseResult(final Map response) { + return Boolean.TRUE; + } + }); + } + + public static Observable initTunnel(final AdminApi api) { + throw new UnsupportedOperationException("Core_initTunnel is not implemented in " + CLASS_NAME); + } + + public static Observable pid(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("Core_pid")) { + @Override + protected Long parseResult(final Map response) { + return (Long) response.get(wrapString("pid")); + } + }); + } + } + + public static class EthInterface { + + public static Observable beacon(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_beacon is not implemented in " + CLASS_NAME); + } + + public static Observable beginConnection(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_beginConnection is not implemented in " + CLASS_NAME); + } + + public static Observable listDevices(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_listDevices is not implemented in " + CLASS_NAME); + } + + public static Observable new0(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_new is not implemented in " + CLASS_NAME); + } + } + + public static class FileNo { + + public static Observable> import0(final AdminApi api, final String path) { + return Observable.create(new BaseOnSubscribe>(api, new Request("FileNo_import", + new LinkedHashMap() {{ + put(wrapString("path"), wrapString(path)); + put(wrapString("type"), wrapString("android")); + }})) { + @Override + protected Map parseResult(final Map response) { + return new HashMap() {{ + put("tunfd", (Long) response.get(wrapString("tunfd"))); + put("type", (Long) response.get(wrapString("type"))); + }}; + } + }); + } + } + + public static class InterfaceController { + + public static Observable disconnectPeer(final AdminApi api) { + throw new UnsupportedOperationException("InterfaceController_disconnectPeer is not implemented in " + CLASS_NAME); + } + + public static Observable peerStatsConnection(final AdminApi api) { + throw new UnsupportedOperationException("InterfaceController_peerStats is not implemented in " + CLASS_NAME); + } + + public static Observable resetPeering(final AdminApi api) { + throw new UnsupportedOperationException("InterfaceController_resetPeering is not implemented in " + CLASS_NAME); + } + } + + public static class IpTunnel { + + public static Observable allowConnection(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_allowConnection is not implemented in " + CLASS_NAME); + } + + public static Observable connectTo(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_connectTo is not implemented in " + CLASS_NAME); + } + + public static Observable listConnections(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_listConnections is not implemented in " + CLASS_NAME); + } + + public static Observable removeConnection(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_removeConnection is not implemented in " + CLASS_NAME); + } + + public static Observable showConnection(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_showConnection is not implemented in " + CLASS_NAME); + } + } + + public static class Janitor { + + public static Observable dumpRumorMill(final AdminApi api) { + throw new UnsupportedOperationException("Janitor_dumpRumorMill is not implemented in " + CLASS_NAME); + } + } + + public static class NodeStore { + + public static Observable dumpTable(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_dumpTable is not implemented in " + CLASS_NAME); + } + + public static Observable getLink(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_getLink is not implemented in " + CLASS_NAME); + } + + public static Observable getRouteLabel(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_getRouteLabel is not implemented in " + CLASS_NAME); + } + + public static Observable nodeForAddr(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_nodeForAddr is not implemented in " + CLASS_NAME); + } + } + + public static class RouteGen { + + public static Observable addException(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_addException is not implemented in " + CLASS_NAME); + } + + public static Observable addLocalPrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_addLocalPrefix is not implemented in " + CLASS_NAME); + } + + public static Observable addPrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_addPrefix is not implemented in " + CLASS_NAME); + } + + public static Observable commit(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_commit is not implemented in " + CLASS_NAME); + } + + public static Observable getExceptions(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getExceptions is not implemented in " + CLASS_NAME); + } + + public static Observable getGeneratedRoutes(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getGeneratedRoutes is not implemented in " + CLASS_NAME); + } + + public static Observable getLocalPrefixes(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getLocalPrefixes is not implemented in " + CLASS_NAME); + } + + public static Observable getPrefixes(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getPrefixes is not implemented in " + CLASS_NAME); + } + + public static Observable removeException(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_removeException is not implemented in " + CLASS_NAME); + } + + public static Observable removeLocalPrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_removeLocalPrefix is not implemented in " + CLASS_NAME); + } + + public static Observable removePrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_removePrefix is not implemented in " + CLASS_NAME); + } + } + + public static class RouterModule { + + public static Observable findNode(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_findNode is not implemented in " + CLASS_NAME); + } + + public static Observable getPeers(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_getPeers is not implemented in " + CLASS_NAME); + } + + public static Observable lookup(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_lookup is not implemented in " + CLASS_NAME); + } + + public static Observable nextHop(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_nextHop is not implemented in " + CLASS_NAME); + } - Map response = parse(responseDgram.getData()); - Log.i("cjdns_AdminAPI", "response: " + response.toString()); - return response; + public static Observable pingNode(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_pingNode is not implemented in " + CLASS_NAME); + } } + public static class SearchRunner { + + public static Observable search(final AdminApi api) { + throw new UnsupportedOperationException("SearchRunner_search is not implemented in " + CLASS_NAME); + } + + public static Observable showActiveSearch(final AdminApi api) { + throw new UnsupportedOperationException("SearchRunner_showActiveSearch is not implemented in " + CLASS_NAME); + } + } + + public static class Security { + + public static Observable checkPermissions(final AdminApi api) { + throw new UnsupportedOperationException("Security_checkPermissions is not implemented in " + CLASS_NAME); + } + + public static Observable chroot(final AdminApi api) { + throw new UnsupportedOperationException("Security_chroot is not implemented in " + CLASS_NAME); + } + + public static Observable getUser(final AdminApi api) { + throw new UnsupportedOperationException("Security_getUser is not implemented in " + CLASS_NAME); + } + + public static Observable nofiles(final AdminApi api) { + throw new UnsupportedOperationException("Security_nofiles is not implemented in " + CLASS_NAME); + } + + public static Observable noforks(final AdminApi api) { + throw new UnsupportedOperationException("Security_noforks is not implemented in " + CLASS_NAME); + } + + public static Observable seccomp(final AdminApi api) { + throw new UnsupportedOperationException("Security_seccomp is not implemented in " + CLASS_NAME); + } + + public static Observable setUser(final AdminApi api) { + throw new UnsupportedOperationException("Security_setUser is not implemented in " + CLASS_NAME); + } + + public static Observable setupComplete(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("Security_setupComplete", null)) { + @Override + protected Boolean parseResult(final Map response) { + return Boolean.TRUE; + } + }); + } + } + + public static class SessionManager { + + public static Observable getHandles(final AdminApi api) { + throw new UnsupportedOperationException("SessionManager_getHandles is not implemented in " + CLASS_NAME); + } + + public static Observable sessionStats(final AdminApi api) { + throw new UnsupportedOperationException("SessionManager_sessionStats is not implemented in " + CLASS_NAME); + } + } + + public static class SwitchPinger { + + public static Observable ping(final AdminApi api) { + throw new UnsupportedOperationException("SwitchPinger_ping is not implemented in " + CLASS_NAME); + } + } + + public static class UdpInterface { + + public static Observable new0(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("UDPInterface_new", + new LinkedHashMap() {{ + put(wrapString("bindAddress"), wrapString("0.0.0.0:0")); + }})) { + @Override + protected Long parseResult(Map response) { + return (Long) response.get(wrapString("interfaceNumber")); + } + }); + } + + public static Observable beginConnection(final AdminApi api, final String publicKey, final String address, + final Long interfaceNumber, final String login, final String password) { + return Observable.create(new BaseOnSubscribe(api, new Request("UDPInterface_beginConnection", + new LinkedHashMap() {{ + put(wrapString("publicKey"), wrapString(publicKey)); + put(wrapString("address"), wrapString(address)); + put(wrapString("interfaceNumber"), interfaceNumber); + if (login != null) { + put(wrapString("login"), wrapString(login)); + } + put(wrapString("password"), wrapString(password)); + }})) { + @Override + protected Boolean parseResult(Map response) { + return Boolean.TRUE; + } + }); + } + } + + public static Observable memory(final AdminApi api) { + throw new UnsupportedOperationException("memory is not implemented in " + CLASS_NAME); + } + + public static Observable ping(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("ping")) { + @Override + protected Boolean parseResult(Map response) { + return Boolean.TRUE; + } + }); + } /** * Create a new UDP datagram socket. @@ -196,7 +473,7 @@ private Map send(Map request) throws IOException { * @return The socket. * @throws SocketException Thrown if failed to create or bind. */ - private DatagramSocket newSocket() throws SocketException { + private static DatagramSocket newSocket() throws SocketException { DatagramSocket socket = new DatagramSocket(); socket.setSoTimeout(SOCKET_TIMEOUT); return socket; @@ -209,7 +486,7 @@ private DatagramSocket newSocket() throws SocketException { * @return The bencoded byte array. * @throws IOException */ - private byte[] serialize(Map request) throws IOException { + private static byte[] serialize(Map request) throws IOException { Bencode serializer = new Bencode(); ByteArrayOutputStream output = new ByteArrayOutputStream(); serializer.setRootElement(request); @@ -224,9 +501,210 @@ private byte[] serialize(Map request) throws IOException { * @return The response as a map. * @throws IOException */ - private Map parse(byte[] data) throws IOException { + private static Map parse(byte[] data) throws IOException { StringReader input = new StringReader(new String(data)); Bencode parser = new Bencode(input); return (Map) parser.getRootElement(); } + + /** + * Converts bytes to a HEX encoded string. + * + * @param bytes The byte array. + * @return The HEX encoded string. + */ + private static String bytesToHex(byte[] bytes) { + String hexString = null; + if (bytes != null && bytes.length > 0) { + final char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[v >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + hexString = new String(hexChars); + } + return hexString; + } + + /** + * Wraps a string into a {@link ByteBuffer}. + * + * @param value The string. + * @return The wrapped {@link ByteBuffer}. + */ + private static ByteBuffer wrapString(String value) { + return ByteBuffer.wrap(value.getBytes()); + } + + /** + * Sends an authenticated request to the {@link AdminApi}. + * + * @param request The request. + * @param api The {@link AdminApi}. + * @return The response as a map. + * @throws NoSuchAlgorithmException Thrown if SHA-256 is missing. + * @throws IOException Thrown if request failed. + */ + private static Map sendAuthenticatedRequest(Request request, AdminApi api) throws NoSuchAlgorithmException, IOException { + Log.i(TAG, request.name + " sent"); + + // Get authentication session cookie. + String cookie = getCookie(api); + + // Generate dummy hash. + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(ADMIN_API_PASSWORD); + digest.update(cookie.getBytes()); + String dummyHash = bytesToHex(digest.digest()); + + // Assemble unsigned request. + HashMap authenticatedRequest = new LinkedHashMap<>(); + authenticatedRequest.put(wrapString("q"), wrapString("auth")); + authenticatedRequest.put(wrapString("aq"), wrapString(request.name)); + if (request.args != null) { + authenticatedRequest.put(wrapString("args"), request.args); + } + authenticatedRequest.put(wrapString("hash"), wrapString(dummyHash)); + authenticatedRequest.put(wrapString("cookie"), wrapString(cookie)); + + // Sign request. + byte[] requestBytes = serialize(authenticatedRequest); + digest.reset(); + digest.update(requestBytes); + String hash = bytesToHex(digest.digest()); + authenticatedRequest.put(wrapString("hash"), wrapString(hash)); + + // Send request. + return send(authenticatedRequest, api); + } + + /** + * Gets an authentication cookie from the {@link AdminApi}. + * + * @param api The {@link AdminApi}. + * @return The cookie. + * @throws IOException Thrown if request failed. + */ + private static String getCookie(AdminApi api) throws IOException { + Map response = send(REQUEST_COOKIE, api); + Object cookie = response.get(wrapString("cookie")); + if (cookie instanceof ByteBuffer) { + return new String(((ByteBuffer) cookie).array()); + } else { + throw new IOException("Unable to fetch authentication cookie"); + } + } + + /** + * Sends a request to the {@link AdminApi}. + * + * @param request The request. + * @param api The {@link AdminApi}. + * @return The response as a map. + * @throws IOException Thrown if request failed. + */ + private static Map send(Map request, AdminApi api) throws IOException { + DatagramSocket socket = newSocket(); + + byte[] data = serialize(request); + DatagramPacket dgram = new DatagramPacket(data, data.length, api.mAdminApiAddress, ADMIN_API_PORT); + socket.send(dgram); + + DatagramPacket responseDgram = new DatagramPacket(new byte[DATAGRAM_LENGTH], DATAGRAM_LENGTH); + socket.receive(responseDgram); + socket.close(); + + byte[] resData = responseDgram.getData(); + int i = resData.length - 1; + while (resData[i] == 0) { + --i; + } + byte[] resDataClean = Arrays.copyOf(resData, i + 1); + return parse(resDataClean); + } + + /** + * Model object encapsulating the name and arguments of a request. + */ + private static class Request { + + private final String name; + + private final LinkedHashMap args; + + private Request(String name, LinkedHashMap args) { + this.name = name; + this.args = args; + } + + private Request(String name) { + this(name, null); + } + } + + /** + * Abstract class that implements the basic {@link rx.Observable.OnSubscribe} behaviour of each API. + * + * @param The return type of the API response. + */ + private static abstract class BaseOnSubscribe implements Observable.OnSubscribe { + + private static final ByteBuffer ERROR_KEY = wrapString("error"); + + private static final String ERROR_NONE = "none"; + + private AdminApi mApi; + + private Request mRequest; + + private BaseOnSubscribe(AdminApi api, Request request) { + mApi = api; + mRequest = request; + } + + @Override + public void call(Subscriber subscriber) { + try { + final Map response = AdminApi.sendAuthenticatedRequest(mRequest, mApi); + + // Check for error in response. + final Object error = response.get(ERROR_KEY); + if (error instanceof ByteBuffer) { + String errorString = new String(((ByteBuffer) error).array()); + if (!ERROR_NONE.equals(errorString)) { + Log.e(TAG, mRequest.name + " failed: " + errorString); + subscriber.onError(new IOException(mRequest.name + " failed: " + errorString)); + return; + } + } + + // Parse response for result. + final T result = parseResult(response); + if (result != null) { + Log.e(TAG, mRequest.name + " completed"); + subscriber.onNext(result); + subscriber.onCompleted(); + } else { + Log.e(TAG, "Failed to parse result from " + mRequest.name); + subscriber.onError(new IOException("Failed to parse result from " + mRequest.name)); + } + } catch (SocketTimeoutException e) { + Log.e(TAG, mRequest.name + " timed out"); + subscriber.onError(e); + } catch (NoSuchAlgorithmException | IOException e) { + Log.e(TAG, "Unexpected failure from " + mRequest.name, e); + subscriber.onError(e); + } + } + + /** + * Implementation must specify how to parse the response and return a value of type {@link T}. + * Returning {@code null} will lead to {@link Subscriber#onError(Throwable)} being called. + * + * @param response The response from the API as a {@link Map}. + * @return A value of type {@link T}. + */ + protected abstract T parseResult(final Map response); + } } diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java index 808c5ec..1c7e093 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java @@ -53,6 +53,7 @@ public void inject(Object object) { @Module( injects = { MainActivity.class, + CjdnsVpnService.class, CjdnsService.class, MePageFragment.class, PeersPageFragment.class, @@ -91,10 +92,7 @@ public Bus provideBus() { @Singleton @Provides public Cjdroute provideCjdroute(Context context) { - // TODO Change this conditional to (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) when VpnService is implemented. - // TODO Use Lollipop for now to allow any API level below to connect with tun device. - // TODO Unable to run cjdroute as root since Lollipop, so there is no point trying. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return new Cjdroute.Compat(context.getApplicationContext()); } return new Cjdroute.Default(context.getApplicationContext()); diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java index 52ada11..4b3faaa 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java @@ -23,6 +23,8 @@ import rx.schedulers.Schedulers; /** + * TODO Only needed for compat. + *

* Service for managing cjdroute. */ public class CjdnsService extends Service { diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java new file mode 100644 index 0000000..c670151 --- /dev/null +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java @@ -0,0 +1,207 @@ +package berlin.meshnet.cjdns; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.net.VpnService; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import berlin.meshnet.cjdns.model.Node; +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.schedulers.Schedulers; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class CjdnsVpnService extends VpnService { + + private static final String TAG = CjdnsVpnService.class.getSimpleName(); + + /** + * The VPN session name. + */ + private static final String SESSION_NAME = "VPN over cjdns"; + + /** + * The maximum transmission unit for the VPN interface. + */ + private static final int MTU = 1304; + + /** + * Route for cjdns addresses. + */ + private static final String CJDNS_ROUTE = "fc00::"; + + /** + * Default route for the VPN interface. A default route is needed for some applications to work. + */ + private static final String DEFAULT_ROUTE = "::"; + + /** + * DNS server for the VPN connection. We must set a DNS server for Lollipop devices to work. + */ + private static final String DNS_SERVER = "8.8.8.8"; + + /** + * Path to a transient named pipe for sending a message from the Java process to the native + * process. The VPN interface file descriptor is translated by the kernel across this named pipe. + */ + private static final String SEND_FD_PIPE_PATH_TEMPLATE = "%1$s/pipe_%2$s"; + + private ParcelFileDescriptor mInterface; + + @Inject + Cjdroute mCjdroute; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // Inject dependencies. + ((CjdnsApplication) getApplication()).inject(this); + + final String pipePath = String.format(Locale.ENGLISH, SEND_FD_PIPE_PATH_TEMPLATE, + getFilesDir().getPath(), UUID.randomUUID()); + try { + final AdminApi api = new AdminApi(); + // TODO Move UDP interface adding to separate place. + CjdrouteConf.fetch0(this) + .flatMap(new Func1>() { + @Override + public Observable call(Node.Me me) { + return mCjdroute.start(); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return AdminApi.Security.setupComplete(api); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return AdminApi.UdpInterface.new0(api); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Long udpInterfaceNumber) { + return AdminApi.UdpInterface.beginConnection(api, + "1941p5k8qqvj17vjrkb9z97wscvtgc1vp8pv1huk5120cu42ytt0.k", + "104.200.29.163:53053", + udpInterfaceNumber, + null, + "8fVMl0oo6QI6wKeMneuY26x1MCgRemg"); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return CjdrouteConf.fetch0(CjdnsVpnService.this); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(final Node.Me me) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // Close any existing session. + close(); + + // Start new session. + mInterface = new Builder() + .setSession(SESSION_NAME) + .setMtu(MTU) + .addAddress(me.address, 8) + .addRoute(CJDNS_ROUTE, 8) + .addRoute(DEFAULT_ROUTE, 0) + .addDnsServer(DNS_SERVER) + .establish(); + subscriber.onNext(mInterface.getFd()); + subscriber.onCompleted(); + } + }); + } + }) + .flatMap(new Func1>>() { + @Override + public Observable> call(Integer fd) { + // Send VPN interface file descriptor through the pipe after + // AdminApi.FileNo.import0() constructs that pipe. + FileDescriptorSender.send(pipePath, fd) + .delaySubscription(3L, TimeUnit.SECONDS) + .subscribe(new Action1() { + @Override + public void call(Boolean isSuccessful) { + Log.i(TAG, "VPN interface file descriptor imported to native process"); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to import VPN interface file descriptor to native process", throwable); + } + }); + + // Start named pipe to import VPN interface file descriptor. + return AdminApi.FileNo.import0(api, pipePath); + } + }) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(Map file) { + return AdminApi.Core.initTunfd(api, file.get("tunfd"), file.get("type")); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + new Action1() { + @Override + public void call(Boolean isSuccessful) { + Log.i(TAG, "TUN interface initialized"); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to initialize TUN interface", throwable); + } + }); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } + + return START_STICKY; + } + + @Override + public void onDestroy() { + close(); + + // TODO Purge stale pipes. + } + + /** + * Close any existing session. + */ + private void close() { + if (mInterface != null) { + try { + mInterface.close(); + } catch (IOException e) { + // Do nothing. + } + mInterface = null; + } + } +} diff --git a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java index 6566c64..aea9814 100644 --- a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java +++ b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java @@ -4,18 +4,19 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; -import android.os.Process; -import android.preference.PreferenceManager; import android.util.Log; -import org.json.JSONException; import org.json.JSONObject; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.UnknownHostException; import java.util.Locale; +import java.util.UUID; +import berlin.meshnet.cjdns.model.Node; import berlin.meshnet.cjdns.util.InputStreamObservable; import rx.Observable; import rx.Subscriber; @@ -42,33 +43,29 @@ abstract class Cjdroute { /** * Value that represents an invalid PID. */ - private static final int INVALID_PID = Integer.MIN_VALUE; + static final long INVALID_PID = Long.MIN_VALUE; /** - * {@link Observable} for the PID of any currently running cjdroute process. If none is running, - * this {@link Observable} will complete without calling {@link Subscriber#onNext(Object)}. + * Checks if the node is running. * - * @param context The {@link Context}. - * @return The {@link Observable}. + * @return {@link Observable} that emits the PID if the node is running. */ - public static Observable running(Context context) { - final Context appContext = context.getApplicationContext(); - return Observable - .create(new Observable.OnSubscribe() { + public static Observable running() throws UnknownHostException { + final AdminApi api = new AdminApi(); + return AdminApi.ping(api) + .filter(new Func1() { @Override - public void call(Subscriber subscriber) { - int pid = PreferenceManager.getDefaultSharedPreferences(appContext) - .getInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); - subscriber.onNext(pid); - subscriber.onCompleted(); + public Boolean call(Boolean isSuccessful) { + return isSuccessful; } }) - .filter(new Func1() { + .flatMap(new Func1>() { @Override - public Boolean call(Integer pid) { - return pid != INVALID_PID; + public Observable call(Boolean isSuccessful) { + return AdminApi.Core.pid(api); } - }); + }) + .defaultIfEmpty(INVALID_PID); } /** @@ -83,7 +80,12 @@ public Boolean call(Integer pid) { * * @return The {@link Subscriber}. */ - abstract Subscriber terminate(); + abstract Subscriber terminate(); + + /** + * TODO + */ + abstract Observable start(); /** * Default implementation of {@link Cjdroute}. This relies on {@link android.net.VpnService} @@ -106,23 +108,35 @@ static class Default extends Cjdroute { @Override public Subscriber execute() { - // TODO Make this work. - throw new UnsupportedOperationException("Execution of cjdroute is not yet supported for your API level"); + throw new UnsupportedOperationException("Deprecated"); } @Override - public Subscriber terminate() { - return new Subscriber() { + public Subscriber terminate() { + return new Subscriber() { @Override - public void onNext(Integer pid) { + public void onNext(Long pid) { Log.i(TAG, "Terminating cjdroute with pid=" + pid); - Process.killProcess(pid); - // Erase PID. - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); - editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); - editor.apply(); + try { + AdminApi api = new AdminApi(); + AdminApi.Core.exit(api) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(new Action1() { + @Override + public void call(Boolean isSuccessful) { + Log.i(TAG, "cjdroute terminated"); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to terminate cjdroute", throwable); + } + }); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } } @Override @@ -136,8 +150,114 @@ public void onError(Throwable e) { } }; } + + @Override + public Observable start() { + return CjdrouteConf.fetch0(mContext) + .flatMap(new Func1>() { + @Override + public Observable call(Node.Me me) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + try { + final File filesDir = mContext.getFilesDir(); + final String pipe = UUID.randomUUID().toString(); + + // Start cjdroute. + Process process = new ProcessBuilder("./cjdroute", "core", filesDir.getPath(), pipe) + .directory(filesDir) + .redirectErrorStream(true) + .start(); + + // Subscribe to input stream. + final InputStream is = process.getInputStream(); + InputStreamObservable.line(is) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + new Action1() { + @Override + public void call(String line) { + Log.i(TAG, line); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to parse input stream", throwable); + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + }, + new Action0() { + @Override + public void call() { + Log.i(TAG, "Completed parsing of input stream"); + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + }); + + // TODO Replace this with directly passing params. + CjdrouteConf.fetch0(mContext) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(new Action1() { + @Override + public void call(Node.Me me) { + try { + Process initProcess = new ProcessBuilder("./cjdroute-init", + filesDir.getPath(), pipe, me.privateKey, "127.0.0.1:11234", "NONE") + .directory(filesDir) + .redirectErrorStream(true) + .start(); + } catch (IOException e) { + Log.e(TAG, "Failed to start cjdroute-init process", e); + } + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to start cjdroute-init process", throwable); + } + }); + + // TODO Check for when cjdroute is ready instead. + Thread.sleep(5000L); + + // Ping node to check if node is running. + AdminApi api = new AdminApi(); + Boolean isNodeRunning = AdminApi.ping(api).toBlocking().first(); + if (isNodeRunning != null && isNodeRunning) { + Log.i(TAG, "cjdroute started"); + subscriber.onNext(Boolean.TRUE); + subscriber.onCompleted(); + } else { + Log.i(TAG, "Failed to start cjdroute"); + subscriber.onError(new IOException("Failed to start cjdroute")); + } + } catch (IOException | InterruptedException e) { + Log.e(TAG, "Failed to start cjdroute", e); + subscriber.onError(e); + } + } + }); + } + }); + } } + /** * Compat implementation of {@link Cjdroute}. This allows cjdroute to create a TUN device and * requires super user permission. @@ -190,123 +310,124 @@ public Subscriber execute() { return new Subscriber() { @Override public void onNext(JSONObject cjdrouteConf) { - DataOutputStream os = null; - try { - java.lang.Process process = Runtime.getRuntime().exec(CMD_SUBSTITUTE_ROOT_USER); - - // Subscribe to input stream. - final InputStream is = process.getInputStream(); - InputStreamObservable.line(is) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - new Action1() { - @Override - public void call(String line) { - Log.i(TAG, line); - } - }, new Action1() { - @Override - public void call(Throwable throwable) { - Log.e(TAG, "Failed to parse input stream", throwable); - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }, - new Action0() { - @Override - public void call() { - Log.i(TAG, "Completed parsing of input stream"); - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }); - - // Subscribe to error stream. - final AdminApi adminApi = AdminApi.from(cjdrouteConf); - final String adminLine = String.format(Locale.ENGLISH, LINE_ADMIN_API, adminApi.getBind()); - final InputStream es = process.getErrorStream(); - InputStreamObservable.line(es) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - new Action1() { - @Override - public void call(String line) { - Log.i(TAG, line); - - // Find and store cjdroute PID. - // TODO Apply filter operator on the line. - if (line.contains(adminLine)) { - try { - // TODO Apply corePid as operator. - int pid = adminApi.corePid(); - - // Store PID on disk to persist across java process crashes. - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); - editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, pid); - editor.apply(); - } catch (IOException e) { - Log.e(TAG, "Failed to get cjdroute PID", e); - } - } - } - }, new Action1() { - @Override - public void call(Throwable throwable) { - Log.e(TAG, "Failed to parse error stream", throwable); - if (es != null) { - try { - es.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }, - new Action0() { - @Override - public void call() { - Log.i(TAG, "Completed parsing of error stream"); - if (es != null) { - try { - es.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }); - - // Execute cjdroute. - String filesDir = mContext.getFilesDir().getPath(); - os = new DataOutputStream(process.getOutputStream()); - os.writeBytes(String.format(CMD_EXECUTE_CJDROUTE, filesDir, filesDir)); - os.writeBytes(CMD_NEWLINE); - os.writeBytes(CMD_ADD_DEFAULT_ROUTE); - os.flush(); - } catch (IOException | JSONException e) { - Log.e(TAG, "Failed to execute cjdroute", e); - } finally { - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // Do nothing. - } - } - } + // TODO Fix Compat implementation. +// DataOutputStream os = null; +// try { +// java.lang.Process process = Runtime.getRuntime().exec(CMD_SUBSTITUTE_ROOT_USER); +// +// // Subscribe to input stream. +// final InputStream is = process.getInputStream(); +// InputStreamObservable.line(is) +// .subscribeOn(Schedulers.io()) +// .observeOn(Schedulers.io()) +// .subscribe( +// new Action1() { +// @Override +// public void call(String line) { +// Log.i(TAG, line); +// } +// }, new Action1() { +// @Override +// public void call(Throwable throwable) { +// Log.e(TAG, "Failed to parse input stream", throwable); +// if (is != null) { +// try { +// is.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }, +// new Action0() { +// @Override +// public void call() { +// Log.i(TAG, "Completed parsing of input stream"); +// if (is != null) { +// try { +// is.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }); +// +// // Subscribe to error stream. +// final AdminApi adminApi = AdminApi.from(cjdrouteConf); +// final String adminLine = String.format(Locale.ENGLISH, LINE_ADMIN_API, adminApi.getBind()); +// final InputStream es = process.getErrorStream(); +// InputStreamObservable.line(es) +// .subscribeOn(Schedulers.io()) +// .observeOn(Schedulers.io()) +// .subscribe( +// new Action1() { +// @Override +// public void call(String line) { +// Log.i(TAG, line); +// +// // Find and store cjdroute PID. +// // TODO Apply filter operator on the line. +// if (line.contains(adminLine)) { +// try { +// // TODO Apply corePid as operator. +// int pid = adminApi.corePid(); +// +// // Store PID on disk to persist across java process crashes. +// SharedPreferences.Editor editor = PreferenceManager +// .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); +// editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, pid); +// editor.apply(); +// } catch (IOException e) { +// Log.e(TAG, "Failed to get cjdroute PID", e); +// } +// } +// } +// }, new Action1() { +// @Override +// public void call(Throwable throwable) { +// Log.e(TAG, "Failed to parse error stream", throwable); +// if (es != null) { +// try { +// es.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }, +// new Action0() { +// @Override +// public void call() { +// Log.i(TAG, "Completed parsing of error stream"); +// if (es != null) { +// try { +// es.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }); +// +// // Execute cjdroute. +// String filesDir = mContext.getFilesDir().getPath(); +// os = new DataOutputStream(process.getOutputStream()); +// os.writeBytes(String.format(Locale.ENGLISH, CMD_EXECUTE_CJDROUTE, filesDir, filesDir)); +// os.writeBytes(CMD_NEWLINE); +// os.writeBytes(CMD_ADD_DEFAULT_ROUTE); +// os.flush(); +// } catch (IOException | JSONException e) { +// Log.e(TAG, "Failed to execute cjdroute", e); +// } finally { +// if (os != null) { +// try { +// os.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } } @Override @@ -322,10 +443,10 @@ public void onError(Throwable e) { } @Override - public Subscriber terminate() { - return new Subscriber() { + public Subscriber terminate() { + return new Subscriber() { @Override - public void onNext(Integer pid) { + public void onNext(Long pid) { Log.i(TAG, "Terminating cjdroute with pid=" + pid); // Kill cjdroute as root. @@ -336,11 +457,11 @@ public void onNext(Integer pid) { os.writeBytes(String.format(Locale.ENGLISH, CMD_KILL_PROCESS, pid)); os.flush(); - // Erase PID. - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); - editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); - editor.apply(); + // Erase PID. TODO Change implementation. +// SharedPreferences.Editor editor = PreferenceManager +// .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); +// editor.putLong(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); +// editor.apply(); } catch (IOException e) { Log.e(TAG, "Failed to terminate cjdroute", e); } finally { @@ -365,5 +486,10 @@ public void onError(Throwable e) { } }; } + + @Override + Observable start() { + return null; + } } } diff --git a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java index ceb60e7..07b1856 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java @@ -2,7 +2,10 @@ import android.annotation.TargetApi; import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; +import android.preference.PreferenceManager; +import android.text.TextUtils; import org.json.JSONException; import org.json.JSONObject; @@ -18,6 +21,7 @@ import java.io.OutputStream; import java.util.Locale; +import berlin.meshnet.cjdns.model.Node; import rx.Observable; import rx.Subscriber; @@ -51,6 +55,20 @@ abstract class CjdrouteConf { */ private static final Object sLock = new Object(); + /** + * Shared preference key for storing this node's address. + */ + private static final String SHARED_PREFERENCES_KEY_ADDRESS = "address"; + + /** + * Shared preference key for storing this node's public key. + */ + private static final String SHARED_PREFERENCES_KEY_PUBLIC_KEY = "publicKey"; + + /** + * Shared preference key for storing this node's private key. + */ + private static final String SHARED_PREFERENCES_KEY_PRIVATE_KEY = "privateKey"; /** * Default public peer interface. TODO Remove. @@ -67,6 +85,78 @@ abstract class CjdrouteConf { " \"location\": \"Newark,NJ,USA\"\n" + "}"; + public static Observable fetch0(Context context) { + final Context appContext = context.getApplicationContext(); + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + String filesDir = appContext.getFilesDir().getPath(); + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(appContext); + Node.Me me = from(sharedPref.getString(SHARED_PREFERENCES_KEY_ADDRESS, null), + sharedPref.getString(SHARED_PREFERENCES_KEY_PUBLIC_KEY, null), + sharedPref.getString(SHARED_PREFERENCES_KEY_PRIVATE_KEY, null)); + if (me != null) { + // Return existing node info. + subscriber.onNext(me); + subscriber.onCompleted(); + } else { + // Generate new node and return info. + try { + // Copy executables. + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE); + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE + "-init"); // TODO Remove. + + // Create new configuration file from which to get node info. + String[] cmd = { + CMD_SET_UP_SHELL, + CMD_EXECUTE_COMMAND, + String.format(Locale.ENGLISH, CMD_GENERATE_CJDROUTE_CONF_TEMPLATE, filesDir, filesDir) + }; + InputStream is = null; + try { + // Generate new configurations. + Process process = Runtime.getRuntime().exec(cmd); + is = process.getInputStream(); + JSONObject json = new JSONObject(fromInputStream(is)); + + // Get node info. + String ipv6 = (String) json.get("ipv6"); + String publicKey = (String) json.get("publicKey"); + String privateKey = (String) json.get("privateKey"); + me = from(ipv6, publicKey, privateKey); + if (me != null) { + // Store node info. + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(SHARED_PREFERENCES_KEY_ADDRESS, ipv6); + editor.putString(SHARED_PREFERENCES_KEY_PUBLIC_KEY, publicKey); + editor.putString(SHARED_PREFERENCES_KEY_PRIVATE_KEY, privateKey); + editor.apply(); + + // Return JSON object and complete Rx contract. + subscriber.onNext(new Node.Me("Me", ipv6, publicKey, privateKey)); + subscriber.onCompleted(); + } else { + subscriber.onError(new IOException("Failed to generate node info")); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + } catch (IOException e) { + subscriber.onError(e); + } + } + } + }); + } + /** * {@link Observable} for cjdroute configuration JSON object. * @@ -76,7 +166,6 @@ abstract class CjdrouteConf { public static Observable fetch(Context context) { final Context appContext = context.getApplicationContext(); return Observable.create(new Observable.OnSubscribe() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void call(Subscriber subscriber) { synchronized (sLock) { @@ -105,20 +194,42 @@ public void call(Subscriber subscriber) { } } } else { - // If cjdroute is not present in the files directory, it needs to be copied over from assets. - File cjdroutefile = new File(filesDir, Cjdroute.FILENAME_CJDROUTE); - if (!cjdroutefile.exists()) { - // Copy cjdroute from assets folder to the files directory. + try { + // Copy executables. + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE); + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE + "-init"); // TODO Remove. + + // Create new configuration file from which to return JSON object. + String[] cmd = { + CMD_SET_UP_SHELL, + CMD_EXECUTE_COMMAND, + String.format(Locale.ENGLISH, CMD_GENERATE_CJDROUTE_CONF_TEMPLATE, filesDir, filesDir) + }; InputStream is = null; FileOutputStream os = null; try { - String abi = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI; - is = appContext.getAssets().open(abi + "/" + Cjdroute.FILENAME_CJDROUTE); - os = appContext.openFileOutput(Cjdroute.FILENAME_CJDROUTE, Context.MODE_PRIVATE); - copyStream(is, os); - } catch (IOException e) { + // Generate new configurations. + Process process = Runtime.getRuntime().exec(cmd); + is = process.getInputStream(); + JSONObject json = new JSONObject(fromInputStream(is)); + + // Append default peer credentials. TODO Remove. + json.getJSONObject("interfaces") + .getJSONArray("UDPInterface") + .getJSONObject(0) + .getJSONObject("connectTo") + .put(DEFAULT_PEER_INTERFACE, new JSONObject(DEFAULT_PEER_CREDENTIALS)); + + // Write configurations to file. + os = appContext.openFileOutput(FILENAME_CJDROUTE_CONF, Context.MODE_PRIVATE); + os.write(json.toString().getBytes()); + os.flush(); + + // Return JSON object and complete Rx contract. + subscriber.onNext(json); + subscriber.onCompleted(); + } catch (IOException | JSONException e) { subscriber.onError(e); - return; } finally { if (is != null) { try { @@ -135,62 +246,8 @@ public void call(Subscriber subscriber) { } } } - } - - // Create new configuration file from which to return JSON object. - if (cjdroutefile.exists()) { - if (cjdroutefile.canExecute() || cjdroutefile.setExecutable(true)) { - String[] cmd = { - CMD_SET_UP_SHELL, - CMD_EXECUTE_COMMAND, - String.format(Locale.ENGLISH, CMD_GENERATE_CJDROUTE_CONF_TEMPLATE, filesDir, filesDir) - }; - InputStream is = null; - FileOutputStream os = null; - try { - // Generate new configurations. - Process process = Runtime.getRuntime().exec(cmd); - is = process.getInputStream(); - JSONObject json = new JSONObject(fromInputStream(is)); - - // Append default peer credentials. TODO Remove. - json.getJSONObject("interfaces") - .getJSONArray("UDPInterface") - .getJSONObject(0) - .getJSONObject("connectTo") - .put(DEFAULT_PEER_INTERFACE, new JSONObject(DEFAULT_PEER_CREDENTIALS)); - - // Write configurations to file. - os = appContext.openFileOutput(FILENAME_CJDROUTE_CONF, Context.MODE_PRIVATE); - os.write(json.toString().getBytes()); - os.flush(); - - // Return JSON object and complete Rx contract. - subscriber.onNext(json); - subscriber.onCompleted(); - } catch (IOException | JSONException e) { - subscriber.onError(e); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Do nothing. - } - } - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - } else { - subscriber.onError(new IOException("Failed to execute cjdroute in " + cjdroutefile.getPath())); - } - } else { - subscriber.onError(new FileNotFoundException("Failed to find cjdroute in " + cjdroutefile.getPath())); + } catch (IOException e) { + subscriber.onError(e); } } } @@ -198,6 +255,74 @@ public void call(Subscriber subscriber) { }); } + /** + * Creates a {@link berlin.meshnet.cjdns.model.Node.Me}. + * + * @param address The ipv6 address. + * @param publicKey The public key. + * @param privateKey The private key. + * @return The {@link berlin.meshnet.cjdns.model.Node.Me}; or {@code null} if invalid input. + */ + private static Node.Me from(String address, String publicKey, String privateKey) { + if (!TextUtils.isEmpty(address) && !TextUtils.isEmpty(publicKey) && !TextUtils.isEmpty(privateKey)) { + return new Node.Me("Me", address, publicKey, privateKey); + } + return null; + } + + /** + * Copies a file from assets folder and makes executable. If the file is already in that state, + * this is a no-op. + * + * @param context The {@link Context}. + * @param filesDir The files directory. + * @param filename The filename to copy. + * @return The copied executable file. + * @throws IOException Thrown if copying failed. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static File copyExecutable(Context context, String filesDir, String filename) throws IOException { + // If file is not present in the files directory, it needs to be copied over from assets. + File copyFile = new File(filesDir, filename); + if (!copyFile.exists()) { + // Copy file from assets folder to the files directory. + InputStream is = null; + FileOutputStream os = null; + try { + String abi = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI; + is = context.getAssets().open(abi + "/" + filename); + os = context.openFileOutput(filename, Context.MODE_PRIVATE); + copyStream(is, os); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + if (os != null) { + try { + os.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + } + + // Check file existence and permissions. + if (copyFile.exists()) { + if (copyFile.canExecute() || copyFile.setExecutable(true)) { + return copyFile; + } else { + throw new IOException("Failed to make " + copyFile + " executable in " + copyFile.getPath()); + } + } else { + throw new FileNotFoundException("Failed to create " + copyFile + " in " + copyFile.getPath()); + } + } + /** * Writes an {@link InputStream} to an {@link OutputStream}. * diff --git a/src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java b/src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java new file mode 100644 index 0000000..b19078d --- /dev/null +++ b/src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java @@ -0,0 +1,54 @@ +package berlin.meshnet.cjdns; + +import java.io.IOException; +import java.util.Locale; + +import rx.Observable; +import rx.Subscriber; + +/** + * Utility for sending a file descriptor through a named pipe. + */ +public class FileDescriptorSender { + + static { + System.loadLibrary("sendfd"); + } + + /** + * Native method for sending file descriptor through the named pipe. + * + * @param path The path to the named pipe. + * @param file_descriptor The file descriptor. + * @return {@code 0} if successful; {@code -1} if failed. + */ + public static native int sendfd(String path, int file_descriptor); + + /** + * Sends a file descriptor through the named pipe. + * + * @param path The path to the named pipe. + * @param fd The file descriptor. + * @return {@link Observable} that emits {@code true} if successful; otherwise + * {@link Subscriber#onError(Throwable)} is called. + */ + static Observable send(final String path, final int fd) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + try { + if (FileDescriptorSender.sendfd(path, fd) == 0) { + subscriber.onNext(true); + subscriber.onCompleted(); + } else { + Exception e = new IOException(String.format(Locale.ENGLISH, + "Failed to send file descriptor %1$s to named pipe %2$s", fd, path)); + subscriber.onError(e); + } + } catch (Exception e) { + subscriber.onError(e); + } + } + }); + } +} diff --git a/src/main/java/berlin/meshnet/cjdns/MainActivity.java b/src/main/java/berlin/meshnet/cjdns/MainActivity.java index 19c406a..7de6dbc 100644 --- a/src/main/java/berlin/meshnet/cjdns/MainActivity.java +++ b/src/main/java/berlin/meshnet/cjdns/MainActivity.java @@ -1,8 +1,11 @@ package berlin.meshnet.cjdns; +import android.annotation.TargetApi; import android.content.Intent; import android.content.res.Configuration; import android.content.res.TypedArray; +import android.net.VpnService; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; @@ -15,6 +18,7 @@ import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SwitchCompat; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -30,6 +34,7 @@ import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -48,12 +53,13 @@ import butterknife.InjectView; import rx.Subscription; import rx.android.app.AppObservable; -import rx.functions.Action0; import rx.functions.Action1; import rx.schedulers.Schedulers; public class MainActivity extends AppCompatActivity { + private static final String TAG = MainActivity.class.getSimpleName(); + private static final String BUNDLE_KEY_SELECTED_CONTENT = "selectedContent"; @Inject @@ -169,37 +175,38 @@ public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); - // Set initial state of toggle and click behaviour. + // Configure toggle click behaviour. final SwitchCompat cjdnsServiceSwitch = (SwitchCompat) MenuItemCompat.getActionView(menu.findItem(R.id.switch_cjdns_service)); - mSubscriptions.add(AppObservable.bindActivity(this, Cjdroute.running(this) - .subscribeOn(Schedulers.io())) - .subscribe(new Action1() { - @Override - public void call(Integer pid) { - // Change toggle check state if there is a currently running cjdroute process. - cjdnsServiceSwitch.setChecked(true); - } - }, new Action1() { - @Override - public void call(Throwable throwable) { - // Do nothing. - } - }, new Action0() { - @Override - public void call() { - // Configure toggle click behaviour. - cjdnsServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - mBus.post(new ApplicationEvents.StartCjdnsService()); - } else { - mBus.post(new ApplicationEvents.StopCjdnsService()); - } - } - }); - } - })); + cjdnsServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mBus.post(new ApplicationEvents.StartCjdnsService()); + } else { + mBus.post(new ApplicationEvents.StopCjdnsService()); + } + } + }); + + // Set initial state of toggle and click behaviour. + try { + mSubscriptions.add(AppObservable.bindActivity(this, Cjdroute.running() + .subscribeOn(Schedulers.io())) + .subscribe(new Action1() { + @Override + public void call(Long pid) { + // Change toggle check state if there is a currently running cjdroute process. + cjdnsServiceSwitch.setChecked(pid != Cjdroute.INVALID_PID); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + cjdnsServiceSwitch.setChecked(false); + } + })); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } return super.onCreateOptionsMenu(menu); } @@ -240,10 +247,21 @@ protected void onDestroy() { super.onDestroy(); } + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Subscribe public void handleEvent(ApplicationEvents.StartCjdnsService event) { Toast.makeText(getApplicationContext(), "Starting CjdnsService", Toast.LENGTH_SHORT).show(); - startService(new Intent(getApplicationContext(), CjdnsService.class)); + + // Start cjdns VPN. + Intent intent = VpnService.prepare(this); + if (intent != null) { + startActivityForResult(intent, 0); + } else { + onActivityResult(0, RESULT_OK, null); + } + + // TODO Compat. +// startService(new Intent(getApplicationContext(), CjdnsService.class)); } @Subscribe @@ -251,12 +269,17 @@ public void handleEvent(ApplicationEvents.StopCjdnsService event) { Toast.makeText(getApplicationContext(), "Stopping CjdnsService", Toast.LENGTH_SHORT).show(); // Kill cjdroute process. - Cjdroute.running(this) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe(mCjdroute.terminate()); + try { + Cjdroute.running() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(mCjdroute.terminate()); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } - stopService(new Intent(getApplicationContext(), CjdnsService.class)); + // TODO Compat. +// stopService(new Intent(getApplicationContext(), CjdnsService.class)); } @Subscribe @@ -316,4 +339,11 @@ public boolean onOptionsItemSelected(MenuItem item) { } return super.onOptionsItemSelected(item); } + + @Override + protected void onActivityResult(int request, int result, Intent data) { + if (result == RESULT_OK) { + startService(new Intent(this, CjdnsVpnService.class)); + } + } } diff --git a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java index 805bcda..7c04f0b 100644 --- a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java @@ -219,7 +219,7 @@ public View getView(int position, View convertView, ViewGroup parent) { public void onClick(View v) { List connections = new ArrayList<>(Arrays.asList(mPeer.outgoingConnections)); connections.remove(credential); - Node.Peer update = new Node.Peer(mPeer.id, mPeer.name, mPeer.publicKey, + Node.Peer update = new Node.Peer(mPeer.id, mPeer.name, "", mPeer.publicKey, connections.toArray(new Credential[connections.size()])); mBus.post(new PeerEvents.Update(update)); } diff --git a/src/main/java/berlin/meshnet/cjdns/model/Node.java b/src/main/java/berlin/meshnet/cjdns/model/Node.java index 86b2666..3e50e40 100644 --- a/src/main/java/berlin/meshnet/cjdns/model/Node.java +++ b/src/main/java/berlin/meshnet/cjdns/model/Node.java @@ -7,14 +7,14 @@ public abstract class Node { public final String name; - public final String publicKey; - public final String address; - public Node(String name, String publicKey) { + public final String publicKey; + + public Node(String name, String address, String publicKey) { this.name = name; + this.address = address; this.publicKey = publicKey; - this.address = "fc00:0000:0000:0000:0000:0000:0000:0000"; } /** @@ -26,8 +26,8 @@ public static class Me extends Node { public final Stats.Me stats; - public Me(String name, String publicKey, String privateKey) { - super(name, publicKey); + public Me(String name, String address, String publicKey, String privateKey) { + super(name, address, publicKey); this.privateKey = privateKey; this.stats = new Stats.Me("", true, 0L, 0, 0, 0, 0, 0, 0); } @@ -44,8 +44,8 @@ public static class Peer extends Node { public final Stats stats; - public Peer(int id, String name, String publicKey, Credential[] outgoingConnections) { - super(name, publicKey); + public Peer(int id, String name, String address, String publicKey, Credential[] outgoingConnections) { + super(name, address, publicKey); this.id = id; this.outgoingConnections = outgoingConnections; this.stats = new Stats("", true, 0L, 0, 0, 0, 0, 0); diff --git a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java index ab243fb..b976f82 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java @@ -19,7 +19,7 @@ class Mock implements MeProducer { @Override public Observable stream() { BehaviorSubject stream = BehaviorSubject.create(); - return stream.startWith(new Node.Me("Hyperborean", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); + return stream.startWith(new Node.Me("Hyperborean", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); } } } diff --git a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java index b3c96f1..791a358 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java @@ -30,15 +30,15 @@ public interface PeersProducer { class Mock implements PeersProducer { private static List sPeers = new ArrayList() {{ - add(new Node.Peer(0, "Alice", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ + add(new Node.Peer(0, "Alice", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ new Credential(0, "Alice credential 0", new Protocol(Protocol.Interface.udp, Protocol.Link.wifiDirect), "Loremipsumdolorsitametpharetrae"), new Credential(1, "Alice credential 1", new Protocol(Protocol.Interface.eth, Protocol.Link.bluetooth), "Loremipsumdolorsitametpharetrae") })); - add(new Node.Peer(1, "Bob", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ + add(new Node.Peer(1, "Bob", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ new Credential(2, "Bob credential 0", new Protocol(Protocol.Interface.udp, Protocol.Link.overlay), "Loremipsumdolorsitametpharetrae") })); - add(new Node.Peer(2, "Caleb", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{})); - add(new Node.Peer(3, "Danielle", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null)); + add(new Node.Peer(2, "Caleb", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{})); + add(new Node.Peer(3, "Danielle", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null)); }}; private ReplaySubject mCreateStream = ReplaySubject.create(); @@ -64,7 +64,7 @@ public Observable removeStream() { @Subscribe public void handleEvent(PeerEvents.Create event) { - Node.Peer peer = new Node.Peer(sPeers.size(), UUID.randomUUID().toString(), + Node.Peer peer = new Node.Peer(sPeers.size(), UUID.randomUUID().toString(), "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null); sPeers.add(peer); mCreateStream.onNext(peer); diff --git a/src/main/jni/Android.mk b/src/main/jni/Android.mk new file mode 100644 index 0000000..b268f45 --- /dev/null +++ b/src/main/jni/Android.mk @@ -0,0 +1,14 @@ +# Path of the sources +JNI_DIR := $(call my-dir) + +LOCAL_PATH := $(JNI_DIR) + +# The only real JNI libraries +include $(CLEAR_VARS) +LOCAL_CFLAGS = -DTARGET_ARCH_ABI=\"${TARGET_ARCH_ABI}\" +LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog +LOCAL_SRC_FILES:= sendfd.cpp +LOCAL_MODULE = sendfd +include $(BUILD_SHARED_LIBRARY) +Truct Sockaddr* lladdr = Sockaddr_clone(lladdrParm, epAlloc); + diff --git a/src/main/jni/Application.mk b/src/main/jni/Application.mk new file mode 100644 index 0000000..499b8ac --- /dev/null +++ b/src/main/jni/Application.mk @@ -0,0 +1,12 @@ +APP_ABI := arm64-v8a armeabi armeabi-v7a mips x86 x86_64 +APP_PLATFORM := android-23 + +APP_STL:=stlport_static +#APP_STL:=gnustl_shared + +#APP_OPTIM := release + +#LOCAL_ARM_MODE := arm + +#NDK_TOOLCHAIN_VERSION=clang + diff --git a/src/main/jni/sendfd.cpp b/src/main/jni/sendfd.cpp new file mode 100644 index 0000000..e46bba4 --- /dev/null +++ b/src/main/jni/sendfd.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "sendfd.h" + +jint Java_berlin_meshnet_cjdns_FileDescriptorSender_sendfd(JNIEnv *env, jobject thiz, jstring path, jint file_descriptor) +{ + int fd, len, err, rval; + const char *pipe_path = env->GetStringUTFChars(path, 0); + struct sockaddr_un un; + char buf[256]; + +#ifndef NDEBUG + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd() called with [%s] [%d]", pipe_path, file_descriptor); +#endif + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { +#ifndef NDEBUG + char *errstr = strerror(errno); + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd socket() failed [%s]", errstr); +#endif + return (jint)-1; + } + + memset(&un, 0, sizeof(un)); + un.sun_family = AF_UNIX; + strcpy(un.sun_path, pipe_path); + if (connect(fd, (struct sockaddr *)&un, sizeof(struct sockaddr_un)) < 0) { +#ifndef NDEBUG + char *errstr = strerror(errno); + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd connect() failed [%s]", errstr); +#endif + close(fd); + return (jint)-1; + } + + struct msghdr msg; + struct iovec iov[1]; + + union { + struct cmsghdr cm; + char control[CMSG_SPACE(sizeof(int))]; + } control_un; + struct cmsghdr *cmptr; + + msg.msg_control = control_un.control; + msg.msg_controllen = sizeof(control_un.control); + + cmptr = CMSG_FIRSTHDR(&msg); + cmptr->cmsg_len = CMSG_LEN(sizeof(int)); + cmptr->cmsg_level = SOL_SOCKET; + cmptr->cmsg_type = SCM_RIGHTS; + *((int *) CMSG_DATA(cmptr)) = file_descriptor; + + msg.msg_name = NULL; + msg.msg_namelen = 0; + + iov[0].iov_base = buf; + iov[0].iov_len = sizeof(buf); + msg.msg_iov = iov; + msg.msg_iovlen = 1; + + int r; + if ((r = sendmsg(fd, &msg, MSG_NOSIGNAL)) < 0) { +#ifndef NDEBUG + char *errstr = strerror(errno); + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd sendmsg() failed [%s]", errstr); +#endif + close(fd); + return (jint)-1; + } + + close(fd); + return (jint)0; +} diff --git a/src/main/jni/sendfd.h b/src/main/jni/sendfd.h new file mode 100644 index 0000000..4b0f1be --- /dev/null +++ b/src/main/jni/sendfd.h @@ -0,0 +1,11 @@ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +jint Java_berlin_meshnet_cjdns_FileDescriptorSender_sendfd(JNIEnv *env, jobject thiz, jstring path, jint file_descriptor); + +#ifdef __cplusplus +} +#endif From d142a191f614ce27415864718bb79290a6d848a2 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Thu, 28 Apr 2016 23:34:22 -0400 Subject: [PATCH 03/24] Add a second public peer --- .../berlin/meshnet/cjdns/CjdnsVpnService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java index c670151..cd497b4 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java @@ -104,6 +104,23 @@ public Observable call(Long udpInterfaceNumber) { "8fVMl0oo6QI6wKeMneuY26x1MCgRemg"); } }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return AdminApi.UdpInterface.new0(api); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Long udpInterfaceNumber) { + return AdminApi.UdpInterface.beginConnection(api, + "2scyvybg4qqms1c5c9nyt50b1cdscxnr6ycpwsxf6pccbmwuynk0.k", + "159.203.5.91:30664", + udpInterfaceNumber, + "android-public", + "kj1rur4buavtyp2mavch5nghsnd4bpf"); + } + }) .flatMap(new Func1>() { @Override public Observable call(Boolean isSuccessful) { From 2ca111d10ca7453075a69558fd78a64218fd133d Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Fri, 29 Apr 2016 02:21:29 -0400 Subject: [PATCH 04/24] Hook up basic UI to AdminApi --- .../java/berlin/meshnet/cjdns/AdminApi.java | 31 +++++++- .../meshnet/cjdns/CjdnsApplication.java | 11 ++- .../berlin/meshnet/cjdns/CjdrouteConf.java | 2 +- .../berlin/meshnet/cjdns/MainActivity.java | 14 +++- .../meshnet/cjdns/page/MePageFragment.java | 2 +- .../meshnet/cjdns/producer/MeProducer.java | 20 ++++- .../meshnet/cjdns/producer/PeersProducer.java | 77 +++++++++++++++++++ 7 files changed, 144 insertions(+), 13 deletions(-) diff --git a/src/main/java/berlin/meshnet/cjdns/AdminApi.java b/src/main/java/berlin/meshnet/cjdns/AdminApi.java index 1e032c1..f6bda3f 100644 --- a/src/main/java/berlin/meshnet/cjdns/AdminApi.java +++ b/src/main/java/berlin/meshnet/cjdns/AdminApi.java @@ -19,15 +19,18 @@ import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import berlin.meshnet.cjdns.model.Node; import rx.Observable; import rx.Subscriber; /** * API for administration of the cjdns node. */ -class AdminApi { +public class AdminApi { private static final String TAG = AdminApi.class.getSimpleName(); @@ -223,8 +226,30 @@ public static Observable disconnectPeer(final AdminApi api) { throw new UnsupportedOperationException("InterfaceController_disconnectPeer is not implemented in " + CLASS_NAME); } - public static Observable peerStatsConnection(final AdminApi api) { - throw new UnsupportedOperationException("InterfaceController_peerStats is not implemented in " + CLASS_NAME); + public static Observable> peerStats(final AdminApi api) { + return Observable.create(new BaseOnSubscribe>(api, new Request("InterfaceController_peerStats", + new LinkedHashMap() {{ + // TODO Handle paging. + put(wrapString("page"), Long.valueOf(0L)); + }})) { + @Override + protected List parseResult(final Map response) { + List peers = new LinkedList<>(); + List peerStats = (List) response.get(wrapString("peers")); + for (Object entry : peerStats) { + Object user = ((Map) entry).get(wrapString("user")); + Object addr = ((Map) entry).get(wrapString("addr")); + Object publicKey = ((Map) entry).get(wrapString("publicKey")); + peers.add(new Node.Peer(peers.size(), + user != null ? new String(((ByteBuffer) user).array()) : null, + user != null ? new String(((ByteBuffer) addr).array()) : null, + user != null ? new String(((ByteBuffer) publicKey).array()) : null, + null + )); + } + return peers; + } + }); } public static Observable resetPeering(final AdminApi api) { diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java index 1c7e093..194313b 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java @@ -8,6 +8,8 @@ import com.squareup.otto.Bus; +import java.net.UnknownHostException; + import javax.inject.Singleton; import berlin.meshnet.cjdns.dialog.ConnectionsDialogFragment; @@ -105,12 +107,17 @@ public SettingsProducer provideSettingsProducer(Context context, SharedPreferenc @Provides public MeProducer provideMeProducer() { - return new MeProducer.Mock(); + return new MeProducer.Default(); } @Provides public PeersProducer providePeerListProducer() { - return new PeersProducer.Mock(); + try { + return new PeersProducer.Default(); + } catch (UnknownHostException e) { + // TODO + } + return null; } @Provides diff --git a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java index 07b1856..2e98cc1 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java @@ -28,7 +28,7 @@ /** * Configurations for cjdroute. */ -abstract class CjdrouteConf { +public abstract class CjdrouteConf { /** * The filename for the cjdroute configurations. diff --git a/src/main/java/berlin/meshnet/cjdns/MainActivity.java b/src/main/java/berlin/meshnet/cjdns/MainActivity.java index 7de6dbc..00c4e09 100644 --- a/src/main/java/berlin/meshnet/cjdns/MainActivity.java +++ b/src/main/java/berlin/meshnet/cjdns/MainActivity.java @@ -84,6 +84,8 @@ public class MainActivity extends AppCompatActivity { private List mSubscriptions = new ArrayList<>(); + private boolean mIsCjdnsRunning = false; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -175,15 +177,19 @@ public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); + // TODO Sync toggle properly. + // Configure toggle click behaviour. final SwitchCompat cjdnsServiceSwitch = (SwitchCompat) MenuItemCompat.getActionView(menu.findItem(R.id.switch_cjdns_service)); cjdnsServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { + if (isChecked && !mIsCjdnsRunning) { mBus.post(new ApplicationEvents.StartCjdnsService()); - } else { + mIsCjdnsRunning = true; + } else if (!isChecked && mIsCjdnsRunning) { mBus.post(new ApplicationEvents.StopCjdnsService()); + mIsCjdnsRunning = false; } } }); @@ -196,12 +202,12 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @Override public void call(Long pid) { // Change toggle check state if there is a currently running cjdroute process. - cjdnsServiceSwitch.setChecked(pid != Cjdroute.INVALID_PID); + cjdnsServiceSwitch.setChecked(mIsCjdnsRunning = pid != Cjdroute.INVALID_PID); } }, new Action1() { @Override public void call(Throwable throwable) { - cjdnsServiceSwitch.setChecked(false); + cjdnsServiceSwitch.setChecked(mIsCjdnsRunning = false); } })); } catch (UnknownHostException e) { diff --git a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java index fe436f1..d03cba5 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java @@ -73,7 +73,7 @@ public void call(Theme theme) { } })); - mSubscriptions.add(AppObservable.bindFragment(this, mMeProducer.stream()) + mSubscriptions.add(AppObservable.bindFragment(this, mMeProducer.stream(getContext())) .subscribe(new Action1() { @Override public void call(Node.Me me) { diff --git a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java index b976f82..be0d99b 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java @@ -1,7 +1,11 @@ package berlin.meshnet.cjdns.producer; +import android.content.Context; + +import berlin.meshnet.cjdns.CjdrouteConf; import berlin.meshnet.cjdns.model.Node; import rx.Observable; +import rx.schedulers.Schedulers; import rx.subjects.BehaviorSubject; /** @@ -9,7 +13,19 @@ */ public interface MeProducer { - Observable stream(); + Observable stream(Context context); + + /** + * Default implementation of a {@link MeProducer}. + */ + class Default implements MeProducer { + + @Override + public Observable stream(Context context) { + return CjdrouteConf.fetch0(context) + .subscribeOn(Schedulers.io()); + } + } /** * Mock implementation of a {@link MeProducer}. @@ -17,7 +33,7 @@ public interface MeProducer { class Mock implements MeProducer { @Override - public Observable stream() { + public Observable stream(Context context) { BehaviorSubject stream = BehaviorSubject.create(); return stream.startWith(new Node.Me("Hyperborean", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); } diff --git a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java index 791a358..2b97360 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java @@ -2,15 +2,21 @@ import com.squareup.otto.Subscribe; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; +import berlin.meshnet.cjdns.AdminApi; import berlin.meshnet.cjdns.event.PeerEvents; import berlin.meshnet.cjdns.model.Credential; import berlin.meshnet.cjdns.model.Node; import berlin.meshnet.cjdns.model.Protocol; import rx.Observable; +import rx.Subscriber; +import rx.functions.Func1; +import rx.schedulers.Schedulers; import rx.subjects.ReplaySubject; /** @@ -24,6 +30,77 @@ public interface PeersProducer { Observable removeStream(); + /** + * Default implementation of a {@link PeersProducer}. + */ + class Default implements PeersProducer { + + private AdminApi mApi; + + private List mPeers = new ArrayList<>(); + + private ReplaySubject mCreateStream = ReplaySubject.create(); + + private ReplaySubject mUpdateStream = ReplaySubject.create(); + + private ReplaySubject mRemoveStream = ReplaySubject.create(); + + public Default() throws UnknownHostException { + mApi = new AdminApi(); + } + + @Override + public Observable createStream() { + return mCreateStream.startWith( + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + List peers = AdminApi.InterfaceController.peerStats(mApi) + .toBlocking().first(); + for (Node.Peer peer : peers) { + subscriber.onNext(peer); + } + subscriber.onCompleted(); + } + })) + .subscribeOn(Schedulers.io()); + } + + @Override + public Observable updateStream() { + return mUpdateStream.subscribeOn(Schedulers.io()); + } + + @Override + public Observable removeStream() { + return mRemoveStream.subscribeOn(Schedulers.io()); + } + + @Subscribe + public void handleEvent(PeerEvents.Create event) { + Node.Peer peer = new Node.Peer(mPeers.size(), UUID.randomUUID().toString(), "fc::/8", + "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null); + mPeers.add(peer); + mCreateStream.onNext(peer); + } + + @Subscribe + public void handleEvent(PeerEvents.Update event) { + int index = mPeers.indexOf(event.mPeer); + if (mPeers.remove(event.mPeer)) { + mPeers.add(index, event.mPeer); + mUpdateStream.onNext(event.mPeer); + } + } + + @Subscribe + public void handleEvent(PeerEvents.Remove event) { + if (mPeers.remove(event.mPeer)) { + mRemoveStream.onNext(event.mPeer); + } + } + } + /** * Mock implementation of a {@link PeersProducer}. */ From 86384816f3072032a551bf1668cf2e191dd821b4 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Fri, 29 Apr 2016 03:12:27 -0400 Subject: [PATCH 05/24] Reset IPv6 identity of node --- .../berlin/meshnet/cjdns/CjdnsService.java | 5 ++++ .../berlin/meshnet/cjdns/CjdnsVpnService.java | 17 ++++++++++++- .../berlin/meshnet/cjdns/MainActivity.java | 14 +++++++---- .../dialog/ConnectionsDialogFragment.java | 10 ++++++++ .../cjdns/page/CredentialsPageFragment.java | 20 ++++++++++++++++ .../meshnet/cjdns/page/MePageFragment.java | 10 ++++++++ .../meshnet/cjdns/page/PeersPageFragment.java | 20 ++++++++++++++++ .../cjdns/page/SettingsPageFragment.java | 24 +++++++++++++++++++ .../meshnet/cjdns/producer/MeProducer.java | 7 ++++-- .../meshnet/cjdns/producer/PeersProducer.java | 7 ++++-- src/main/res/values/strings.xml | 3 +++ src/main/res/xml/preferences.xml | 4 ++++ 12 files changed, 132 insertions(+), 9 deletions(-) diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java index 4b3faaa..969dc3d 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java @@ -74,6 +74,11 @@ public void onCreate() { public void call(JSONObject cjdrouteConf) { startForeground(NOTIFICATION_ID, buildNotification(cjdrouteConf)); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed in subscribe", throwable); + } })); // Execute cjdroute. diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java index cd497b4..2b7bc0a 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java @@ -7,6 +7,9 @@ import android.os.ParcelFileDescriptor; import android.util.Log; +import com.squareup.otto.Bus; +import com.squareup.otto.Subscribe; + import java.io.IOException; import java.net.UnknownHostException; import java.util.Locale; @@ -16,6 +19,7 @@ import javax.inject.Inject; +import berlin.meshnet.cjdns.event.ApplicationEvents; import berlin.meshnet.cjdns.model.Node; import rx.Observable; import rx.Subscriber; @@ -61,6 +65,9 @@ public class CjdnsVpnService extends VpnService { private ParcelFileDescriptor mInterface; + @Inject + Bus mBus; + @Inject Cjdroute mCjdroute; @@ -69,6 +76,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Inject dependencies. ((CjdnsApplication) getApplication()).inject(this); + // Register so we can subscribe to stop events. + mBus.register(this); + final String pipePath = String.format(Locale.ENGLISH, SEND_FD_PIPE_PATH_TEMPLATE, getFilesDir().getPath(), UUID.randomUUID()); try { @@ -203,11 +213,16 @@ public void call(Throwable throwable) { @Override public void onDestroy() { + mBus.unregister(this); close(); - // TODO Purge stale pipes. } + @Subscribe + public void handleEvent(ApplicationEvents.StopCjdnsService event) { + stopSelf(); + } + /** * Close any existing session. */ diff --git a/src/main/java/berlin/meshnet/cjdns/MainActivity.java b/src/main/java/berlin/meshnet/cjdns/MainActivity.java index 00c4e09..a5d7f98 100644 --- a/src/main/java/berlin/meshnet/cjdns/MainActivity.java +++ b/src/main/java/berlin/meshnet/cjdns/MainActivity.java @@ -74,6 +74,8 @@ public class MainActivity extends AppCompatActivity { @InjectView(R.id.drawer) ListView mDrawer; + private SwitchCompat mSwitch; + private ActionBarDrawerToggle mDrawerToggle; private ArrayAdapter mDrawerAdapter; @@ -180,8 +182,8 @@ public boolean onCreateOptionsMenu(Menu menu) { // TODO Sync toggle properly. // Configure toggle click behaviour. - final SwitchCompat cjdnsServiceSwitch = (SwitchCompat) MenuItemCompat.getActionView(menu.findItem(R.id.switch_cjdns_service)); - cjdnsServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + mSwitch = (SwitchCompat) MenuItemCompat.getActionView(menu.findItem(R.id.switch_cjdns_service)); + mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked && !mIsCjdnsRunning) { @@ -202,12 +204,12 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @Override public void call(Long pid) { // Change toggle check state if there is a currently running cjdroute process. - cjdnsServiceSwitch.setChecked(mIsCjdnsRunning = pid != Cjdroute.INVALID_PID); + mSwitch.setChecked(mIsCjdnsRunning = pid != Cjdroute.INVALID_PID); } }, new Action1() { @Override public void call(Throwable throwable) { - cjdnsServiceSwitch.setChecked(mIsCjdnsRunning = false); + mSwitch.setChecked(mIsCjdnsRunning = false); } })); } catch (UnknownHostException e) { @@ -270,6 +272,7 @@ public void handleEvent(ApplicationEvents.StartCjdnsService event) { // startService(new Intent(getApplicationContext(), CjdnsService.class)); } + @Subscribe public void handleEvent(ApplicationEvents.StopCjdnsService event) { Toast.makeText(getApplicationContext(), "Stopping CjdnsService", Toast.LENGTH_SHORT).show(); @@ -284,6 +287,9 @@ public void handleEvent(ApplicationEvents.StopCjdnsService event) { Log.e(TAG, "Failed to start AdminApi", e); } + // TODO Do this properly. + mSwitch.setChecked(false); + // TODO Compat. // stopService(new Intent(getApplicationContext(), CjdnsService.class)); } diff --git a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java index 7c04f0b..882350e 100644 --- a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java @@ -152,6 +152,11 @@ public void call(Theme theme) { mIsInternalsVisible = theme.isInternalsVisible; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(peerStream.subscribe(new Action1() { @@ -160,6 +165,11 @@ public void call(Node.Peer peer) { mPeer = peer; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } diff --git a/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java index ea4ba8b..cd9c86a 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java @@ -185,6 +185,11 @@ public void call(Theme theme) { mIsInternalsVisible = theme.isInternalsVisible; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(createStream.subscribe(new Action1() { @@ -193,6 +198,11 @@ public void call(Credential.Authorized credential) { mCredentials.add(credential); notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(updateStream.subscribe(new Action1() { @@ -204,6 +214,11 @@ public void call(Credential.Authorized credential) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(removeStream.subscribe(new Action1() { @@ -215,6 +230,11 @@ public void call(Credential.Authorized credential) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } diff --git a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java index d03cba5..d69eec2 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java @@ -71,6 +71,11 @@ public void onActivityCreated(Bundle savedInstanceState) { public void call(Theme theme) { mPublicKey.setVisibility(theme.isInternalsVisible ? View.VISIBLE : View.GONE); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(AppObservable.bindFragment(this, mMeProducer.stream(getContext())) @@ -81,6 +86,11 @@ public void call(Node.Me me) { mAddressTextView.setText(me.address); mPublicKeyTextView.setText(me.publicKey); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } diff --git a/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java index 18c678a..ec116df 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java @@ -158,6 +158,11 @@ public void call(Theme theme) { mIsInternalsVisible = theme.isInternalsVisible; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(createStream.subscribe(new Action1() { @@ -166,6 +171,11 @@ public void call(Node.Peer peer) { mPeers.add(peer); notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(updateStream.subscribe(new Action1() { @@ -177,6 +187,11 @@ public void call(Node.Peer peer) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(removeStream.subscribe(new Action1() { @@ -188,6 +203,11 @@ public void call(Node.Peer peer) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } diff --git a/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java index 66e7f98..bbd25b7 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java @@ -7,17 +7,21 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; import android.provider.Settings; import android.support.v4.app.Fragment; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; +import com.squareup.otto.Bus; + import java.io.File; import javax.inject.Inject; import berlin.meshnet.cjdns.CjdnsApplication; import berlin.meshnet.cjdns.R; +import berlin.meshnet.cjdns.event.ApplicationEvents; /** * The page to configure application settings. @@ -26,6 +30,9 @@ public class SettingsPageFragment extends PreferenceFragmentCompat { private static final String TYPE_APK = "image/apk"; + @Inject + Bus mBus; + @Inject SharedPreferences mSharedPreferences; @@ -80,6 +87,23 @@ public boolean onPreferenceClick(Preference preference) { preference.setEnabled(false); } + String resetIdentityKey = getString(R.string.setting_reset_identity_key); + getPreferenceManager().findPreference(resetIdentityKey) + .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // Wipe node info. + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.clear(); + editor.apply(); + + // Shut off node. + mBus.post(new ApplicationEvents.StopCjdnsService()); + + return true; + } + }); + String sendApkKey = getString(R.string.setting_send_apk_key); getPreferenceManager().findPreference(sendApkKey) .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { diff --git a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java index be0d99b..57e6a6b 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java @@ -34,8 +34,11 @@ class Mock implements MeProducer { @Override public Observable stream(Context context) { - BehaviorSubject stream = BehaviorSubject.create(); - return stream.startWith(new Node.Me("Hyperborean", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); + return BehaviorSubject.create() + .startWith(new Node.Me("Me", + "fc00::/8", + "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", + "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); } } } diff --git a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java index 2b97360..027046f 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java @@ -78,8 +78,11 @@ public Observable removeStream() { @Subscribe public void handleEvent(PeerEvents.Create event) { - Node.Peer peer = new Node.Peer(mPeers.size(), UUID.randomUUID().toString(), "fc::/8", - "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null); + Node.Peer peer = new Node.Peer(mPeers.size(), + UUID.randomUUID().toString(), + "fc00::/8", + "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", + null); mPeers.add(peer); mCreateStream.onNext(peer); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 786d539..4db3c11 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ setting_verbose_enabled_key setting_encrypt_enabled_key + setting_reset_identity_key setting_send_apk_key setting_link_wifi_direct_key setting_link_bluetooth_key @@ -46,6 +47,8 @@ Enabled Configure device storage encryption Not supported + Reset Identity + Wipe IPv6 of this node Share Application Send APK to another device diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 0dc23af..6956442 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -12,6 +12,10 @@ android:enabled="false" android:key="@string/setting_encrypt_enabled_key" android:title="@string/settings_page_setting_encrypt_title" /> + Date: Fri, 29 Apr 2016 03:14:26 -0400 Subject: [PATCH 06/24] Hide Connectivity Settings --- src/main/res/xml/preferences.xml | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 6956442..579d73b 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -22,24 +22,24 @@ android:title="@string/settings_page_setting_send_apk_title" /> - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 09e86385d3d9c996ddf68638ef721d8a57c8c016 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Fri, 29 Apr 2016 03:29:58 -0400 Subject: [PATCH 07/24] Sharing cjdns IPv6 on click --- .../berlin/meshnet/cjdns/MainActivity.java | 2 +- .../meshnet/cjdns/page/MePageFragment.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/berlin/meshnet/cjdns/MainActivity.java b/src/main/java/berlin/meshnet/cjdns/MainActivity.java index a5d7f98..10800a1 100644 --- a/src/main/java/berlin/meshnet/cjdns/MainActivity.java +++ b/src/main/java/berlin/meshnet/cjdns/MainActivity.java @@ -209,7 +209,7 @@ public void call(Long pid) { }, new Action1() { @Override public void call(Throwable throwable) { - mSwitch.setChecked(mIsCjdnsRunning = false); +// mSwitch.setChecked(mIsCjdnsRunning = false); } })); } catch (UnknownHostException e) { diff --git a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java index d69eec2..95c9845 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java @@ -1,5 +1,6 @@ package berlin.meshnet.cjdns.page; +import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; @@ -92,6 +93,29 @@ public void call(Throwable throwable) { // TODO } })); + + // Share address on click. + mAddressTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mMeProducer.stream(getContext()) + .subscribe(new Action1() { + @Override + public void call(Node.Me me) { + Intent intent = new Intent(android.content.Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(android.content.Intent.EXTRA_SUBJECT, "cjdns IPv6"); + intent.putExtra(android.content.Intent.EXTRA_TEXT, me.address); + startActivity(Intent.createChooser(intent, "Share using...")); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } + }); + } + }); } @Override From 8f92f7af5284899fdcf5582b350ca6f42318605b Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Sat, 30 Apr 2016 02:30:55 -0400 Subject: [PATCH 08/24] Proper shutdown sequence of VPN service --- .../berlin/meshnet/cjdns/CjdnsVpnService.java | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java index 2b7bc0a..26a8ec2 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java @@ -23,6 +23,7 @@ import berlin.meshnet.cjdns.model.Node; import rx.Observable; import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; @@ -63,6 +64,9 @@ public class CjdnsVpnService extends VpnService { */ private static final String SEND_FD_PIPE_PATH_TEMPLATE = "%1$s/pipe_%2$s"; + /** + * VPN interface. + */ private ParcelFileDescriptor mInterface; @Inject @@ -79,6 +83,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Register so we can subscribe to stop events. mBus.register(this); + // Start cjdns process and VPN. final String pipePath = String.format(Locale.ENGLISH, SEND_FD_PIPE_PATH_TEMPLATE, getFilesDir().getPath(), UUID.randomUUID()); try { @@ -97,6 +102,12 @@ public Observable call(Boolean isSuccessful) { return AdminApi.Security.setupComplete(api); } }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return close(); + } + }) .flatMap(new Func1>() { @Override public Observable call(Boolean isSuccessful) { @@ -143,9 +154,6 @@ public Observable call(final Node.Me me) { return Observable.create(new Observable.OnSubscribe() { @Override public void call(Subscriber subscriber) { - // Close any existing session. - close(); - // Start new session. mInterface = new Builder() .setSession(SESSION_NAME) @@ -213,27 +221,46 @@ public void call(Throwable throwable) { @Override public void onDestroy() { + // Unregister from bus. mBus.unregister(this); - close(); - // TODO Purge stale pipes. } @Subscribe public void handleEvent(ApplicationEvents.StopCjdnsService event) { - stopSelf(); + close().subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1() { + @Override + public void call(Boolean aBoolean) { + stopSelf(); + } + }); + + // TODO Kill native process if VPN service is closed in some other way. } /** - * Close any existing session. + * Closes any existing session. */ - private void close() { - if (mInterface != null) { - try { - mInterface.close(); - } catch (IOException e) { - // Do nothing. + private Observable close() { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // Close VPN interface. + if (mInterface != null) { + try { + mInterface.close(); + } catch (IOException e) { + // Do nothing. + } + mInterface = null; + } + + // TODO Purge stale pipes. + + subscriber.onNext(Boolean.TRUE); + subscriber.onCompleted(); } - mInterface = null; - } + }); } } From fae62242b5923977f4545322f6d5874f04b590ef Mon Sep 17 00:00:00 2001 From: Emil Suleymanov Date: Tue, 5 Jul 2016 22:51:19 +0400 Subject: [PATCH 09/24] Automatically build cjdroute and some updates --- build.gradle | 56 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index dc9962f..406103c 100644 --- a/build.gradle +++ b/build.gradle @@ -8,25 +8,51 @@ allprojects { buildscript { repositories { jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'org.ajoberstar:gradle-git:0.2.3' } } +import org.ajoberstar.gradle.git.tasks.* + +task Clone(type: GitClone) { + def destination = file("cjdns") + uri = "https://github.com/cjdelisle/cjdns" + destinationPath = destination + bare = false + enabled = !destination.exists() //to clone only once +} + +task BuildCJDNS(type: Exec) { + workingDir file("cjdns") + commandLine file("cjdns/android_do") +} + +task Copy(type: Copy) { + from 'cjdns/build_android/out/*' + into 'src/main/assets/' +} + apply plugin: 'com.android.application' apply plugin: 'checkstyle' android { - buildToolsVersion '23.0.2' - compileSdkVersion 23 + buildToolsVersion '24' + compileSdkVersion 24 defaultConfig { minSdkVersion 9 - targetSdkVersion 23 + targetSdkVersion 24 versionCode 1 versionName "1.0.0-SNAPSHOT" } + sourceSets.main { + jni.srcDirs = [] + jniLibs.srcDir 'src/main/libs' + } signingConfigs { release } @@ -38,15 +64,25 @@ android { lintOptions { disable 'InvalidPackage' } + } +afterEvaluate { + android.applicationVariants.all { variant -> + variant.javaCompiler.dependsOn(Clone) + } +} + +Clone.finalizedBy(BuildCJDNS) +BuildCJDNS.finalizedBy(Copy) + dependencies { - compile 'com.android.support:support-v4:23.1.1' - compile 'com.android.support:appcompat-v7:23.1.1' - compile 'com.android.support:cardview-v7:23.1.1' - compile 'com.android.support:recyclerview-v7:23.1.1' - compile 'com.android.support:preference-v7:23.1.1' - compile 'com.android.support:preference-v14:23.1.1' + compile 'com.android.support:support-v4:24.0.0' + compile 'com.android.support:appcompat-v7:24.0.0' + compile 'com.android.support:cardview-v7:24.0.0' + compile 'com.android.support:recyclerview-v7:24.0.0' + compile 'com.android.support:preference-v7:24.0.0' + compile 'com.android.support:preference-v14:24.0.0' compile 'com.jakewharton:butterknife:6.0.0' compile 'com.joanzapata.android:android-iconify:1.0.9' compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') { From 288de07ceb3a200b42dfda7a4010f49cd81f3874 Mon Sep 17 00:00:00 2001 From: Emil Suleymanov Date: Tue, 5 Jul 2016 23:23:56 +0400 Subject: [PATCH 10/24] Update gradle --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 42cf951..4de0289 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip From c4718c6fad021d30a93f0d8b7f231d0bacc166b4 Mon Sep 17 00:00:00 2001 From: Emil Suleymanov Date: Tue, 5 Jul 2016 23:32:11 +0400 Subject: [PATCH 11/24] Update API in travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dea662c..8f7bde4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,10 @@ android: - tools # The BuildTools version used by your project - - build-tools-23.0.2 + - build-tools-24 # The SDK version used to compile your project - - android-23 + - android-24 - extra-google-google_play_services - extra-google-m2repository From 1fb93d8b371ca56cfc68a6fa26955a8d6b13f770 Mon Sep 17 00:00:00 2001 From: Emil Suleymanov Date: Tue, 5 Jul 2016 23:52:10 +0400 Subject: [PATCH 12/24] Oups --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 406103c..b0142eb 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ task BuildCJDNS(type: Exec) { commandLine file("cjdns/android_do") } -task Copy(type: Copy) { +task CopyT(type: Copy) { from 'cjdns/build_android/out/*' into 'src/main/assets/' } @@ -74,7 +74,7 @@ afterEvaluate { } Clone.finalizedBy(BuildCJDNS) -BuildCJDNS.finalizedBy(Copy) +BuildCJDNS.finalizedBy(CopyT) dependencies { compile 'com.android.support:support-v4:24.0.0' From acfdd7e0042e716700c4328671616a703008b065 Mon Sep 17 00:00:00 2001 From: Emil Suleymanov Date: Wed, 6 Jul 2016 00:08:17 +0400 Subject: [PATCH 13/24] Update readme n travis --- .travis.yml | 2 +- README.md | 22 +++++----------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f7bde4..9f558db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ android: - tools # The BuildTools version used by your project - - build-tools-24 + - build-tools-24.0.0 # The SDK version used to compile your project - android-24 diff --git a/README.md b/README.md index 694104e..c55b9ab 100644 --- a/README.md +++ b/README.md @@ -9,28 +9,16 @@ Meshnet is an Android app that lets you connect to cjdns networks, without the n Installation ------------ - -1. Install the [Android SDK](http://developer.android.com/sdk/index.html) -2. Clone this application repo -3. Clone [cjdns](https://github.com/hyperboria/cjdns) and build native binaries: - - ``` - ./android_do - ``` - -4. Copy built artifacts from **./build_android/** into the application repo such that corresponding **cjdroute** binaries are located as such: - - ``` - ./src/main/assets/armeabi-v7a/cjdroute - ./src/main/assets/x86/cjdroute - ``` - -5. Build application and install on device: + +1. Clone this application repo +2. Build application and install on device: ``` ./gradlew installDebug ``` +3. (Optional) Install the [Android SDK](http://developer.android.com/sdk/index.html) and open project using it + Contact ------- From 1ba4d57a090884d2895ead8d13225c9913473b59 Mon Sep 17 00:00:00 2001 From: Emil Suleymanov Date: Wed, 6 Jul 2016 00:25:20 +0400 Subject: [PATCH 14/24] Hide cjdns build output unless an error is faced --- build.gradle | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/build.gradle b/build.gradle index b0142eb..60ae22b 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,16 @@ task Clone(type: GitClone) { task BuildCJDNS(type: Exec) { workingDir file("cjdns") commandLine file("cjdns/android_do") + + standardOutput = new ByteArrayOutputStream() + errorOutput = standardOutput + ignoreExitValue = true + doLast { + if (execResult.exitValue != 0) { + println(standardOutput.toString()) + throw new GradleException("Exec failed! See output above.") + } + } } task CopyT(type: Copy) { From 221717f29efd0120d7d5e139c29344962d87b7f7 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Mon, 10 Oct 2016 01:38:57 -0400 Subject: [PATCH 15/24] Merge sssemil/patch-2 with hyperboria/develop --- .gitignore | 11 +- install_debug | 4 + install_release | 4 + src/main/AndroidManifest.xml | 3 + .../java/berlin/meshnet/cjdns/AdminApi.java | 731 ++++++++++++++++-- .../meshnet/cjdns/CjdnsApplication.java | 17 +- .../berlin/meshnet/cjdns/CjdnsService.java | 7 + .../berlin/meshnet/cjdns/CjdnsVpnService.java | 266 +++++++ .../java/berlin/meshnet/cjdns/Cjdroute.java | 440 +++++++---- .../berlin/meshnet/cjdns/CjdrouteConf.java | 261 +++++-- .../meshnet/cjdns/FileDescriptorSender.java | 54 ++ .../berlin/meshnet/cjdns/MainActivity.java | 116 ++- .../dialog/ConnectionsDialogFragment.java | 12 +- .../java/berlin/meshnet/cjdns/model/Node.java | 16 +- .../cjdns/page/CredentialsPageFragment.java | 20 + .../meshnet/cjdns/page/MePageFragment.java | 36 +- .../meshnet/cjdns/page/PeersPageFragment.java | 20 + .../cjdns/page/SettingsPageFragment.java | 24 + .../meshnet/cjdns/producer/MeProducer.java | 27 +- .../meshnet/cjdns/producer/PeersProducer.java | 90 ++- src/main/jni/Android.mk | 14 + src/main/jni/Application.mk | 12 + src/main/jni/sendfd.cpp | 80 ++ src/main/jni/sendfd.h | 11 + src/main/res/values/strings.xml | 3 + src/main/res/xml/preferences.xml | 44 +- 26 files changed, 1964 insertions(+), 359 deletions(-) create mode 100755 install_debug create mode 100755 install_release create mode 100644 src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java create mode 100644 src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java create mode 100644 src/main/jni/Android.mk create mode 100644 src/main/jni/Application.mk create mode 100644 src/main/jni/sendfd.cpp create mode 100644 src/main/jni/sendfd.h diff --git a/.gitignore b/.gitignore index a129b62..2c38e94 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,13 @@ build/ local.properties *.iml *.class -src/main/assets/x86/ +src/main/libs/ +src/main/obj/ +src/main/assets/armeabi/ src/main/assets/armeabi-v7a/ -src/main/assets/cjdroute.conf +src/main/assets/arm64-v8a/ +src/main/assets/x86/ +src/main/assets/x86_64/ +src/main/assets/mips/ +src/main/assets/mips64/ +src/main/assets/all/ \ No newline at end of file diff --git a/install_debug b/install_debug new file mode 100755 index 0000000..b23237d --- /dev/null +++ b/install_debug @@ -0,0 +1,4 @@ +#!/bin/sh + +ndk-build NDK_DEBUG=true NDK_PROJECT_PATH=src/main/ +./gradlew installDebug diff --git a/install_release b/install_release new file mode 100755 index 0000000..d297eec --- /dev/null +++ b/install_release @@ -0,0 +1,4 @@ +#!/bin/sh + +ndk-build NDK_DEBUG=false NDK_PROJECT_PATH=src/main/ +./gradlew installRelease diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index b653535..e1d5afc 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ + diff --git a/src/main/java/berlin/meshnet/cjdns/AdminApi.java b/src/main/java/berlin/meshnet/cjdns/AdminApi.java index 0160122..f6bda3f 100644 --- a/src/main/java/berlin/meshnet/cjdns/AdminApi.java +++ b/src/main/java/berlin/meshnet/cjdns/AdminApi.java @@ -3,8 +3,6 @@ import android.util.Log; import org.bitlet.wetorrent.bencode.Bencode; -import org.json.JSONException; -import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -13,94 +11,725 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import berlin.meshnet.cjdns.model.Node; +import rx.Observable; +import rx.Subscriber; +/** + * API for administration of the cjdns node. + */ public class AdminApi { - public static final int TIMEOUT = 5000; - public static final int DGRAM_LENGTH = 4096; - private InetAddress address; - private int port; - private byte[] password; + private static final String TAG = AdminApi.class.getSimpleName(); - static AdminApi from(JSONObject cjdrouteConf) throws IOException, JSONException { - JSONObject admin = cjdrouteConf.getJSONObject("admin"); - String[] bind = admin.getString("bind").split(":"); + /** + * Name of this class. + */ + private static final String CLASS_NAME = AdminApi.class.getSimpleName(); - InetAddress address = InetAddress.getByName(bind[0]); - int port = Integer.parseInt(bind[1]); - byte[] password = admin.getString("password").getBytes(); + /** + * UDP datagram socket timeout in milliseconds. + */ + private static final int SOCKET_TIMEOUT = 30000; - return new AdminApi(address, port, password); + /** + * UDP datagram length. + */ + private static final int DATAGRAM_LENGTH = 4096; + + /** + * Array used for HEX encoding. + */ + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + + /** + * Request to {@link AdminApi} for authentication cookie. + */ + private static final HashMap REQUEST_COOKIE = new LinkedHashMap() {{ + put(wrapString("q"), wrapString("cookie")); + }}; + + /** + * The local IP address to bind the admin RPC server. + */ + private static final String ADMIN_API_ADDRESS = "127.0.0.1"; + + /** + * The port to bind the admin RPC server. + */ + private static final int ADMIN_API_PORT = 11234; + + /** + * The password for authenticated requests. + */ + private static final byte[] ADMIN_API_PASSWORD = "NONE".getBytes(); + + /** + * The local IP address to bind the admin RPC server, as an {@link InetAddress}. + */ + private final InetAddress mAdminApiAddress; + + /** + * Constructor. + */ + public AdminApi() throws UnknownHostException { + mAdminApiAddress = InetAddress.getByName(ADMIN_API_ADDRESS); } - private AdminApi(InetAddress address, int port, byte[] password) { - this.address = address; - this.port = port; - this.password = password; + public static class AdminLog { + + public static Observable logMany(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_logMany is not implemented in " + CLASS_NAME); + } + + public static Observable subscribe(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_subscribe is not implemented in " + CLASS_NAME); + } + + public static Observable subscriptions(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_subscriptions is not implemented in " + CLASS_NAME); + } + + public static Observable unsubscribe(final AdminApi api) { + throw new UnsupportedOperationException("AdminLog_unsubscribe is not implemented in " + CLASS_NAME); + } } - public String getBind() { - return this.address.getHostAddress() + ":" + this.port; + public static class Admin { + + public static Observable asyncEnabled(final AdminApi api) { + throw new UnsupportedOperationException("Admin_asyncEnabled is not implemented in " + CLASS_NAME); + } + + public static Observable availableFunctions(final AdminApi api) { + throw new UnsupportedOperationException("Admin_availableFunctions is not implemented in " + CLASS_NAME); + } } - public int corePid() throws IOException { - // try { - HashMap request = new HashMap<>(); - request.put(ByteBuffer.wrap("q".getBytes()), ByteBuffer.wrap("Core_pid".getBytes())); + public static class Allocator { - Map response = perform(request); - Long pid = (Long) response.get(ByteBuffer.wrap("pid".getBytes())); + public static Observable bytesAllocated(final AdminApi api) { + throw new UnsupportedOperationException("Allocator_bytesAllocated is not implemented in " + CLASS_NAME); + } - return pid.intValue(); - // } catch (IOException e) { - // return 0; - // } + public static Observable snapshot(final AdminApi api) { + throw new UnsupportedOperationException("Allocator_snapshot is not implemented in " + CLASS_NAME); + } } - public Node NodeStore_nodeForAddr() throws IOException { - return new Node.Peer(0, "Some Peer Node", "foo.k", null); + public static class AuthorizedPasswords { + + public static Observable add(final AdminApi api) { + throw new UnsupportedOperationException("AuthorizedPasswords_add is not implemented in " + CLASS_NAME); + } + + public static Observable list(final AdminApi api) { + throw new UnsupportedOperationException("AuthorizedPasswords_list is not implemented in " + CLASS_NAME); + } + + public static Observable remove(final AdminApi api) { + throw new UnsupportedOperationException("AuthorizedPasswords_remove is not implemented in " + CLASS_NAME); + } } - public Map perform(Map request) throws IOException { - DatagramSocket socket = newSocket(); + public static class Core { - byte[] data = serialize(request); - DatagramPacket dgram = new DatagramPacket(data, data.length, this.address, this.port); - socket.send(dgram); + public static Observable exit(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("Core_exit")) { + @Override + protected Boolean parseResult(final Map response) { + return Boolean.TRUE; + } + }); + } - DatagramPacket responseDgram = new DatagramPacket(new byte[DGRAM_LENGTH], DGRAM_LENGTH); - socket.receive(responseDgram); - socket.close(); + public static Observable initTunfd(final AdminApi api, final Long tunfd, final Long type) { + return Observable.create(new BaseOnSubscribe(api, new Request("Core_initTunfd", + new LinkedHashMap() {{ + put(wrapString("tunfd"), tunfd); + put(wrapString("type"), type); + }})) { + @Override + protected Boolean parseResult(final Map response) { + return Boolean.TRUE; + } + }); + } + + public static Observable initTunnel(final AdminApi api) { + throw new UnsupportedOperationException("Core_initTunnel is not implemented in " + CLASS_NAME); + } + + public static Observable pid(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("Core_pid")) { + @Override + protected Long parseResult(final Map response) { + return (Long) response.get(wrapString("pid")); + } + }); + } + } + + public static class EthInterface { + + public static Observable beacon(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_beacon is not implemented in " + CLASS_NAME); + } + + public static Observable beginConnection(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_beginConnection is not implemented in " + CLASS_NAME); + } + + public static Observable listDevices(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_listDevices is not implemented in " + CLASS_NAME); + } + + public static Observable new0(final AdminApi api) { + throw new UnsupportedOperationException("ETHInterface_new is not implemented in " + CLASS_NAME); + } + } + + public static class FileNo { + + public static Observable> import0(final AdminApi api, final String path) { + return Observable.create(new BaseOnSubscribe>(api, new Request("FileNo_import", + new LinkedHashMap() {{ + put(wrapString("path"), wrapString(path)); + put(wrapString("type"), wrapString("android")); + }})) { + @Override + protected Map parseResult(final Map response) { + return new HashMap() {{ + put("tunfd", (Long) response.get(wrapString("tunfd"))); + put("type", (Long) response.get(wrapString("type"))); + }}; + } + }); + } + } + + public static class InterfaceController { + + public static Observable disconnectPeer(final AdminApi api) { + throw new UnsupportedOperationException("InterfaceController_disconnectPeer is not implemented in " + CLASS_NAME); + } + + public static Observable> peerStats(final AdminApi api) { + return Observable.create(new BaseOnSubscribe>(api, new Request("InterfaceController_peerStats", + new LinkedHashMap() {{ + // TODO Handle paging. + put(wrapString("page"), Long.valueOf(0L)); + }})) { + @Override + protected List parseResult(final Map response) { + List peers = new LinkedList<>(); + List peerStats = (List) response.get(wrapString("peers")); + for (Object entry : peerStats) { + Object user = ((Map) entry).get(wrapString("user")); + Object addr = ((Map) entry).get(wrapString("addr")); + Object publicKey = ((Map) entry).get(wrapString("publicKey")); + peers.add(new Node.Peer(peers.size(), + user != null ? new String(((ByteBuffer) user).array()) : null, + user != null ? new String(((ByteBuffer) addr).array()) : null, + user != null ? new String(((ByteBuffer) publicKey).array()) : null, + null + )); + } + return peers; + } + }); + } + + public static Observable resetPeering(final AdminApi api) { + throw new UnsupportedOperationException("InterfaceController_resetPeering is not implemented in " + CLASS_NAME); + } + } + + public static class IpTunnel { + + public static Observable allowConnection(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_allowConnection is not implemented in " + CLASS_NAME); + } + + public static Observable connectTo(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_connectTo is not implemented in " + CLASS_NAME); + } + + public static Observable listConnections(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_listConnections is not implemented in " + CLASS_NAME); + } - Map response = parse(responseDgram.getData()); - Log.i("cjdns_AdminAPI", "response: " + response.toString()); - return response; + public static Observable removeConnection(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_removeConnection is not implemented in " + CLASS_NAME); + } + + public static Observable showConnection(final AdminApi api) { + throw new UnsupportedOperationException("IpTunnel_showConnection is not implemented in " + CLASS_NAME); + } } - protected byte[] serialize(Map request) throws IOException { + public static class Janitor { + + public static Observable dumpRumorMill(final AdminApi api) { + throw new UnsupportedOperationException("Janitor_dumpRumorMill is not implemented in " + CLASS_NAME); + } + } + + public static class NodeStore { + + public static Observable dumpTable(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_dumpTable is not implemented in " + CLASS_NAME); + } + + public static Observable getLink(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_getLink is not implemented in " + CLASS_NAME); + } + + public static Observable getRouteLabel(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_getRouteLabel is not implemented in " + CLASS_NAME); + } + + public static Observable nodeForAddr(final AdminApi api) { + throw new UnsupportedOperationException("NodeStore_nodeForAddr is not implemented in " + CLASS_NAME); + } + } + + public static class RouteGen { + + public static Observable addException(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_addException is not implemented in " + CLASS_NAME); + } + + public static Observable addLocalPrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_addLocalPrefix is not implemented in " + CLASS_NAME); + } + + public static Observable addPrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_addPrefix is not implemented in " + CLASS_NAME); + } + + public static Observable commit(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_commit is not implemented in " + CLASS_NAME); + } + + public static Observable getExceptions(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getExceptions is not implemented in " + CLASS_NAME); + } + + public static Observable getGeneratedRoutes(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getGeneratedRoutes is not implemented in " + CLASS_NAME); + } + + public static Observable getLocalPrefixes(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getLocalPrefixes is not implemented in " + CLASS_NAME); + } + + public static Observable getPrefixes(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_getPrefixes is not implemented in " + CLASS_NAME); + } + + public static Observable removeException(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_removeException is not implemented in " + CLASS_NAME); + } + + public static Observable removeLocalPrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_removeLocalPrefix is not implemented in " + CLASS_NAME); + } + + public static Observable removePrefix(final AdminApi api) { + throw new UnsupportedOperationException("RouteGen_removePrefix is not implemented in " + CLASS_NAME); + } + } + + public static class RouterModule { + + public static Observable findNode(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_findNode is not implemented in " + CLASS_NAME); + } + + public static Observable getPeers(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_getPeers is not implemented in " + CLASS_NAME); + } + + public static Observable lookup(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_lookup is not implemented in " + CLASS_NAME); + } + + public static Observable nextHop(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_nextHop is not implemented in " + CLASS_NAME); + } + + public static Observable pingNode(final AdminApi api) { + throw new UnsupportedOperationException("RouterModule_pingNode is not implemented in " + CLASS_NAME); + } + } + + public static class SearchRunner { + + public static Observable search(final AdminApi api) { + throw new UnsupportedOperationException("SearchRunner_search is not implemented in " + CLASS_NAME); + } + + public static Observable showActiveSearch(final AdminApi api) { + throw new UnsupportedOperationException("SearchRunner_showActiveSearch is not implemented in " + CLASS_NAME); + } + } + + public static class Security { + + public static Observable checkPermissions(final AdminApi api) { + throw new UnsupportedOperationException("Security_checkPermissions is not implemented in " + CLASS_NAME); + } + + public static Observable chroot(final AdminApi api) { + throw new UnsupportedOperationException("Security_chroot is not implemented in " + CLASS_NAME); + } + + public static Observable getUser(final AdminApi api) { + throw new UnsupportedOperationException("Security_getUser is not implemented in " + CLASS_NAME); + } + + public static Observable nofiles(final AdminApi api) { + throw new UnsupportedOperationException("Security_nofiles is not implemented in " + CLASS_NAME); + } + + public static Observable noforks(final AdminApi api) { + throw new UnsupportedOperationException("Security_noforks is not implemented in " + CLASS_NAME); + } + + public static Observable seccomp(final AdminApi api) { + throw new UnsupportedOperationException("Security_seccomp is not implemented in " + CLASS_NAME); + } + + public static Observable setUser(final AdminApi api) { + throw new UnsupportedOperationException("Security_setUser is not implemented in " + CLASS_NAME); + } + + public static Observable setupComplete(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("Security_setupComplete", null)) { + @Override + protected Boolean parseResult(final Map response) { + return Boolean.TRUE; + } + }); + } + } + + public static class SessionManager { + + public static Observable getHandles(final AdminApi api) { + throw new UnsupportedOperationException("SessionManager_getHandles is not implemented in " + CLASS_NAME); + } + + public static Observable sessionStats(final AdminApi api) { + throw new UnsupportedOperationException("SessionManager_sessionStats is not implemented in " + CLASS_NAME); + } + } + + public static class SwitchPinger { + + public static Observable ping(final AdminApi api) { + throw new UnsupportedOperationException("SwitchPinger_ping is not implemented in " + CLASS_NAME); + } + } + + public static class UdpInterface { + + public static Observable new0(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("UDPInterface_new", + new LinkedHashMap() {{ + put(wrapString("bindAddress"), wrapString("0.0.0.0:0")); + }})) { + @Override + protected Long parseResult(Map response) { + return (Long) response.get(wrapString("interfaceNumber")); + } + }); + } + + public static Observable beginConnection(final AdminApi api, final String publicKey, final String address, + final Long interfaceNumber, final String login, final String password) { + return Observable.create(new BaseOnSubscribe(api, new Request("UDPInterface_beginConnection", + new LinkedHashMap() {{ + put(wrapString("publicKey"), wrapString(publicKey)); + put(wrapString("address"), wrapString(address)); + put(wrapString("interfaceNumber"), interfaceNumber); + if (login != null) { + put(wrapString("login"), wrapString(login)); + } + put(wrapString("password"), wrapString(password)); + }})) { + @Override + protected Boolean parseResult(Map response) { + return Boolean.TRUE; + } + }); + } + } + + public static Observable memory(final AdminApi api) { + throw new UnsupportedOperationException("memory is not implemented in " + CLASS_NAME); + } + + public static Observable ping(final AdminApi api) { + return Observable.create(new BaseOnSubscribe(api, new Request("ping")) { + @Override + protected Boolean parseResult(Map response) { + return Boolean.TRUE; + } + }); + } + + /** + * Create a new UDP datagram socket. + * + * @return The socket. + * @throws SocketException Thrown if failed to create or bind. + */ + private static DatagramSocket newSocket() throws SocketException { + DatagramSocket socket = new DatagramSocket(); + socket.setSoTimeout(SOCKET_TIMEOUT); + return socket; + } + + /** + * Serializes request into bencoded byte array. + * + * @param request The request as a map. + * @return The bencoded byte array. + * @throws IOException + */ + private static byte[] serialize(Map request) throws IOException { Bencode serializer = new Bencode(); ByteArrayOutputStream output = new ByteArrayOutputStream(); serializer.setRootElement(request); serializer.print(output); - return output.toByteArray(); } - protected Map parse(byte[] data) throws IOException { + /** + * Parses response from a bencoded byte array. + * + * @param data The bencoded data. + * @return The response as a map. + * @throws IOException + */ + private static Map parse(byte[] data) throws IOException { StringReader input = new StringReader(new String(data)); Bencode parser = new Bencode(input); - return (Map) parser.getRootElement(); } - protected DatagramSocket newSocket() throws SocketException { - DatagramSocket socket = new DatagramSocket(); - socket.setSoTimeout(TIMEOUT); - return socket; + /** + * Converts bytes to a HEX encoded string. + * + * @param bytes The byte array. + * @return The HEX encoded string. + */ + private static String bytesToHex(byte[] bytes) { + String hexString = null; + if (bytes != null && bytes.length > 0) { + final char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[v >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + hexString = new String(hexChars); + } + return hexString; + } + + /** + * Wraps a string into a {@link ByteBuffer}. + * + * @param value The string. + * @return The wrapped {@link ByteBuffer}. + */ + private static ByteBuffer wrapString(String value) { + return ByteBuffer.wrap(value.getBytes()); + } + + /** + * Sends an authenticated request to the {@link AdminApi}. + * + * @param request The request. + * @param api The {@link AdminApi}. + * @return The response as a map. + * @throws NoSuchAlgorithmException Thrown if SHA-256 is missing. + * @throws IOException Thrown if request failed. + */ + private static Map sendAuthenticatedRequest(Request request, AdminApi api) throws NoSuchAlgorithmException, IOException { + Log.i(TAG, request.name + " sent"); + + // Get authentication session cookie. + String cookie = getCookie(api); + + // Generate dummy hash. + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(ADMIN_API_PASSWORD); + digest.update(cookie.getBytes()); + String dummyHash = bytesToHex(digest.digest()); + + // Assemble unsigned request. + HashMap authenticatedRequest = new LinkedHashMap<>(); + authenticatedRequest.put(wrapString("q"), wrapString("auth")); + authenticatedRequest.put(wrapString("aq"), wrapString(request.name)); + if (request.args != null) { + authenticatedRequest.put(wrapString("args"), request.args); + } + authenticatedRequest.put(wrapString("hash"), wrapString(dummyHash)); + authenticatedRequest.put(wrapString("cookie"), wrapString(cookie)); + + // Sign request. + byte[] requestBytes = serialize(authenticatedRequest); + digest.reset(); + digest.update(requestBytes); + String hash = bytesToHex(digest.digest()); + authenticatedRequest.put(wrapString("hash"), wrapString(hash)); + + // Send request. + return send(authenticatedRequest, api); + } + + /** + * Gets an authentication cookie from the {@link AdminApi}. + * + * @param api The {@link AdminApi}. + * @return The cookie. + * @throws IOException Thrown if request failed. + */ + private static String getCookie(AdminApi api) throws IOException { + Map response = send(REQUEST_COOKIE, api); + Object cookie = response.get(wrapString("cookie")); + if (cookie instanceof ByteBuffer) { + return new String(((ByteBuffer) cookie).array()); + } else { + throw new IOException("Unable to fetch authentication cookie"); + } + } + + /** + * Sends a request to the {@link AdminApi}. + * + * @param request The request. + * @param api The {@link AdminApi}. + * @return The response as a map. + * @throws IOException Thrown if request failed. + */ + private static Map send(Map request, AdminApi api) throws IOException { + DatagramSocket socket = newSocket(); + + byte[] data = serialize(request); + DatagramPacket dgram = new DatagramPacket(data, data.length, api.mAdminApiAddress, ADMIN_API_PORT); + socket.send(dgram); + + DatagramPacket responseDgram = new DatagramPacket(new byte[DATAGRAM_LENGTH], DATAGRAM_LENGTH); + socket.receive(responseDgram); + socket.close(); + + byte[] resData = responseDgram.getData(); + int i = resData.length - 1; + while (resData[i] == 0) { + --i; + } + byte[] resDataClean = Arrays.copyOf(resData, i + 1); + return parse(resDataClean); + } + + /** + * Model object encapsulating the name and arguments of a request. + */ + private static class Request { + + private final String name; + + private final LinkedHashMap args; + + private Request(String name, LinkedHashMap args) { + this.name = name; + this.args = args; + } + + private Request(String name) { + this(name, null); + } + } + + /** + * Abstract class that implements the basic {@link rx.Observable.OnSubscribe} behaviour of each API. + * + * @param The return type of the API response. + */ + private static abstract class BaseOnSubscribe implements Observable.OnSubscribe { + + private static final ByteBuffer ERROR_KEY = wrapString("error"); + + private static final String ERROR_NONE = "none"; + + private AdminApi mApi; + + private Request mRequest; + + private BaseOnSubscribe(AdminApi api, Request request) { + mApi = api; + mRequest = request; + } + + @Override + public void call(Subscriber subscriber) { + try { + final Map response = AdminApi.sendAuthenticatedRequest(mRequest, mApi); + + // Check for error in response. + final Object error = response.get(ERROR_KEY); + if (error instanceof ByteBuffer) { + String errorString = new String(((ByteBuffer) error).array()); + if (!ERROR_NONE.equals(errorString)) { + Log.e(TAG, mRequest.name + " failed: " + errorString); + subscriber.onError(new IOException(mRequest.name + " failed: " + errorString)); + return; + } + } + + // Parse response for result. + final T result = parseResult(response); + if (result != null) { + Log.e(TAG, mRequest.name + " completed"); + subscriber.onNext(result); + subscriber.onCompleted(); + } else { + Log.e(TAG, "Failed to parse result from " + mRequest.name); + subscriber.onError(new IOException("Failed to parse result from " + mRequest.name)); + } + } catch (SocketTimeoutException e) { + Log.e(TAG, mRequest.name + " timed out"); + subscriber.onError(e); + } catch (NoSuchAlgorithmException | IOException e) { + Log.e(TAG, "Unexpected failure from " + mRequest.name, e); + subscriber.onError(e); + } + } + + /** + * Implementation must specify how to parse the response and return a value of type {@link T}. + * Returning {@code null} will lead to {@link Subscriber#onError(Throwable)} being called. + * + * @param response The response from the API as a {@link Map}. + * @return A value of type {@link T}. + */ + protected abstract T parseResult(final Map response); } } diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java index 808c5ec..194313b 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java @@ -8,6 +8,8 @@ import com.squareup.otto.Bus; +import java.net.UnknownHostException; + import javax.inject.Singleton; import berlin.meshnet.cjdns.dialog.ConnectionsDialogFragment; @@ -53,6 +55,7 @@ public void inject(Object object) { @Module( injects = { MainActivity.class, + CjdnsVpnService.class, CjdnsService.class, MePageFragment.class, PeersPageFragment.class, @@ -91,10 +94,7 @@ public Bus provideBus() { @Singleton @Provides public Cjdroute provideCjdroute(Context context) { - // TODO Change this conditional to (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) when VpnService is implemented. - // TODO Use Lollipop for now to allow any API level below to connect with tun device. - // TODO Unable to run cjdroute as root since Lollipop, so there is no point trying. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return new Cjdroute.Compat(context.getApplicationContext()); } return new Cjdroute.Default(context.getApplicationContext()); @@ -107,12 +107,17 @@ public SettingsProducer provideSettingsProducer(Context context, SharedPreferenc @Provides public MeProducer provideMeProducer() { - return new MeProducer.Mock(); + return new MeProducer.Default(); } @Provides public PeersProducer providePeerListProducer() { - return new PeersProducer.Mock(); + try { + return new PeersProducer.Default(); + } catch (UnknownHostException e) { + // TODO + } + return null; } @Provides diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java index 52ada11..969dc3d 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java @@ -23,6 +23,8 @@ import rx.schedulers.Schedulers; /** + * TODO Only needed for compat. + *

* Service for managing cjdroute. */ public class CjdnsService extends Service { @@ -72,6 +74,11 @@ public void onCreate() { public void call(JSONObject cjdrouteConf) { startForeground(NOTIFICATION_ID, buildNotification(cjdrouteConf)); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed in subscribe", throwable); + } })); // Execute cjdroute. diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java new file mode 100644 index 0000000..26a8ec2 --- /dev/null +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java @@ -0,0 +1,266 @@ +package berlin.meshnet.cjdns; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.net.VpnService; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.squareup.otto.Bus; +import com.squareup.otto.Subscribe; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import berlin.meshnet.cjdns.event.ApplicationEvents; +import berlin.meshnet.cjdns.model.Node; +import rx.Observable; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.schedulers.Schedulers; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class CjdnsVpnService extends VpnService { + + private static final String TAG = CjdnsVpnService.class.getSimpleName(); + + /** + * The VPN session name. + */ + private static final String SESSION_NAME = "VPN over cjdns"; + + /** + * The maximum transmission unit for the VPN interface. + */ + private static final int MTU = 1304; + + /** + * Route for cjdns addresses. + */ + private static final String CJDNS_ROUTE = "fc00::"; + + /** + * Default route for the VPN interface. A default route is needed for some applications to work. + */ + private static final String DEFAULT_ROUTE = "::"; + + /** + * DNS server for the VPN connection. We must set a DNS server for Lollipop devices to work. + */ + private static final String DNS_SERVER = "8.8.8.8"; + + /** + * Path to a transient named pipe for sending a message from the Java process to the native + * process. The VPN interface file descriptor is translated by the kernel across this named pipe. + */ + private static final String SEND_FD_PIPE_PATH_TEMPLATE = "%1$s/pipe_%2$s"; + + /** + * VPN interface. + */ + private ParcelFileDescriptor mInterface; + + @Inject + Bus mBus; + + @Inject + Cjdroute mCjdroute; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // Inject dependencies. + ((CjdnsApplication) getApplication()).inject(this); + + // Register so we can subscribe to stop events. + mBus.register(this); + + // Start cjdns process and VPN. + final String pipePath = String.format(Locale.ENGLISH, SEND_FD_PIPE_PATH_TEMPLATE, + getFilesDir().getPath(), UUID.randomUUID()); + try { + final AdminApi api = new AdminApi(); + // TODO Move UDP interface adding to separate place. + CjdrouteConf.fetch0(this) + .flatMap(new Func1>() { + @Override + public Observable call(Node.Me me) { + return mCjdroute.start(); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return AdminApi.Security.setupComplete(api); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return close(); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return AdminApi.UdpInterface.new0(api); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Long udpInterfaceNumber) { + return AdminApi.UdpInterface.beginConnection(api, + "1941p5k8qqvj17vjrkb9z97wscvtgc1vp8pv1huk5120cu42ytt0.k", + "104.200.29.163:53053", + udpInterfaceNumber, + null, + "8fVMl0oo6QI6wKeMneuY26x1MCgRemg"); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return AdminApi.UdpInterface.new0(api); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Long udpInterfaceNumber) { + return AdminApi.UdpInterface.beginConnection(api, + "2scyvybg4qqms1c5c9nyt50b1cdscxnr6ycpwsxf6pccbmwuynk0.k", + "159.203.5.91:30664", + udpInterfaceNumber, + "android-public", + "kj1rur4buavtyp2mavch5nghsnd4bpf"); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean isSuccessful) { + return CjdrouteConf.fetch0(CjdnsVpnService.this); + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(final Node.Me me) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // Start new session. + mInterface = new Builder() + .setSession(SESSION_NAME) + .setMtu(MTU) + .addAddress(me.address, 8) + .addRoute(CJDNS_ROUTE, 8) + .addRoute(DEFAULT_ROUTE, 0) + .addDnsServer(DNS_SERVER) + .establish(); + subscriber.onNext(mInterface.getFd()); + subscriber.onCompleted(); + } + }); + } + }) + .flatMap(new Func1>>() { + @Override + public Observable> call(Integer fd) { + // Send VPN interface file descriptor through the pipe after + // AdminApi.FileNo.import0() constructs that pipe. + FileDescriptorSender.send(pipePath, fd) + .delaySubscription(3L, TimeUnit.SECONDS) + .subscribe(new Action1() { + @Override + public void call(Boolean isSuccessful) { + Log.i(TAG, "VPN interface file descriptor imported to native process"); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to import VPN interface file descriptor to native process", throwable); + } + }); + + // Start named pipe to import VPN interface file descriptor. + return AdminApi.FileNo.import0(api, pipePath); + } + }) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(Map file) { + return AdminApi.Core.initTunfd(api, file.get("tunfd"), file.get("type")); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + new Action1() { + @Override + public void call(Boolean isSuccessful) { + Log.i(TAG, "TUN interface initialized"); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to initialize TUN interface", throwable); + } + }); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } + + return START_STICKY; + } + + @Override + public void onDestroy() { + // Unregister from bus. + mBus.unregister(this); + } + + @Subscribe + public void handleEvent(ApplicationEvents.StopCjdnsService event) { + close().subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1() { + @Override + public void call(Boolean aBoolean) { + stopSelf(); + } + }); + + // TODO Kill native process if VPN service is closed in some other way. + } + + /** + * Closes any existing session. + */ + private Observable close() { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // Close VPN interface. + if (mInterface != null) { + try { + mInterface.close(); + } catch (IOException e) { + // Do nothing. + } + mInterface = null; + } + + // TODO Purge stale pipes. + + subscriber.onNext(Boolean.TRUE); + subscriber.onCompleted(); + } + }); + } +} diff --git a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java index 1e4b25d..aea9814 100644 --- a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java +++ b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java @@ -4,18 +4,19 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; -import android.os.Process; -import android.preference.PreferenceManager; import android.util.Log; -import org.json.JSONException; import org.json.JSONObject; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.UnknownHostException; import java.util.Locale; +import java.util.UUID; +import berlin.meshnet.cjdns.model.Node; import berlin.meshnet.cjdns.util.InputStreamObservable; import rx.Observable; import rx.Subscriber; @@ -42,33 +43,29 @@ abstract class Cjdroute { /** * Value that represents an invalid PID. */ - private static final int INVALID_PID = Integer.MIN_VALUE; + static final long INVALID_PID = Long.MIN_VALUE; /** - * {@link Observable} for the PID of any currently running cjdroute process. If none is running, - * this {@link Observable} will complete without calling {@link Subscriber#onNext(Object)}. + * Checks if the node is running. * - * @param context The {@link Context}. - * @return The {@link Observable}. + * @return {@link Observable} that emits the PID if the node is running. */ - public static Observable running(Context context) { - final Context appContext = context.getApplicationContext(); - return Observable - .create(new Observable.OnSubscribe() { + public static Observable running() throws UnknownHostException { + final AdminApi api = new AdminApi(); + return AdminApi.ping(api) + .filter(new Func1() { @Override - public void call(Subscriber subscriber) { - int pid = PreferenceManager.getDefaultSharedPreferences(appContext) - .getInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); - subscriber.onNext(pid); - subscriber.onCompleted(); + public Boolean call(Boolean isSuccessful) { + return isSuccessful; } }) - .filter(new Func1() { + .flatMap(new Func1>() { @Override - public Boolean call(Integer pid) { - return pid != INVALID_PID; + public Observable call(Boolean isSuccessful) { + return AdminApi.Core.pid(api); } - }); + }) + .defaultIfEmpty(INVALID_PID); } /** @@ -83,7 +80,12 @@ public Boolean call(Integer pid) { * * @return The {@link Subscriber}. */ - abstract Subscriber terminate(); + abstract Subscriber terminate(); + + /** + * TODO + */ + abstract Observable start(); /** * Default implementation of {@link Cjdroute}. This relies on {@link android.net.VpnService} @@ -106,23 +108,35 @@ static class Default extends Cjdroute { @Override public Subscriber execute() { - // TODO Make this work. - throw new UnsupportedOperationException("Execution of cjdroute is not yet supported for your API level"); + throw new UnsupportedOperationException("Deprecated"); } @Override - public Subscriber terminate() { - return new Subscriber() { + public Subscriber terminate() { + return new Subscriber() { @Override - public void onNext(Integer pid) { + public void onNext(Long pid) { Log.i(TAG, "Terminating cjdroute with pid=" + pid); - Process.killProcess(pid); - // Erase PID. - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); - editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); - editor.apply(); + try { + AdminApi api = new AdminApi(); + AdminApi.Core.exit(api) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(new Action1() { + @Override + public void call(Boolean isSuccessful) { + Log.i(TAG, "cjdroute terminated"); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to terminate cjdroute", throwable); + } + }); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } } @Override @@ -136,8 +150,114 @@ public void onError(Throwable e) { } }; } + + @Override + public Observable start() { + return CjdrouteConf.fetch0(mContext) + .flatMap(new Func1>() { + @Override + public Observable call(Node.Me me) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + try { + final File filesDir = mContext.getFilesDir(); + final String pipe = UUID.randomUUID().toString(); + + // Start cjdroute. + Process process = new ProcessBuilder("./cjdroute", "core", filesDir.getPath(), pipe) + .directory(filesDir) + .redirectErrorStream(true) + .start(); + + // Subscribe to input stream. + final InputStream is = process.getInputStream(); + InputStreamObservable.line(is) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + new Action1() { + @Override + public void call(String line) { + Log.i(TAG, line); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to parse input stream", throwable); + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + }, + new Action0() { + @Override + public void call() { + Log.i(TAG, "Completed parsing of input stream"); + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + }); + + // TODO Replace this with directly passing params. + CjdrouteConf.fetch0(mContext) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(new Action1() { + @Override + public void call(Node.Me me) { + try { + Process initProcess = new ProcessBuilder("./cjdroute-init", + filesDir.getPath(), pipe, me.privateKey, "127.0.0.1:11234", "NONE") + .directory(filesDir) + .redirectErrorStream(true) + .start(); + } catch (IOException e) { + Log.e(TAG, "Failed to start cjdroute-init process", e); + } + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + Log.e(TAG, "Failed to start cjdroute-init process", throwable); + } + }); + + // TODO Check for when cjdroute is ready instead. + Thread.sleep(5000L); + + // Ping node to check if node is running. + AdminApi api = new AdminApi(); + Boolean isNodeRunning = AdminApi.ping(api).toBlocking().first(); + if (isNodeRunning != null && isNodeRunning) { + Log.i(TAG, "cjdroute started"); + subscriber.onNext(Boolean.TRUE); + subscriber.onCompleted(); + } else { + Log.i(TAG, "Failed to start cjdroute"); + subscriber.onError(new IOException("Failed to start cjdroute")); + } + } catch (IOException | InterruptedException e) { + Log.e(TAG, "Failed to start cjdroute", e); + subscriber.onError(e); + } + } + }); + } + }); + } } + /** * Compat implementation of {@link Cjdroute}. This allows cjdroute to create a TUN device and * requires super user permission. @@ -190,121 +310,124 @@ public Subscriber execute() { return new Subscriber() { @Override public void onNext(JSONObject cjdrouteConf) { - DataOutputStream os = null; - try { - java.lang.Process process = Runtime.getRuntime().exec(CMD_SUBSTITUTE_ROOT_USER); - - // Subscribe to input stream. - final InputStream is = process.getInputStream(); - InputStreamObservable.line(is) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - new Action1() { - @Override - public void call(String line) { - Log.i(TAG, line); - } - }, new Action1() { - @Override - public void call(Throwable throwable) { - Log.e(TAG, "Failed to parse input stream", throwable); - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }, - new Action0() { - @Override - public void call() { - Log.i(TAG, "Completed parsing of input stream"); - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }); - - // Subscribe to error stream. - final AdminApi adminApi = AdminApi.from(cjdrouteConf); - final String adminLine = String.format(Locale.ENGLISH, LINE_ADMIN_API, adminApi.getBind()); - final InputStream es = process.getErrorStream(); - InputStreamObservable.line(es) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - new Action1() { - @Override - public void call(String line) { - Log.i(TAG, line); - - // Find and store cjdroute PID. - if (line.contains(adminLine)) { - try { - int pid = adminApi.corePid(); - - // Store PID on disk to persist across java process crashes. - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); - editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, pid); - editor.apply(); - } catch (IOException e) { - Log.e(TAG, "Failed to get cjdroute PID", e); - } - } - } - }, new Action1() { - @Override - public void call(Throwable throwable) { - Log.e(TAG, "Failed to parse error stream", throwable); - if (es != null) { - try { - es.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }, - new Action0() { - @Override - public void call() { - Log.i(TAG, "Completed parsing of error stream"); - if (es != null) { - try { - es.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - }); - - // Execute cjdroute. - String filesDir = mContext.getFilesDir().getPath(); - os = new DataOutputStream(process.getOutputStream()); - os.writeBytes(String.format(CMD_EXECUTE_CJDROUTE, filesDir, filesDir)); - os.writeBytes(CMD_NEWLINE); - os.writeBytes(CMD_ADD_DEFAULT_ROUTE); - os.flush(); - } catch (IOException | JSONException e) { - Log.e(TAG, "Failed to execute cjdroute", e); - } finally { - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // Do nothing. - } - } - } + // TODO Fix Compat implementation. +// DataOutputStream os = null; +// try { +// java.lang.Process process = Runtime.getRuntime().exec(CMD_SUBSTITUTE_ROOT_USER); +// +// // Subscribe to input stream. +// final InputStream is = process.getInputStream(); +// InputStreamObservable.line(is) +// .subscribeOn(Schedulers.io()) +// .observeOn(Schedulers.io()) +// .subscribe( +// new Action1() { +// @Override +// public void call(String line) { +// Log.i(TAG, line); +// } +// }, new Action1() { +// @Override +// public void call(Throwable throwable) { +// Log.e(TAG, "Failed to parse input stream", throwable); +// if (is != null) { +// try { +// is.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }, +// new Action0() { +// @Override +// public void call() { +// Log.i(TAG, "Completed parsing of input stream"); +// if (is != null) { +// try { +// is.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }); +// +// // Subscribe to error stream. +// final AdminApi adminApi = AdminApi.from(cjdrouteConf); +// final String adminLine = String.format(Locale.ENGLISH, LINE_ADMIN_API, adminApi.getBind()); +// final InputStream es = process.getErrorStream(); +// InputStreamObservable.line(es) +// .subscribeOn(Schedulers.io()) +// .observeOn(Schedulers.io()) +// .subscribe( +// new Action1() { +// @Override +// public void call(String line) { +// Log.i(TAG, line); +// +// // Find and store cjdroute PID. +// // TODO Apply filter operator on the line. +// if (line.contains(adminLine)) { +// try { +// // TODO Apply corePid as operator. +// int pid = adminApi.corePid(); +// +// // Store PID on disk to persist across java process crashes. +// SharedPreferences.Editor editor = PreferenceManager +// .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); +// editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, pid); +// editor.apply(); +// } catch (IOException e) { +// Log.e(TAG, "Failed to get cjdroute PID", e); +// } +// } +// } +// }, new Action1() { +// @Override +// public void call(Throwable throwable) { +// Log.e(TAG, "Failed to parse error stream", throwable); +// if (es != null) { +// try { +// es.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }, +// new Action0() { +// @Override +// public void call() { +// Log.i(TAG, "Completed parsing of error stream"); +// if (es != null) { +// try { +// es.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } +// }); +// +// // Execute cjdroute. +// String filesDir = mContext.getFilesDir().getPath(); +// os = new DataOutputStream(process.getOutputStream()); +// os.writeBytes(String.format(Locale.ENGLISH, CMD_EXECUTE_CJDROUTE, filesDir, filesDir)); +// os.writeBytes(CMD_NEWLINE); +// os.writeBytes(CMD_ADD_DEFAULT_ROUTE); +// os.flush(); +// } catch (IOException | JSONException e) { +// Log.e(TAG, "Failed to execute cjdroute", e); +// } finally { +// if (os != null) { +// try { +// os.close(); +// } catch (IOException e) { +// // Do nothing. +// } +// } +// } } @Override @@ -320,10 +443,10 @@ public void onError(Throwable e) { } @Override - public Subscriber terminate() { - return new Subscriber() { + public Subscriber terminate() { + return new Subscriber() { @Override - public void onNext(Integer pid) { + public void onNext(Long pid) { Log.i(TAG, "Terminating cjdroute with pid=" + pid); // Kill cjdroute as root. @@ -334,11 +457,11 @@ public void onNext(Integer pid) { os.writeBytes(String.format(Locale.ENGLISH, CMD_KILL_PROCESS, pid)); os.flush(); - // Erase PID. - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); - editor.putInt(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); - editor.apply(); + // Erase PID. TODO Change implementation. +// SharedPreferences.Editor editor = PreferenceManager +// .getDefaultSharedPreferences(mContext.getApplicationContext()).edit(); +// editor.putLong(SHARED_PREFERENCES_KEY_CJDROUTE_PID, INVALID_PID); +// editor.apply(); } catch (IOException e) { Log.e(TAG, "Failed to terminate cjdroute", e); } finally { @@ -363,5 +486,10 @@ public void onError(Throwable e) { } }; } + + @Override + Observable start() { + return null; + } } } diff --git a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java index d31f152..2e98cc1 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdrouteConf.java @@ -2,7 +2,10 @@ import android.annotation.TargetApi; import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; +import android.preference.PreferenceManager; +import android.text.TextUtils; import org.json.JSONException; import org.json.JSONObject; @@ -18,13 +21,14 @@ import java.io.OutputStream; import java.util.Locale; +import berlin.meshnet.cjdns.model.Node; import rx.Observable; import rx.Subscriber; /** * Configurations for cjdroute. */ -public class CjdrouteConf { +public abstract class CjdrouteConf { /** * The filename for the cjdroute configurations. @@ -51,6 +55,20 @@ public class CjdrouteConf { */ private static final Object sLock = new Object(); + /** + * Shared preference key for storing this node's address. + */ + private static final String SHARED_PREFERENCES_KEY_ADDRESS = "address"; + + /** + * Shared preference key for storing this node's public key. + */ + private static final String SHARED_PREFERENCES_KEY_PUBLIC_KEY = "publicKey"; + + /** + * Shared preference key for storing this node's private key. + */ + private static final String SHARED_PREFERENCES_KEY_PRIVATE_KEY = "privateKey"; /** * Default public peer interface. TODO Remove. @@ -67,6 +85,78 @@ public class CjdrouteConf { " \"location\": \"Newark,NJ,USA\"\n" + "}"; + public static Observable fetch0(Context context) { + final Context appContext = context.getApplicationContext(); + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + String filesDir = appContext.getFilesDir().getPath(); + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(appContext); + Node.Me me = from(sharedPref.getString(SHARED_PREFERENCES_KEY_ADDRESS, null), + sharedPref.getString(SHARED_PREFERENCES_KEY_PUBLIC_KEY, null), + sharedPref.getString(SHARED_PREFERENCES_KEY_PRIVATE_KEY, null)); + if (me != null) { + // Return existing node info. + subscriber.onNext(me); + subscriber.onCompleted(); + } else { + // Generate new node and return info. + try { + // Copy executables. + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE); + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE + "-init"); // TODO Remove. + + // Create new configuration file from which to get node info. + String[] cmd = { + CMD_SET_UP_SHELL, + CMD_EXECUTE_COMMAND, + String.format(Locale.ENGLISH, CMD_GENERATE_CJDROUTE_CONF_TEMPLATE, filesDir, filesDir) + }; + InputStream is = null; + try { + // Generate new configurations. + Process process = Runtime.getRuntime().exec(cmd); + is = process.getInputStream(); + JSONObject json = new JSONObject(fromInputStream(is)); + + // Get node info. + String ipv6 = (String) json.get("ipv6"); + String publicKey = (String) json.get("publicKey"); + String privateKey = (String) json.get("privateKey"); + me = from(ipv6, publicKey, privateKey); + if (me != null) { + // Store node info. + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(SHARED_PREFERENCES_KEY_ADDRESS, ipv6); + editor.putString(SHARED_PREFERENCES_KEY_PUBLIC_KEY, publicKey); + editor.putString(SHARED_PREFERENCES_KEY_PRIVATE_KEY, privateKey); + editor.apply(); + + // Return JSON object and complete Rx contract. + subscriber.onNext(new Node.Me("Me", ipv6, publicKey, privateKey)); + subscriber.onCompleted(); + } else { + subscriber.onError(new IOException("Failed to generate node info")); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + } catch (IOException e) { + subscriber.onError(e); + } + } + } + }); + } + /** * {@link Observable} for cjdroute configuration JSON object. * @@ -76,7 +166,6 @@ public class CjdrouteConf { public static Observable fetch(Context context) { final Context appContext = context.getApplicationContext(); return Observable.create(new Observable.OnSubscribe() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void call(Subscriber subscriber) { synchronized (sLock) { @@ -105,20 +194,42 @@ public void call(Subscriber subscriber) { } } } else { - // If cjdroute is not present in the files directory, it needs to be copied over from assets. - File cjdroutefile = new File(filesDir, Cjdroute.FILENAME_CJDROUTE); - if (!cjdroutefile.exists()) { - // Copy cjdroute from assets folder to the files directory. + try { + // Copy executables. + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE); + copyExecutable(appContext, filesDir, Cjdroute.FILENAME_CJDROUTE + "-init"); // TODO Remove. + + // Create new configuration file from which to return JSON object. + String[] cmd = { + CMD_SET_UP_SHELL, + CMD_EXECUTE_COMMAND, + String.format(Locale.ENGLISH, CMD_GENERATE_CJDROUTE_CONF_TEMPLATE, filesDir, filesDir) + }; InputStream is = null; FileOutputStream os = null; try { - String abi = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI; - is = appContext.getAssets().open(abi + "/" + Cjdroute.FILENAME_CJDROUTE); - os = appContext.openFileOutput(Cjdroute.FILENAME_CJDROUTE, Context.MODE_PRIVATE); - copyStream(is, os); - } catch (IOException e) { + // Generate new configurations. + Process process = Runtime.getRuntime().exec(cmd); + is = process.getInputStream(); + JSONObject json = new JSONObject(fromInputStream(is)); + + // Append default peer credentials. TODO Remove. + json.getJSONObject("interfaces") + .getJSONArray("UDPInterface") + .getJSONObject(0) + .getJSONObject("connectTo") + .put(DEFAULT_PEER_INTERFACE, new JSONObject(DEFAULT_PEER_CREDENTIALS)); + + // Write configurations to file. + os = appContext.openFileOutput(FILENAME_CJDROUTE_CONF, Context.MODE_PRIVATE); + os.write(json.toString().getBytes()); + os.flush(); + + // Return JSON object and complete Rx contract. + subscriber.onNext(json); + subscriber.onCompleted(); + } catch (IOException | JSONException e) { subscriber.onError(e); - return; } finally { if (is != null) { try { @@ -135,62 +246,8 @@ public void call(Subscriber subscriber) { } } } - } - - // Create new configuration file from which to return JSON object. - if (cjdroutefile.exists()) { - if (cjdroutefile.canExecute() || cjdroutefile.setExecutable(true)) { - String[] cmd = { - CMD_SET_UP_SHELL, - CMD_EXECUTE_COMMAND, - String.format(Locale.ENGLISH, CMD_GENERATE_CJDROUTE_CONF_TEMPLATE, filesDir, filesDir) - }; - InputStream is = null; - FileOutputStream os = null; - try { - // Generate new configurations. - Process process = Runtime.getRuntime().exec(cmd); - is = process.getInputStream(); - JSONObject json = new JSONObject(fromInputStream(is)); - - // Append default peer credentials. TODO Remove. - json.getJSONObject("interfaces") - .getJSONArray("UDPInterface") - .getJSONObject(0) - .getJSONObject("connectTo") - .put(DEFAULT_PEER_INTERFACE, new JSONObject(DEFAULT_PEER_CREDENTIALS)); - - // Write configurations to file. - os = appContext.openFileOutput(FILENAME_CJDROUTE_CONF, Context.MODE_PRIVATE); - os.write(json.toString().getBytes()); - os.flush(); - - // Return JSON object and complete Rx contract. - subscriber.onNext(json); - subscriber.onCompleted(); - } catch (IOException | JSONException e) { - subscriber.onError(e); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Do nothing. - } - } - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - } else { - subscriber.onError(new IOException("Failed to execute cjdroute in " + cjdroutefile.getPath())); - } - } else { - subscriber.onError(new FileNotFoundException("Failed to find cjdroute in " + cjdroutefile.getPath())); + } catch (IOException e) { + subscriber.onError(e); } } } @@ -198,6 +255,74 @@ public void call(Subscriber subscriber) { }); } + /** + * Creates a {@link berlin.meshnet.cjdns.model.Node.Me}. + * + * @param address The ipv6 address. + * @param publicKey The public key. + * @param privateKey The private key. + * @return The {@link berlin.meshnet.cjdns.model.Node.Me}; or {@code null} if invalid input. + */ + private static Node.Me from(String address, String publicKey, String privateKey) { + if (!TextUtils.isEmpty(address) && !TextUtils.isEmpty(publicKey) && !TextUtils.isEmpty(privateKey)) { + return new Node.Me("Me", address, publicKey, privateKey); + } + return null; + } + + /** + * Copies a file from assets folder and makes executable. If the file is already in that state, + * this is a no-op. + * + * @param context The {@link Context}. + * @param filesDir The files directory. + * @param filename The filename to copy. + * @return The copied executable file. + * @throws IOException Thrown if copying failed. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static File copyExecutable(Context context, String filesDir, String filename) throws IOException { + // If file is not present in the files directory, it needs to be copied over from assets. + File copyFile = new File(filesDir, filename); + if (!copyFile.exists()) { + // Copy file from assets folder to the files directory. + InputStream is = null; + FileOutputStream os = null; + try { + String abi = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI; + is = context.getAssets().open(abi + "/" + filename); + os = context.openFileOutput(filename, Context.MODE_PRIVATE); + copyStream(is, os); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + if (os != null) { + try { + os.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + } + + // Check file existence and permissions. + if (copyFile.exists()) { + if (copyFile.canExecute() || copyFile.setExecutable(true)) { + return copyFile; + } else { + throw new IOException("Failed to make " + copyFile + " executable in " + copyFile.getPath()); + } + } else { + throw new FileNotFoundException("Failed to create " + copyFile + " in " + copyFile.getPath()); + } + } + /** * Writes an {@link InputStream} to an {@link OutputStream}. * diff --git a/src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java b/src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java new file mode 100644 index 0000000..b19078d --- /dev/null +++ b/src/main/java/berlin/meshnet/cjdns/FileDescriptorSender.java @@ -0,0 +1,54 @@ +package berlin.meshnet.cjdns; + +import java.io.IOException; +import java.util.Locale; + +import rx.Observable; +import rx.Subscriber; + +/** + * Utility for sending a file descriptor through a named pipe. + */ +public class FileDescriptorSender { + + static { + System.loadLibrary("sendfd"); + } + + /** + * Native method for sending file descriptor through the named pipe. + * + * @param path The path to the named pipe. + * @param file_descriptor The file descriptor. + * @return {@code 0} if successful; {@code -1} if failed. + */ + public static native int sendfd(String path, int file_descriptor); + + /** + * Sends a file descriptor through the named pipe. + * + * @param path The path to the named pipe. + * @param fd The file descriptor. + * @return {@link Observable} that emits {@code true} if successful; otherwise + * {@link Subscriber#onError(Throwable)} is called. + */ + static Observable send(final String path, final int fd) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + try { + if (FileDescriptorSender.sendfd(path, fd) == 0) { + subscriber.onNext(true); + subscriber.onCompleted(); + } else { + Exception e = new IOException(String.format(Locale.ENGLISH, + "Failed to send file descriptor %1$s to named pipe %2$s", fd, path)); + subscriber.onError(e); + } + } catch (Exception e) { + subscriber.onError(e); + } + } + }); + } +} diff --git a/src/main/java/berlin/meshnet/cjdns/MainActivity.java b/src/main/java/berlin/meshnet/cjdns/MainActivity.java index 19c406a..10800a1 100644 --- a/src/main/java/berlin/meshnet/cjdns/MainActivity.java +++ b/src/main/java/berlin/meshnet/cjdns/MainActivity.java @@ -1,8 +1,11 @@ package berlin.meshnet.cjdns; +import android.annotation.TargetApi; import android.content.Intent; import android.content.res.Configuration; import android.content.res.TypedArray; +import android.net.VpnService; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; @@ -15,6 +18,7 @@ import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SwitchCompat; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -30,6 +34,7 @@ import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -48,12 +53,13 @@ import butterknife.InjectView; import rx.Subscription; import rx.android.app.AppObservable; -import rx.functions.Action0; import rx.functions.Action1; import rx.schedulers.Schedulers; public class MainActivity extends AppCompatActivity { + private static final String TAG = MainActivity.class.getSimpleName(); + private static final String BUNDLE_KEY_SELECTED_CONTENT = "selectedContent"; @Inject @@ -68,6 +74,8 @@ public class MainActivity extends AppCompatActivity { @InjectView(R.id.drawer) ListView mDrawer; + private SwitchCompat mSwitch; + private ActionBarDrawerToggle mDrawerToggle; private ArrayAdapter mDrawerAdapter; @@ -78,6 +86,8 @@ public class MainActivity extends AppCompatActivity { private List mSubscriptions = new ArrayList<>(); + private boolean mIsCjdnsRunning = false; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -169,37 +179,42 @@ public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); + // TODO Sync toggle properly. + + // Configure toggle click behaviour. + mSwitch = (SwitchCompat) MenuItemCompat.getActionView(menu.findItem(R.id.switch_cjdns_service)); + mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked && !mIsCjdnsRunning) { + mBus.post(new ApplicationEvents.StartCjdnsService()); + mIsCjdnsRunning = true; + } else if (!isChecked && mIsCjdnsRunning) { + mBus.post(new ApplicationEvents.StopCjdnsService()); + mIsCjdnsRunning = false; + } + } + }); + // Set initial state of toggle and click behaviour. - final SwitchCompat cjdnsServiceSwitch = (SwitchCompat) MenuItemCompat.getActionView(menu.findItem(R.id.switch_cjdns_service)); - mSubscriptions.add(AppObservable.bindActivity(this, Cjdroute.running(this) - .subscribeOn(Schedulers.io())) - .subscribe(new Action1() { - @Override - public void call(Integer pid) { - // Change toggle check state if there is a currently running cjdroute process. - cjdnsServiceSwitch.setChecked(true); - } - }, new Action1() { - @Override - public void call(Throwable throwable) { - // Do nothing. - } - }, new Action0() { - @Override - public void call() { - // Configure toggle click behaviour. - cjdnsServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - mBus.post(new ApplicationEvents.StartCjdnsService()); - } else { - mBus.post(new ApplicationEvents.StopCjdnsService()); - } - } - }); - } - })); + try { + mSubscriptions.add(AppObservable.bindActivity(this, Cjdroute.running() + .subscribeOn(Schedulers.io())) + .subscribe(new Action1() { + @Override + public void call(Long pid) { + // Change toggle check state if there is a currently running cjdroute process. + mSwitch.setChecked(mIsCjdnsRunning = pid != Cjdroute.INVALID_PID); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { +// mSwitch.setChecked(mIsCjdnsRunning = false); + } + })); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } return super.onCreateOptionsMenu(menu); } @@ -240,23 +255,43 @@ protected void onDestroy() { super.onDestroy(); } + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Subscribe public void handleEvent(ApplicationEvents.StartCjdnsService event) { Toast.makeText(getApplicationContext(), "Starting CjdnsService", Toast.LENGTH_SHORT).show(); - startService(new Intent(getApplicationContext(), CjdnsService.class)); + + // Start cjdns VPN. + Intent intent = VpnService.prepare(this); + if (intent != null) { + startActivityForResult(intent, 0); + } else { + onActivityResult(0, RESULT_OK, null); + } + + // TODO Compat. +// startService(new Intent(getApplicationContext(), CjdnsService.class)); } + @Subscribe public void handleEvent(ApplicationEvents.StopCjdnsService event) { Toast.makeText(getApplicationContext(), "Stopping CjdnsService", Toast.LENGTH_SHORT).show(); // Kill cjdroute process. - Cjdroute.running(this) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe(mCjdroute.terminate()); + try { + Cjdroute.running() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(mCjdroute.terminate()); + } catch (UnknownHostException e) { + Log.e(TAG, "Failed to start AdminApi", e); + } - stopService(new Intent(getApplicationContext(), CjdnsService.class)); + // TODO Do this properly. + mSwitch.setChecked(false); + + // TODO Compat. +// stopService(new Intent(getApplicationContext(), CjdnsService.class)); } @Subscribe @@ -316,4 +351,11 @@ public boolean onOptionsItemSelected(MenuItem item) { } return super.onOptionsItemSelected(item); } + + @Override + protected void onActivityResult(int request, int result, Intent data) { + if (result == RESULT_OK) { + startService(new Intent(this, CjdnsVpnService.class)); + } + } } diff --git a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java index 805bcda..882350e 100644 --- a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java @@ -152,6 +152,11 @@ public void call(Theme theme) { mIsInternalsVisible = theme.isInternalsVisible; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(peerStream.subscribe(new Action1() { @@ -160,6 +165,11 @@ public void call(Node.Peer peer) { mPeer = peer; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } @@ -219,7 +229,7 @@ public View getView(int position, View convertView, ViewGroup parent) { public void onClick(View v) { List connections = new ArrayList<>(Arrays.asList(mPeer.outgoingConnections)); connections.remove(credential); - Node.Peer update = new Node.Peer(mPeer.id, mPeer.name, mPeer.publicKey, + Node.Peer update = new Node.Peer(mPeer.id, mPeer.name, "", mPeer.publicKey, connections.toArray(new Credential[connections.size()])); mBus.post(new PeerEvents.Update(update)); } diff --git a/src/main/java/berlin/meshnet/cjdns/model/Node.java b/src/main/java/berlin/meshnet/cjdns/model/Node.java index 86b2666..3e50e40 100644 --- a/src/main/java/berlin/meshnet/cjdns/model/Node.java +++ b/src/main/java/berlin/meshnet/cjdns/model/Node.java @@ -7,14 +7,14 @@ public abstract class Node { public final String name; - public final String publicKey; - public final String address; - public Node(String name, String publicKey) { + public final String publicKey; + + public Node(String name, String address, String publicKey) { this.name = name; + this.address = address; this.publicKey = publicKey; - this.address = "fc00:0000:0000:0000:0000:0000:0000:0000"; } /** @@ -26,8 +26,8 @@ public static class Me extends Node { public final Stats.Me stats; - public Me(String name, String publicKey, String privateKey) { - super(name, publicKey); + public Me(String name, String address, String publicKey, String privateKey) { + super(name, address, publicKey); this.privateKey = privateKey; this.stats = new Stats.Me("", true, 0L, 0, 0, 0, 0, 0, 0); } @@ -44,8 +44,8 @@ public static class Peer extends Node { public final Stats stats; - public Peer(int id, String name, String publicKey, Credential[] outgoingConnections) { - super(name, publicKey); + public Peer(int id, String name, String address, String publicKey, Credential[] outgoingConnections) { + super(name, address, publicKey); this.id = id; this.outgoingConnections = outgoingConnections; this.stats = new Stats("", true, 0L, 0, 0, 0, 0, 0); diff --git a/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java index ea4ba8b..cd9c86a 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java @@ -185,6 +185,11 @@ public void call(Theme theme) { mIsInternalsVisible = theme.isInternalsVisible; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(createStream.subscribe(new Action1() { @@ -193,6 +198,11 @@ public void call(Credential.Authorized credential) { mCredentials.add(credential); notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(updateStream.subscribe(new Action1() { @@ -204,6 +214,11 @@ public void call(Credential.Authorized credential) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(removeStream.subscribe(new Action1() { @@ -215,6 +230,11 @@ public void call(Credential.Authorized credential) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } diff --git a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java index fe436f1..95c9845 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java @@ -1,5 +1,6 @@ package berlin.meshnet.cjdns.page; +import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; @@ -71,9 +72,14 @@ public void onActivityCreated(Bundle savedInstanceState) { public void call(Theme theme) { mPublicKey.setVisibility(theme.isInternalsVisible ? View.VISIBLE : View.GONE); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); - mSubscriptions.add(AppObservable.bindFragment(this, mMeProducer.stream()) + mSubscriptions.add(AppObservable.bindFragment(this, mMeProducer.stream(getContext())) .subscribe(new Action1() { @Override public void call(Node.Me me) { @@ -81,7 +87,35 @@ public void call(Node.Me me) { mAddressTextView.setText(me.address); mPublicKeyTextView.setText(me.publicKey); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); + + // Share address on click. + mAddressTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mMeProducer.stream(getContext()) + .subscribe(new Action1() { + @Override + public void call(Node.Me me) { + Intent intent = new Intent(android.content.Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(android.content.Intent.EXTRA_SUBJECT, "cjdns IPv6"); + intent.putExtra(android.content.Intent.EXTRA_TEXT, me.address); + startActivity(Intent.createChooser(intent, "Share using...")); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } + }); + } + }); } @Override diff --git a/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java index 18c678a..ec116df 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java @@ -158,6 +158,11 @@ public void call(Theme theme) { mIsInternalsVisible = theme.isInternalsVisible; notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(createStream.subscribe(new Action1() { @@ -166,6 +171,11 @@ public void call(Node.Peer peer) { mPeers.add(peer); notifyDataSetChanged(); } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(updateStream.subscribe(new Action1() { @@ -177,6 +187,11 @@ public void call(Node.Peer peer) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); mSubscriptions.add(removeStream.subscribe(new Action1() { @@ -188,6 +203,11 @@ public void call(Node.Peer peer) { notifyDataSetChanged(); } } + }, new Action1() { + @Override + public void call(Throwable throwable) { + // TODO + } })); } diff --git a/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java index 66e7f98..bbd25b7 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java @@ -7,17 +7,21 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; import android.provider.Settings; import android.support.v4.app.Fragment; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; +import com.squareup.otto.Bus; + import java.io.File; import javax.inject.Inject; import berlin.meshnet.cjdns.CjdnsApplication; import berlin.meshnet.cjdns.R; +import berlin.meshnet.cjdns.event.ApplicationEvents; /** * The page to configure application settings. @@ -26,6 +30,9 @@ public class SettingsPageFragment extends PreferenceFragmentCompat { private static final String TYPE_APK = "image/apk"; + @Inject + Bus mBus; + @Inject SharedPreferences mSharedPreferences; @@ -80,6 +87,23 @@ public boolean onPreferenceClick(Preference preference) { preference.setEnabled(false); } + String resetIdentityKey = getString(R.string.setting_reset_identity_key); + getPreferenceManager().findPreference(resetIdentityKey) + .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // Wipe node info. + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.clear(); + editor.apply(); + + // Shut off node. + mBus.post(new ApplicationEvents.StopCjdnsService()); + + return true; + } + }); + String sendApkKey = getString(R.string.setting_send_apk_key); getPreferenceManager().findPreference(sendApkKey) .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { diff --git a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java index ab243fb..57e6a6b 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/MeProducer.java @@ -1,7 +1,11 @@ package berlin.meshnet.cjdns.producer; +import android.content.Context; + +import berlin.meshnet.cjdns.CjdrouteConf; import berlin.meshnet.cjdns.model.Node; import rx.Observable; +import rx.schedulers.Schedulers; import rx.subjects.BehaviorSubject; /** @@ -9,7 +13,19 @@ */ public interface MeProducer { - Observable stream(); + Observable stream(Context context); + + /** + * Default implementation of a {@link MeProducer}. + */ + class Default implements MeProducer { + + @Override + public Observable stream(Context context) { + return CjdrouteConf.fetch0(context) + .subscribeOn(Schedulers.io()); + } + } /** * Mock implementation of a {@link MeProducer}. @@ -17,9 +33,12 @@ public interface MeProducer { class Mock implements MeProducer { @Override - public Observable stream() { - BehaviorSubject stream = BehaviorSubject.create(); - return stream.startWith(new Node.Me("Hyperborean", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); + public Observable stream(Context context) { + return BehaviorSubject.create() + .startWith(new Node.Me("Me", + "fc00::/8", + "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", + "LoremipsumdolorsitametpraesentconsequatliberolacusmagnisEratgrav")); } } } diff --git a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java index b3c96f1..027046f 100644 --- a/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java +++ b/src/main/java/berlin/meshnet/cjdns/producer/PeersProducer.java @@ -2,15 +2,21 @@ import com.squareup.otto.Subscribe; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; +import berlin.meshnet.cjdns.AdminApi; import berlin.meshnet.cjdns.event.PeerEvents; import berlin.meshnet.cjdns.model.Credential; import berlin.meshnet.cjdns.model.Node; import berlin.meshnet.cjdns.model.Protocol; import rx.Observable; +import rx.Subscriber; +import rx.functions.Func1; +import rx.schedulers.Schedulers; import rx.subjects.ReplaySubject; /** @@ -24,21 +30,95 @@ public interface PeersProducer { Observable removeStream(); + /** + * Default implementation of a {@link PeersProducer}. + */ + class Default implements PeersProducer { + + private AdminApi mApi; + + private List mPeers = new ArrayList<>(); + + private ReplaySubject mCreateStream = ReplaySubject.create(); + + private ReplaySubject mUpdateStream = ReplaySubject.create(); + + private ReplaySubject mRemoveStream = ReplaySubject.create(); + + public Default() throws UnknownHostException { + mApi = new AdminApi(); + } + + @Override + public Observable createStream() { + return mCreateStream.startWith( + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + List peers = AdminApi.InterfaceController.peerStats(mApi) + .toBlocking().first(); + for (Node.Peer peer : peers) { + subscriber.onNext(peer); + } + subscriber.onCompleted(); + } + })) + .subscribeOn(Schedulers.io()); + } + + @Override + public Observable updateStream() { + return mUpdateStream.subscribeOn(Schedulers.io()); + } + + @Override + public Observable removeStream() { + return mRemoveStream.subscribeOn(Schedulers.io()); + } + + @Subscribe + public void handleEvent(PeerEvents.Create event) { + Node.Peer peer = new Node.Peer(mPeers.size(), + UUID.randomUUID().toString(), + "fc00::/8", + "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", + null); + mPeers.add(peer); + mCreateStream.onNext(peer); + } + + @Subscribe + public void handleEvent(PeerEvents.Update event) { + int index = mPeers.indexOf(event.mPeer); + if (mPeers.remove(event.mPeer)) { + mPeers.add(index, event.mPeer); + mUpdateStream.onNext(event.mPeer); + } + } + + @Subscribe + public void handleEvent(PeerEvents.Remove event) { + if (mPeers.remove(event.mPeer)) { + mRemoveStream.onNext(event.mPeer); + } + } + } + /** * Mock implementation of a {@link PeersProducer}. */ class Mock implements PeersProducer { private static List sPeers = new ArrayList() {{ - add(new Node.Peer(0, "Alice", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ + add(new Node.Peer(0, "Alice", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ new Credential(0, "Alice credential 0", new Protocol(Protocol.Interface.udp, Protocol.Link.wifiDirect), "Loremipsumdolorsitametpharetrae"), new Credential(1, "Alice credential 1", new Protocol(Protocol.Interface.eth, Protocol.Link.bluetooth), "Loremipsumdolorsitametpharetrae") })); - add(new Node.Peer(1, "Bob", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ + add(new Node.Peer(1, "Bob", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{ new Credential(2, "Bob credential 0", new Protocol(Protocol.Interface.udp, Protocol.Link.overlay), "Loremipsumdolorsitametpharetrae") })); - add(new Node.Peer(2, "Caleb", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{})); - add(new Node.Peer(3, "Danielle", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null)); + add(new Node.Peer(2, "Caleb", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", new Credential[]{})); + add(new Node.Peer(3, "Danielle", "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null)); }}; private ReplaySubject mCreateStream = ReplaySubject.create(); @@ -64,7 +144,7 @@ public Observable removeStream() { @Subscribe public void handleEvent(PeerEvents.Create event) { - Node.Peer peer = new Node.Peer(sPeers.size(), UUID.randomUUID().toString(), + Node.Peer peer = new Node.Peer(sPeers.size(), UUID.randomUUID().toString(), "", "Loremipsumdolorsitametpharetraeratestvivamusrisusi.k", null); sPeers.add(peer); mCreateStream.onNext(peer); diff --git a/src/main/jni/Android.mk b/src/main/jni/Android.mk new file mode 100644 index 0000000..b268f45 --- /dev/null +++ b/src/main/jni/Android.mk @@ -0,0 +1,14 @@ +# Path of the sources +JNI_DIR := $(call my-dir) + +LOCAL_PATH := $(JNI_DIR) + +# The only real JNI libraries +include $(CLEAR_VARS) +LOCAL_CFLAGS = -DTARGET_ARCH_ABI=\"${TARGET_ARCH_ABI}\" +LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog +LOCAL_SRC_FILES:= sendfd.cpp +LOCAL_MODULE = sendfd +include $(BUILD_SHARED_LIBRARY) +Truct Sockaddr* lladdr = Sockaddr_clone(lladdrParm, epAlloc); + diff --git a/src/main/jni/Application.mk b/src/main/jni/Application.mk new file mode 100644 index 0000000..499b8ac --- /dev/null +++ b/src/main/jni/Application.mk @@ -0,0 +1,12 @@ +APP_ABI := arm64-v8a armeabi armeabi-v7a mips x86 x86_64 +APP_PLATFORM := android-23 + +APP_STL:=stlport_static +#APP_STL:=gnustl_shared + +#APP_OPTIM := release + +#LOCAL_ARM_MODE := arm + +#NDK_TOOLCHAIN_VERSION=clang + diff --git a/src/main/jni/sendfd.cpp b/src/main/jni/sendfd.cpp new file mode 100644 index 0000000..e46bba4 --- /dev/null +++ b/src/main/jni/sendfd.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "sendfd.h" + +jint Java_berlin_meshnet_cjdns_FileDescriptorSender_sendfd(JNIEnv *env, jobject thiz, jstring path, jint file_descriptor) +{ + int fd, len, err, rval; + const char *pipe_path = env->GetStringUTFChars(path, 0); + struct sockaddr_un un; + char buf[256]; + +#ifndef NDEBUG + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd() called with [%s] [%d]", pipe_path, file_descriptor); +#endif + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { +#ifndef NDEBUG + char *errstr = strerror(errno); + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd socket() failed [%s]", errstr); +#endif + return (jint)-1; + } + + memset(&un, 0, sizeof(un)); + un.sun_family = AF_UNIX; + strcpy(un.sun_path, pipe_path); + if (connect(fd, (struct sockaddr *)&un, sizeof(struct sockaddr_un)) < 0) { +#ifndef NDEBUG + char *errstr = strerror(errno); + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd connect() failed [%s]", errstr); +#endif + close(fd); + return (jint)-1; + } + + struct msghdr msg; + struct iovec iov[1]; + + union { + struct cmsghdr cm; + char control[CMSG_SPACE(sizeof(int))]; + } control_un; + struct cmsghdr *cmptr; + + msg.msg_control = control_un.control; + msg.msg_controllen = sizeof(control_un.control); + + cmptr = CMSG_FIRSTHDR(&msg); + cmptr->cmsg_len = CMSG_LEN(sizeof(int)); + cmptr->cmsg_level = SOL_SOCKET; + cmptr->cmsg_type = SCM_RIGHTS; + *((int *) CMSG_DATA(cmptr)) = file_descriptor; + + msg.msg_name = NULL; + msg.msg_namelen = 0; + + iov[0].iov_base = buf; + iov[0].iov_len = sizeof(buf); + msg.msg_iov = iov; + msg.msg_iovlen = 1; + + int r; + if ((r = sendmsg(fd, &msg, MSG_NOSIGNAL)) < 0) { +#ifndef NDEBUG + char *errstr = strerror(errno); + __android_log_print(ANDROID_LOG_DEBUG, "FileDescriptorSender", "sendfd sendmsg() failed [%s]", errstr); +#endif + close(fd); + return (jint)-1; + } + + close(fd); + return (jint)0; +} diff --git a/src/main/jni/sendfd.h b/src/main/jni/sendfd.h new file mode 100644 index 0000000..4b0f1be --- /dev/null +++ b/src/main/jni/sendfd.h @@ -0,0 +1,11 @@ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +jint Java_berlin_meshnet_cjdns_FileDescriptorSender_sendfd(JNIEnv *env, jobject thiz, jstring path, jint file_descriptor); + +#ifdef __cplusplus +} +#endif diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 786d539..4db3c11 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ setting_verbose_enabled_key setting_encrypt_enabled_key + setting_reset_identity_key setting_send_apk_key setting_link_wifi_direct_key setting_link_bluetooth_key @@ -46,6 +47,8 @@ Enabled Configure device storage encryption Not supported + Reset Identity + Wipe IPv6 of this node Share Application Send APK to another device diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 0dc23af..579d73b 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -12,30 +12,34 @@ android:enabled="false" android:key="@string/setting_encrypt_enabled_key" android:title="@string/settings_page_setting_encrypt_title" /> + - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 36d4357d04062496dfd89492379a8587f659617a Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Wed, 12 Oct 2016 03:18:16 -0400 Subject: [PATCH 16/24] Make everything build --- .gitignore | 3 ++- .travis.yml | 11 +++++++++-- README.md | 18 +++++++----------- build.gradle | 51 +++++++++++++++++++++++++-------------------------- 4 files changed, 43 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 2c38e94..d1766d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle/ .idea/ build/ +cjdns/ local.properties *.iml *.class @@ -13,4 +14,4 @@ src/main/assets/x86/ src/main/assets/x86_64/ src/main/assets/mips/ src/main/assets/mips64/ -src/main/assets/all/ \ No newline at end of file +src/main/assets/all/ diff --git a/.travis.yml b/.travis.yml index 9f558db..1b3608c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ android: - tools # The BuildTools version used by your project - - build-tools-24.0.0 + - build-tools-24.0.3 # The SDK version used to compile your project - android-24 @@ -18,4 +18,11 @@ android: - 'android-sdk-license-.+' - 'google-gdk-license-.+' -script: ./gradlew assembleDebug +before_script: + - export NDK_VERSION=android-ndk-r11c + - curl -L https://dl.google.com/android/repository/${NDK_VERSION}-linux-x86_64.zip -O + - unzip -q ${NDK_VERSION}-linux-x86_64.zip + - export ANDROID_NDK_HOME=`pwd`/${NDK_VERSION} + - export PATH=${ANDROID_NDK_HOME}:${PATH} + +script: ./install_debug diff --git a/README.md b/README.md index c55b9ab..8f3b293 100644 --- a/README.md +++ b/README.md @@ -9,26 +9,22 @@ Meshnet is an Android app that lets you connect to cjdns networks, without the n Installation ------------ + +1. Install the [Android SDK](http://developer.android.com/sdk/index.html) 1. Clone this application repo -2. Build application and install on device: - ``` - ./gradlew installDebug - ``` +1. Optionally download the [Android NDK](https://developer.android.com/ndk/index.html) version r11c and set `ANDROID_NDK_HOME` to its path + +1. Build application and install on device by running `./install_debug`. This will also clone the [cjdns repo](https://github.com/cjdelisle/cjdns) and build the native artifacts for Android. If `ANDROID_NDK_HOME` is not set or the version is incorrect, the Android NDK will also be downloaded. -3. (Optional) Install the [Android SDK](http://developer.android.com/sdk/index.html) and open project using it +**Note:** The cjdns repo is currently cloned from a fork until patches are merged into **cjdelisle/cjdns**. Contact ------- - Find out how to help by visiting our [issue tracker](https://github.com/hyperboria/android/issues) -- IRC channel for this project: **#android on [HypeIRC](irc://irc.hypeirc.net)** - - ``` - fc13:6176:aaca:8c7f:9f55:924f:26b3:4b14 - fcbf:7bbc:32e4:0716:bd00:e936:c927:fc14 - ``` +- [Matrix](https://matrix.org) chat room for this project: [#android:tomesh.net](https://chat.tomesh.net/#/room/#android:tomesh.net) Notes ----- diff --git a/build.gradle b/build.gradle index 60ae22b..145e8d9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,32 +17,27 @@ buildscript { } } +ext { + supportVersion = '24.2.1' +} + import org.ajoberstar.gradle.git.tasks.* -task Clone(type: GitClone) { +task cloneCjdns(type: GitClone) { def destination = file("cjdns") - uri = "https://github.com/cjdelisle/cjdns" + + uri = "https://github.com/benhylau/cjdns" // Use this repo until patch is merged in cjdelisle/cjdns destinationPath = destination bare = false - enabled = !destination.exists() //to clone only once + enabled = !destination.exists() // Clone only on first run } -task BuildCJDNS(type: Exec) { +task buildCjdns(type: Exec) { workingDir file("cjdns") commandLine file("cjdns/android_do") - - standardOutput = new ByteArrayOutputStream() - errorOutput = standardOutput - ignoreExitValue = true - doLast { - if (execResult.exitValue != 0) { - println(standardOutput.toString()) - throw new GradleException("Exec failed! See output above.") - } - } } -task CopyT(type: Copy) { +task copyNativeArtifacts(type: Copy) { from 'cjdns/build_android/out/*' into 'src/main/assets/' } @@ -51,48 +46,52 @@ apply plugin: 'com.android.application' apply plugin: 'checkstyle' android { - buildToolsVersion '24' + buildToolsVersion "24.0.3" compileSdkVersion 24 + defaultConfig { minSdkVersion 9 targetSdkVersion 24 versionCode 1 versionName "1.0.0-SNAPSHOT" } + sourceSets.main { jni.srcDirs = [] jniLibs.srcDir 'src/main/libs' } + signingConfigs { release } + buildTypes { release { signingConfig signingConfigs.release } } + lintOptions { disable 'InvalidPackage' } - } afterEvaluate { android.applicationVariants.all { variant -> - variant.javaCompiler.dependsOn(Clone) + variant.javaCompiler.dependsOn(cloneCjdns) } } -Clone.finalizedBy(BuildCJDNS) -BuildCJDNS.finalizedBy(CopyT) +cloneCjdns.finalizedBy(buildCjdns) +buildCjdns.finalizedBy(copyNativeArtifacts) dependencies { - compile 'com.android.support:support-v4:24.0.0' - compile 'com.android.support:appcompat-v7:24.0.0' - compile 'com.android.support:cardview-v7:24.0.0' - compile 'com.android.support:recyclerview-v7:24.0.0' - compile 'com.android.support:preference-v7:24.0.0' - compile 'com.android.support:preference-v14:24.0.0' + compile "com.android.support:support-v4:$supportVersion" + compile "com.android.support:appcompat-v7:$supportVersion" + compile "com.android.support:cardview-v7:$supportVersion" + compile "com.android.support:recyclerview-v7:$supportVersion" + compile "com.android.support:preference-v7:$supportVersion" + compile "com.android.support:preference-v14:$supportVersion" compile 'com.jakewharton:butterknife:6.0.0' compile 'com.joanzapata.android:android-iconify:1.0.9' compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') { From 5e87f2a448cc8d4e826ee37bde3ca3e552fc9bd3 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Wed, 12 Oct 2016 03:40:07 -0400 Subject: [PATCH 17/24] Travis, use jdk8 --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b3608c..556e4f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,11 @@ language: android + android: components: - platform-tools - tools - # The BuildTools version used by your project - build-tools-24.0.3 - - # The SDK version used to compile your project - android-24 - extra-google-google_play_services @@ -18,6 +16,9 @@ android: - 'android-sdk-license-.+' - 'google-gdk-license-.+' +jdk: + - oraclejdk8 + before_script: - export NDK_VERSION=android-ndk-r11c - curl -L https://dl.google.com/android/repository/${NDK_VERSION}-linux-x86_64.zip -O From e4203cc94442f881394041e6a9b944056751c7fd Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Wed, 12 Oct 2016 03:40:50 -0400 Subject: [PATCH 18/24] Format --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 556e4f8..80e347d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ android: - 'google-gdk-license-.+' jdk: - - oraclejdk8 + - oraclejdk8 before_script: - export NDK_VERSION=android-ndk-r11c From b760207bebf01a1dfe3d76f324ea7792c644e005 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Wed, 12 Oct 2016 04:01:00 -0400 Subject: [PATCH 19/24] Update docs and fix Travis --- .travis.yml | 2 +- README.md | 2 +- assemble_debug | 4 ++++ assemble_release | 4 ++++ 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100755 assemble_debug create mode 100755 assemble_release diff --git a/.travis.yml b/.travis.yml index 80e347d..ec9a164 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,4 +26,4 @@ before_script: - export ANDROID_NDK_HOME=`pwd`/${NDK_VERSION} - export PATH=${ANDROID_NDK_HOME}:${PATH} -script: ./install_debug +script: ./assemble_debug diff --git a/README.md b/README.md index 8f3b293..20f88a4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ cjdns for Android Meshnet is an Android app that lets you connect to cjdns networks, without the need for a rooted phone—thanks to the android.net.VpnService API introduced with Android 4.0 (Ice Cream Sandwich). Older versions still require root and routes through a TUN device. -**Current state:** App starts and stops cjdroute for rooted devices with Android 4.4 (KitKat) and below. A public peer is added by default, so you should be able to browse websites and reach services on Hyperboria just by starting the cjdns service with the toggle. All other menus are only populated with mock data at the moment and you cannot add additional peers. +**Current state:** App starts and stops cjdroute and sets up a VPN to access Hyperboria for non-rooted devices. Two public peers are added by default, so you should be able to browse websites and reach services on Hyperboria just by starting the cjdns service with the toggle. All other menus are only populated with mock data at the moment and you cannot add additional peers. Installation ------------ diff --git a/assemble_debug b/assemble_debug new file mode 100755 index 0000000..c66c4c2 --- /dev/null +++ b/assemble_debug @@ -0,0 +1,4 @@ +#!/bin/sh + +ndk-build NDK_DEBUG=true NDK_PROJECT_PATH=src/main/ +./gradlew assembleDebug diff --git a/assemble_release b/assemble_release new file mode 100755 index 0000000..0b12f75 --- /dev/null +++ b/assemble_release @@ -0,0 +1,4 @@ +#!/bin/sh + +ndk-build NDK_DEBUG=false NDK_PROJECT_PATH=src/main/ +./gradlew assembleRelease From 147cbc795a104b131ed4c8e39660a51b250276d3 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Tue, 29 Nov 2016 01:23:07 -0500 Subject: [PATCH 20/24] Update Gradle and Android SDK versions --- .travis.yml | 4 ++-- build.gradle | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index ec9a164..3c81db1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ android: - platform-tools - tools - - build-tools-24.0.3 - - android-24 + - build-tools-25.0.1 + - android-25 - extra-google-google_play_services - extra-google-m2repository diff --git a/build.gradle b/build.gradle index 2028715..8fad380 100644 --- a/build.gradle +++ b/build.gradle @@ -12,13 +12,13 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.2.2' classpath 'org.ajoberstar:gradle-git:0.2.3' } } ext { - supportVersion = '24.2.1' + supportVersion = '25.0.1' } import org.ajoberstar.gradle.git.tasks.* @@ -46,12 +46,12 @@ apply plugin: 'com.android.application' apply plugin: 'checkstyle' android { - buildToolsVersion "24.0.3" - compileSdkVersion 24 + buildToolsVersion "25.0.1" + compileSdkVersion 25 defaultConfig { minSdkVersion 9 - targetSdkVersion 24 + targetSdkVersion 25 versionCode 1 versionName "1.0.0-SNAPSHOT" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4de0289..a104d5d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jan 06 21:17:32 EST 2015 +#Tue Nov 29 01:09:49 EST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip From 9121429f30878a9102521ec62ddf7da4370067c1 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Tue, 29 Nov 2016 02:21:45 -0500 Subject: [PATCH 21/24] Add flag to toggle compilation of cjdns native artifacts --- build.gradle | 6 ++++-- gradle.properties | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 gradle.properties diff --git a/build.gradle b/build.gradle index 8fad380..2e8d820 100644 --- a/build.gradle +++ b/build.gradle @@ -77,8 +77,10 @@ android { } afterEvaluate { - android.applicationVariants.all { variant -> - variant.javaCompiler.dependsOn(cloneCjdns) + if (COMPILE_CJDNS_NATIVE_ARTIFACTS.toBoolean()) { + android.applicationVariants.all { variant -> + variant.javaCompiler.dependsOn(cloneCjdns) + } } } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e21e524 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +COMPILE_CJDNS_NATIVE_ARTIFACTS=true From 4acb574a0f1dfb7995bbaeca05263de44f53615a Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Tue, 29 Nov 2016 03:23:33 -0500 Subject: [PATCH 22/24] Update to Dagger 2.8 --- build.gradle | 4 +- .../meshnet/cjdns/CjdnsApplication.java | 120 ++---------------- .../berlin/meshnet/cjdns/CjdnsService.java | 2 +- .../berlin/meshnet/cjdns/CjdnsVpnService.java | 2 +- .../java/berlin/meshnet/cjdns/Cjdroute.java | 10 +- .../berlin/meshnet/cjdns/MainActivity.java | 2 +- .../dialog/ConnectionsDialogFragment.java | 2 +- .../meshnet/cjdns/page/AboutPageFragment.java | 4 +- .../meshnet/cjdns/page/BasePageFragment.java | 18 --- .../cjdns/page/CredentialsPageFragment.java | 5 +- .../meshnet/cjdns/page/MePageFragment.java | 4 +- .../meshnet/cjdns/page/PeersPageFragment.java | 5 +- .../cjdns/page/SettingsPageFragment.java | 2 +- 13 files changed, 40 insertions(+), 140 deletions(-) delete mode 100644 src/main/java/berlin/meshnet/cjdns/page/BasePageFragment.java diff --git a/build.gradle b/build.gradle index 2e8d820..0ac01ce 100644 --- a/build.gradle +++ b/build.gradle @@ -104,8 +104,8 @@ dependencies { compile 'io.reactivex:rxjava:1.0.7' compile 'io.reactivex:rxandroid:0.24.0' compile 'com.squareup:otto:1.3.5' - compile 'com.squareup.dagger:dagger:1.2.2' - provided 'com.squareup.dagger:dagger-compiler:1.2.2' + compile 'com.google.dagger:dagger:2.8' + provided 'com.google.dagger:dagger-compiler:2.8' } if (project.hasProperty('keyAlias')) { diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java index 194313b..9fdda64 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsApplication.java @@ -1,128 +1,36 @@ package berlin.meshnet.cjdns; import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; import android.preference.PreferenceManager; -import com.squareup.otto.Bus; - -import java.net.UnknownHostException; - -import javax.inject.Singleton; - -import berlin.meshnet.cjdns.dialog.ConnectionsDialogFragment; -import berlin.meshnet.cjdns.page.AboutPageFragment; -import berlin.meshnet.cjdns.page.CredentialsPageFragment; -import berlin.meshnet.cjdns.page.MePageFragment; -import berlin.meshnet.cjdns.page.PeersPageFragment; -import berlin.meshnet.cjdns.page.SettingsPageFragment; -import berlin.meshnet.cjdns.producer.CredentialsProducer; -import berlin.meshnet.cjdns.producer.MeProducer; -import berlin.meshnet.cjdns.producer.PeersProducer; -import berlin.meshnet.cjdns.producer.SettingsProducer; -import dagger.Module; -import dagger.ObjectGraph; -import dagger.Provides; +import berlin.meshnet.cjdns.dagger.CoreComponent; +import berlin.meshnet.cjdns.dagger.DaggerCoreComponent; +import berlin.meshnet.cjdns.dagger.DefaultModule; /** * The {@link android.app.Application}. */ public class CjdnsApplication extends Application { - private ObjectGraph mObjectGraph; + CoreComponent mCoreComponent; @Override public void onCreate() { super.onCreate(); - mObjectGraph = ObjectGraph.create(new DefaultModule(this)); - PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - } - /** - * Injects the dependencies of {@code object} from the {@link dagger.ObjectGraph}. - * - * @param object The object instance to inject dependencies. - */ - public void inject(Object object) { - mObjectGraph.inject(object); + // Set module to provide dependencies and build core component. + mCoreComponent = DaggerCoreComponent.builder() + .defaultModule(new DefaultModule(this)) + .build(); + + // Set default values for user preferences. + PreferenceManager.setDefaultValues(this, R.xml.preferences, false); } /** - * {@link dagger.Module} providing default dependencies. + * Gets the {@link CoreComponent} holding the all dependencies. */ - @Module( - injects = { - MainActivity.class, - CjdnsVpnService.class, - CjdnsService.class, - MePageFragment.class, - PeersPageFragment.class, - CredentialsPageFragment.class, - SettingsPageFragment.class, - ConnectionsDialogFragment.class, - AboutPageFragment.class - } - ) - public static class DefaultModule { - - private Context mContext; - - private DefaultModule(Context context) { - mContext = context; - } - - @Singleton - @Provides - public Context provideContext() { - return mContext; - } - - @Singleton - @Provides - public SharedPreferences provideSharedPreferences(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context); - } - - @Singleton - @Provides - public Bus provideBus() { - return new Bus(); - } - - @Singleton - @Provides - public Cjdroute provideCjdroute(Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - return new Cjdroute.Compat(context.getApplicationContext()); - } - return new Cjdroute.Default(context.getApplicationContext()); - } - - @Provides - public SettingsProducer provideSettingsProducer(Context context, SharedPreferences sharedPreferences) { - return new SettingsProducer.Default(context, sharedPreferences); - } - - @Provides - public MeProducer provideMeProducer() { - return new MeProducer.Default(); - } - - @Provides - public PeersProducer providePeerListProducer() { - try { - return new PeersProducer.Default(); - } catch (UnknownHostException e) { - // TODO - } - return null; - } - - @Provides - public CredentialsProducer provideCredentialListProducer() { - return new CredentialsProducer.Mock(); - } + public CoreComponent getComponent() { + return mCoreComponent; } } \ No newline at end of file diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java index 969dc3d..a08aa26 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsService.java @@ -63,7 +63,7 @@ public void onCreate() { super.onCreate(); // Inject dependencies. - ((CjdnsApplication) getApplication()).inject(this); + ((CjdnsApplication) getApplication()).getComponent().inject(this); // Start foreground service. mSubscriptions.add(CjdrouteConf.fetch(this) diff --git a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java index 26a8ec2..962d0ee 100644 --- a/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java +++ b/src/main/java/berlin/meshnet/cjdns/CjdnsVpnService.java @@ -78,7 +78,7 @@ public class CjdnsVpnService extends VpnService { @Override public int onStartCommand(Intent intent, int flags, int startId) { // Inject dependencies. - ((CjdnsApplication) getApplication()).inject(this); + ((CjdnsApplication) getApplication()).getComponent().inject(this); // Register so we can subscribe to stop events. mBus.register(this); diff --git a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java index aea9814..ed38955 100644 --- a/src/main/java/berlin/meshnet/cjdns/Cjdroute.java +++ b/src/main/java/berlin/meshnet/cjdns/Cjdroute.java @@ -28,7 +28,7 @@ /** * Methods for managing the execution of cjdroute. */ -abstract class Cjdroute { +public abstract class Cjdroute { /** * The filename for the cjdroute executable. @@ -93,7 +93,7 @@ public Observable call(Boolean isSuccessful) { * super user permission. */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - static class Default extends Cjdroute { + public static class Default extends Cjdroute { /** * Log tag. @@ -102,7 +102,7 @@ static class Default extends Cjdroute { private Context mContext; - Default(Context context) { + public Default(Context context) { mContext = context; } @@ -262,7 +262,7 @@ public void call(Throwable throwable) { * Compat implementation of {@link Cjdroute}. This allows cjdroute to create a TUN device and * requires super user permission. */ - static class Compat extends Cjdroute { + public static class Compat extends Cjdroute { /** * Log tag. @@ -301,7 +301,7 @@ static class Compat extends Cjdroute { private Context mContext; - Compat(Context context) { + public Compat(Context context) { mContext = context; } diff --git a/src/main/java/berlin/meshnet/cjdns/MainActivity.java b/src/main/java/berlin/meshnet/cjdns/MainActivity.java index 10800a1..90f8ba4 100644 --- a/src/main/java/berlin/meshnet/cjdns/MainActivity.java +++ b/src/main/java/berlin/meshnet/cjdns/MainActivity.java @@ -96,7 +96,7 @@ public void onCreate(Bundle savedInstanceState) { mActionBar = getSupportActionBar(); // Inject dependencies. - ((CjdnsApplication) getApplication()).inject(this); + ((CjdnsApplication) getApplication()).getComponent().inject(this); ButterKnife.inject(this); final TypedArray drawerIcons = getResources().obtainTypedArray(R.array.drawer_icons); diff --git a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java index 882350e..f30ad1d 100644 --- a/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/dialog/ConnectionsDialogFragment.java @@ -71,7 +71,7 @@ public static DialogFragment newInstance(int peerId) { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - ((CjdnsApplication) getActivity().getApplication()).inject(this); + ((CjdnsApplication) getActivity().getApplication()).getComponent().inject(this); Bundle args = getArguments(); final int peerId = args.getInt(FRAGMENT_BUNDLE_KEY_PEER_ID); diff --git a/src/main/java/berlin/meshnet/cjdns/page/AboutPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/AboutPageFragment.java index e33a3fd..ab69308 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/AboutPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/AboutPageFragment.java @@ -11,13 +11,14 @@ import java.util.regex.Pattern; +import berlin.meshnet.cjdns.CjdnsApplication; import berlin.meshnet.cjdns.R; import butterknife.ButterKnife; /** * The page explaining what the application is about. */ -public class AboutPageFragment extends BasePageFragment { +public class AboutPageFragment extends Fragment { private View mView; @@ -35,6 +36,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + ((CjdnsApplication) getActivity().getApplication()).getComponent().inject(this); TextView version = (TextView) mView.findViewById(R.id.version); TextView ln1 = (TextView) mView.findViewById(R.id.ln1); diff --git a/src/main/java/berlin/meshnet/cjdns/page/BasePageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/BasePageFragment.java deleted file mode 100644 index 3c4c744..0000000 --- a/src/main/java/berlin/meshnet/cjdns/page/BasePageFragment.java +++ /dev/null @@ -1,18 +0,0 @@ -package berlin.meshnet.cjdns.page; - -import android.os.Bundle; -import android.support.v4.app.Fragment; - -import berlin.meshnet.cjdns.CjdnsApplication; - -/** - * The base class for pages. - */ -public abstract class BasePageFragment extends Fragment { - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - ((CjdnsApplication) getActivity().getApplication()).inject(this); - } -} diff --git a/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java index cd9c86a..fe09088 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/CredentialsPageFragment.java @@ -28,6 +28,7 @@ import javax.inject.Inject; +import berlin.meshnet.cjdns.CjdnsApplication; import berlin.meshnet.cjdns.R; import berlin.meshnet.cjdns.event.ApplicationEvents; import berlin.meshnet.cjdns.event.AuthorizedCredentialEvents; @@ -46,7 +47,7 @@ /** * The page representing the list of credentials authorized credentials. */ -public class CredentialsPageFragment extends BasePageFragment { +public class CredentialsPageFragment extends Fragment { @Inject Bus mBus; @@ -79,6 +80,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + ((CjdnsApplication) getActivity().getApplication()).getComponent().inject(this); + mCredentialsRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { diff --git a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java index 95c9845..e8e4c1b 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/MePageFragment.java @@ -15,6 +15,7 @@ import javax.inject.Inject; +import berlin.meshnet.cjdns.CjdnsApplication; import berlin.meshnet.cjdns.R; import berlin.meshnet.cjdns.model.Node; import berlin.meshnet.cjdns.model.Theme; @@ -29,7 +30,7 @@ /** * The page representing the self node. */ -public class MePageFragment extends BasePageFragment { +public class MePageFragment extends Fragment { @Inject SettingsProducer mSettingsProducer; @@ -65,6 +66,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + ((CjdnsApplication) getActivity().getApplication()).getComponent().inject(this); mSubscriptions.add(AppObservable.bindFragment(this, mSettingsProducer.themeStream()) .subscribe(new Action1() { diff --git a/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java index ec116df..dfe5953 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/PeersPageFragment.java @@ -25,6 +25,7 @@ import javax.inject.Inject; +import berlin.meshnet.cjdns.CjdnsApplication; import berlin.meshnet.cjdns.R; import berlin.meshnet.cjdns.event.ApplicationEvents; import berlin.meshnet.cjdns.event.PeerEvents; @@ -43,7 +44,7 @@ /** * The page representing the list of peers. */ -public class PeersPageFragment extends BasePageFragment { +public class PeersPageFragment extends Fragment { @Inject Bus mBus; @@ -76,6 +77,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + ((CjdnsApplication) getActivity().getApplication()).getComponent().inject(this); + mPeersRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { diff --git a/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java b/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java index bbd25b7..9537aa2 100644 --- a/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java +++ b/src/main/java/berlin/meshnet/cjdns/page/SettingsPageFragment.java @@ -48,7 +48,7 @@ public void onCreatePreferences(Bundle bundle, String s) { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - ((CjdnsApplication) getActivity().getApplication()).inject(this); + ((CjdnsApplication) getActivity().getApplication()).getComponent().inject(this); String encryptEnabledKey = getString(R.string.setting_encrypt_enabled_key); SharedPreferences.Editor editor = mSharedPreferences.edit(); From fa329baeef42de59da43eb734e39366c9869eca4 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Tue, 29 Nov 2016 03:46:29 -0500 Subject: [PATCH 23/24] Disable incremental compile --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 0ac01ce..b0ad98f 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,10 @@ android { } } + compileOptions { + incremental = false + } + lintOptions { disable 'InvalidPackage' } From f68e7a59c69a69650a82bc8fdeea22e85bb58055 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Tue, 29 Nov 2016 04:14:13 -0500 Subject: [PATCH 24/24] Fix .gitignore and commit missed files --- .gitignore | 2 +- build.gradle | 27 +++--- .../meshnet/cjdns/dagger/CoreComponent.java | 40 +++++++++ .../meshnet/cjdns/dagger/DefaultModule.java | 85 +++++++++++++++++++ 4 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 src/main/java/berlin/meshnet/cjdns/dagger/CoreComponent.java create mode 100644 src/main/java/berlin/meshnet/cjdns/dagger/DefaultModule.java diff --git a/.gitignore b/.gitignore index d1766d8..1ba3b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .gradle/ .idea/ build/ -cjdns/ +cjdns-src/ local.properties *.iml *.class diff --git a/build.gradle b/build.gradle index b0ad98f..331b945 100644 --- a/build.gradle +++ b/build.gradle @@ -21,24 +21,25 @@ ext { supportVersion = '25.0.1' } -import org.ajoberstar.gradle.git.tasks.* +import org.ajoberstar.gradle.git.tasks.GitClone task cloneCjdns(type: GitClone) { - def destination = file("cjdns") + def destination = file("cjdns-src") - uri = "https://github.com/benhylau/cjdns" // Use this repo until patch is merged in cjdelisle/cjdns + uri = "https://github.com/benhylau/cjdns" + // Use this repo until patch is merged in cjdelisle/cjdns destinationPath = destination bare = false enabled = !destination.exists() // Clone only on first run } task buildCjdns(type: Exec) { - workingDir file("cjdns") - commandLine file("cjdns/android_do") + workingDir file("cjdns-src") + commandLine file("cjdns-src/android_do") } task copyNativeArtifacts(type: Copy) { - from 'cjdns/build_android/out/*' + from 'cjdns-src/build_android/out/*' into 'src/main/assets/' } @@ -55,7 +56,7 @@ android { versionCode 1 versionName "1.0.0-SNAPSHOT" } - + sourceSets.main { jni.srcDirs = [] jniLibs.srcDir 'src/main/libs' @@ -71,21 +72,17 @@ android { } } - compileOptions { - incremental = false - } - lintOptions { disable 'InvalidPackage' } } afterEvaluate { - if (COMPILE_CJDNS_NATIVE_ARTIFACTS.toBoolean()) { - android.applicationVariants.all { variant -> - variant.javaCompiler.dependsOn(cloneCjdns) + if (COMPILE_CJDNS_NATIVE_ARTIFACTS.toBoolean()) { + android.applicationVariants.all { variant -> + variant.javaCompiler.dependsOn(cloneCjdns) + } } - } } cloneCjdns.finalizedBy(buildCjdns) diff --git a/src/main/java/berlin/meshnet/cjdns/dagger/CoreComponent.java b/src/main/java/berlin/meshnet/cjdns/dagger/CoreComponent.java new file mode 100644 index 0000000..a9832aa --- /dev/null +++ b/src/main/java/berlin/meshnet/cjdns/dagger/CoreComponent.java @@ -0,0 +1,40 @@ +package berlin.meshnet.cjdns.dagger; + +import javax.inject.Singleton; + +import berlin.meshnet.cjdns.CjdnsService; +import berlin.meshnet.cjdns.CjdnsVpnService; +import berlin.meshnet.cjdns.MainActivity; +import berlin.meshnet.cjdns.dialog.ConnectionsDialogFragment; +import berlin.meshnet.cjdns.page.AboutPageFragment; +import berlin.meshnet.cjdns.page.CredentialsPageFragment; +import berlin.meshnet.cjdns.page.MePageFragment; +import berlin.meshnet.cjdns.page.PeersPageFragment; +import berlin.meshnet.cjdns.page.SettingsPageFragment; +import dagger.Component; + +/** + * {@link Component} providing core dependencies. + */ +@Singleton +@Component(modules = DefaultModule.class) +public interface CoreComponent { + + void inject(MainActivity inject); + + void inject(CjdnsVpnService inject); + + void inject(CjdnsService inject); + + void inject(MePageFragment inject); + + void inject(PeersPageFragment inject); + + void inject(CredentialsPageFragment inject); + + void inject(SettingsPageFragment inject); + + void inject(AboutPageFragment inject); + + void inject(ConnectionsDialogFragment inject); +} diff --git a/src/main/java/berlin/meshnet/cjdns/dagger/DefaultModule.java b/src/main/java/berlin/meshnet/cjdns/dagger/DefaultModule.java new file mode 100644 index 0000000..bb22a6e --- /dev/null +++ b/src/main/java/berlin/meshnet/cjdns/dagger/DefaultModule.java @@ -0,0 +1,85 @@ +package berlin.meshnet.cjdns.dagger; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; + +import com.squareup.otto.Bus; + +import java.net.UnknownHostException; + +import javax.inject.Singleton; + +import berlin.meshnet.cjdns.Cjdroute; +import berlin.meshnet.cjdns.producer.CredentialsProducer; +import berlin.meshnet.cjdns.producer.MeProducer; +import berlin.meshnet.cjdns.producer.PeersProducer; +import berlin.meshnet.cjdns.producer.SettingsProducer; +import dagger.Module; +import dagger.Provides; + +/** + * {@link dagger.Module} providing default dependencies. + */ +@Module +public class DefaultModule { + + private Context mContext; + + public DefaultModule(Context context) { + mContext = context; + } + + @Singleton + @Provides + Context provideContext() { + return mContext; + } + + @Singleton + @Provides + SharedPreferences provideSharedPreferences(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + @Singleton + @Provides + Bus provideBus() { + return new Bus(); + } + + @Singleton + @Provides + Cjdroute provideCjdroute(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return new Cjdroute.Compat(context.getApplicationContext()); + } + return new Cjdroute.Default(context.getApplicationContext()); + } + + @Provides + SettingsProducer provideSettingsProducer(Context context, SharedPreferences sharedPreferences) { + return new SettingsProducer.Default(context, sharedPreferences); + } + + @Provides + MeProducer provideMeProducer() { + return new MeProducer.Default(); + } + + @Provides + PeersProducer providePeerListProducer() { + try { + return new PeersProducer.Default(); + } catch (UnknownHostException e) { + // TODO + } + return null; + } + + @Provides + CredentialsProducer provideCredentialListProducer() { + return new CredentialsProducer.Mock(); + } +}