From 89b8ee5d962233e64a5af6d3674350e1eae19931 Mon Sep 17 00:00:00 2001 From: Benedict Lau Date: Thu, 28 Apr 2016 02:31:21 -0400 Subject: [PATCH] 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