Browse Source

Merge pull request #38 from rod-hynes/master

Android Library
Rod Hynes 11 years ago
parent
commit
a6f9fce1f3
35 changed files with 1250 additions and 261 deletions
  1. 2 0
      .gitignore
  2. 2 1
      AndroidApp/.idea/gradle.xml
  3. 1 1
      AndroidApp/.idea/misc.xml
  4. 3 1
      AndroidApp/AndroidApp.iml
  5. 4 11
      AndroidApp/README.md
  6. 4 1
      AndroidApp/app/app.iml
  7. 1 1
      AndroidApp/app/build.gradle
  8. 2 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/App.java
  9. 0 202
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Client.java
  10. 133 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Psiphon.java
  11. 5 5
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Service.java
  12. 1 8
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Utils.java
  13. 47 0
      AndroidApp/app/src/main/java/go/Go.java
  14. 251 0
      AndroidApp/app/src/main/java/go/Seq.java
  15. 84 0
      AndroidApp/app/src/main/java/go/psi/Psi.java
  16. BIN
      AndroidApp/app/src/main/jniLibs/armeabi-v7a/libgojni.so
  17. 4 0
      AndroidApp/app/src/main/res/raw/psiphon_config_stub
  18. 1 1
      AndroidApp/build.gradle
  19. 2 2
      AndroidApp/gradle/wrapper/gradle-wrapper.properties
  20. 78 0
      AndroidLibrary/README.md
  21. 49 0
      AndroidLibrary/go_psi/go_psi.go
  22. 28 0
      AndroidLibrary/java_golang/LICENSE
  23. 47 0
      AndroidLibrary/java_golang/go/Go.java
  24. 251 0
      AndroidLibrary/java_golang/go/Seq.java
  25. 84 0
      AndroidLibrary/java_psi/go/psi/Psi.java
  26. BIN
      AndroidLibrary/libgojni.so
  27. 20 0
      AndroidLibrary/libpsi/main.go
  28. 91 0
      AndroidLibrary/psi/psi.go
  29. 13 2
      ConsoleClient/psiphonClient.go
  30. 1 0
      README.md
  31. 9 9
      psiphon/config.go
  32. 3 3
      psiphon/controller.go
  33. 27 11
      psiphon/dataStore.go
  34. 1 1
      psiphon/defaults.go
  35. 1 1
      psiphon/notice.go

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
+# Exclude test config files from source control
+/AndroidApp/app/src/main/res/raw/psiphon_config
 psiphon.config
 psiphon.db*
 

+ 2 - 1
AndroidApp/.idea/gradle.xml

@@ -3,8 +3,9 @@
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
-        <option name="distributionType" value="DEFAULT_WRAPPED" />
+        <option name="distributionType" value="LOCAL" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.2.1" />
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />

+ 1 - 1
AndroidApp/.idea/misc.xml

@@ -3,7 +3,7 @@
   <component name="EntryPointsManager">
     <entry_points version="2.0" />
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
 </project>

+ 3 - 1
AndroidApp/AndroidApp.iml

@@ -7,7 +7,9 @@
       </configuration>
     </facet>
   </component>
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
+  <component name="NewModuleRootManager" inherit-compiler-output="false">
+    <output url="file://$MODULE_DIR$/build/classes/main" />
+    <output-test url="file://$MODULE_DIR$/build/classes/test" />
     <exclude-output />
     <content url="file://$MODULE_DIR$">
       <excludeFolder url="file://$MODULE_DIR$/.gradle" />

+ 4 - 11
AndroidApp/README.md

@@ -11,23 +11,16 @@ traffic through tun2socks and in turn through Psiphon.
 Status
 --------------------------------------------------------------------------------
 
-* Incomplete
-  * Android app builds and runs but it is not yet stable.
+* Pre-release
 
 Native libraries
 --------------------------------------------------------------------------------
 
 `app\src\main\jniLibs\<platform>\libtun2socks.so` is built from the Psiphon fork of badvpn. Source code is here: [https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/](https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/). The source was modified to change the package name to `ca.psiphon.psibot`.
 
-Go client binary and config file
+Psiphon Android Library and config file
 --------------------------------------------------------------------------------
 
-`app\src\main\res\raw\psiphon_tunnel_core_arm` is built with Go targetting android/arm. At this time, android/arm support is not yet released but
-is available in the development branch.
+Uses the [Psiphon Android Library](../AndroidLibrary/README.md).
 
-Install Go from source. The Android instructions are here:
-[https://code.google.com/p/go/source/browse/README?repo=mobile](https://code.google.com/p/go/source/browse/README?repo=mobile).
-
-In summary, download and install the Android NDK, use a script to make a standalone toolchain, and use that toolchain to build android/arm support within the Go source install. Then cross compile as usual.
-
-In `app\src\main\res\raw\psiphon_config`, placeholders must be replaced with valid values.
+`app\src\main\res\raw\psiphon_config_stub` and its placeholder values must be replaced with `app\src\main\res\raw\psiphon_config` and valid configuration values.

+ 4 - 1
AndroidApp/app/app.iml

@@ -10,7 +10,7 @@
       <configuration>
         <option name="SELECTED_BUILD_VARIANT" value="debug" />
         <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
-        <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugJava" />
+        <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
         <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
         <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
         <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugTestSources" />
@@ -31,11 +31,13 @@
       <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/debug" isTestSource="true" generated="true" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/debug" isTestSource="true" generated="true" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/debug" isTestSource="true" generated="true" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/debug" isTestSource="true" generated="true" />
       <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/debug" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/debug" type="java-test-resource" />
       <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
       <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
       <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
@@ -77,6 +79,7 @@
       <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
       <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
       <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
     </content>
     <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
     <orderEntry type="sourceFolder" forTests="false" />

+ 1 - 1
AndroidApp/app/build.gradle

@@ -13,7 +13,7 @@ android {
     }
     buildTypes {
         release {
-            runProguard false
+            minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
     }

+ 2 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/App.java

@@ -20,11 +20,13 @@
 package ca.psiphon.psibot;
 
 import android.app.Application;
+import go.Go;
 
 public class App extends Application {
 
     @Override
     public void onCreate() {
+        Go.init(this.getApplicationContext());
         Log.initialize();
     }
 }

+ 0 - 202
AndroidApp/app/src/main/java/ca/psiphon/psibot/Client.java

@@ -1,202 +0,0 @@
-/*
- * Copyright (c) 2014, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-package ca.psiphon.psibot;
-
-import android.content.Context;
-import android.os.Build;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-
-public class Client {
-
-    private final Context mContext;
-    private final CountDownLatch mTunnelStartedSignal;
-    private File mRootDirectory;
-    private File mExecutableFile;
-    private File mConfigFile;
-    private Process mProcess;
-    private Thread mStdoutThread;
-    private int mLocalSocksProxyPort;
-    private int mLocalHttpProxyPort;
-    private List<String> mHomePages;
-
-    public Client(Context context, CountDownLatch tunnelStartedSignal) {
-        mContext = context;
-        mTunnelStartedSignal = tunnelStartedSignal;
-    }
-
-    public void start() throws Utils.PsibotError {
-        stop();
-        prepareFiles();
-
-        try {
-            mProcess = new ProcessBuilder(
-                    mExecutableFile.getAbsolutePath(),
-                    "--config", mConfigFile.getAbsolutePath())
-                    .directory(mRootDirectory)
-                    .redirectErrorStream(true)
-                    .start();
-        } catch (IOException e) {
-            throw new Utils.PsibotError("failed to start client process", e);
-        }
-
-        mStdoutThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                consumeStream(mProcess.getInputStream());
-                Log.addEntry("Psiphon client stopping");
-            }
-        });
-        mStdoutThread.start();
-
-        mLocalSocksProxyPort = 0;
-        mLocalHttpProxyPort = 0;
-        mHomePages = new ArrayList<String>();
-
-        Log.addEntry("Psiphon client started");
-    }
-
-    public void stop() {
-        if (mProcess != null) {
-            mProcess.destroy();
-            try {
-                mProcess.waitFor();
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
-            Log.addEntry("Psiphon client stopped");
-        }
-        if (mStdoutThread != null) {
-            try {
-                mStdoutThread.join();
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
-        }
-        mProcess = null;
-        mStdoutThread = null;
-    }
-
-    public synchronized int getLocalSocksProxyPort() {
-        return mLocalSocksProxyPort;
-    }
-
-    public synchronized int getLocalHttpProxyPort() {
-        return mLocalHttpProxyPort;
-    }
-
-    public synchronized List<String> getHomePages() {
-        return mHomePages != null ? new ArrayList<String>(mHomePages) : new ArrayList<String>();
-    }
-
-    private void prepareFiles() throws Utils.PsibotError {
-        mRootDirectory = mContext.getDir("psiphon_tunnel_core", Context.MODE_PRIVATE);
-        mExecutableFile = new File(mRootDirectory, "psiphon_tunnel_core");
-        mConfigFile = new File(mRootDirectory, "psiphon_config");
-        if (0 != Build.CPU_ABI.compareTo("armeabi-v7a")) {
-            throw new Utils.PsibotError("no client binary for this CPU");
-        }
-        final String errorMessage = "failed to prepare client files";
-        try {
-            Utils.writeRawResourceFile(mContext, R.raw.psiphon_tunnel_core_arm, mExecutableFile, true);
-            writeConfigFile(mContext, mConfigFile);
-        } catch (IOException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch (JSONException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch (Utils.PsibotError e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        }
-    }
-
-    private void consumeStream(InputStream inputStream) {
-        try {
-            BufferedReader reader =
-                    new BufferedReader(new InputStreamReader(inputStream));
-            String line;
-            while ((line = reader.readLine()) != null) {
-                parseLine(line);
-                // TODO: parse and use the Go client timestamp
-                // Don't display the first 20 characters: the Go client log timestamp
-                Log.addEntry(line.substring(20));
-            }
-            reader.close();
-        } catch (IOException e) {
-            // TODO: stop service if exiting due to error?
-            Log.addEntry("Psiphon client consume error: " + e.getMessage());
-        }
-    }
-
-    private synchronized void parseLine(String line) {
-        // TODO: this is based on temporary log line formats
-        final String socksProxy = "SOCKS-PROXY local SOCKS proxy running at address 127.0.0.1:";
-        final String httpProxy = "HTTP-PROXY local HTTP proxy running at address 127.0.0.1:";
-        final String homePage = "HOMEPAGE ";
-        final String tunnelStarted = "TUNNEL tunnel started";
-        int index;
-        if (-1 != (index = line.indexOf(socksProxy))) {
-            mLocalSocksProxyPort = Integer.parseInt(line.substring(index + socksProxy.length()));
-        } else if (-1 != (index = line.indexOf(httpProxy))) {
-            mLocalHttpProxyPort = Integer.parseInt(line.substring(index + httpProxy.length()));
-        } else if (-1 != (index = line.indexOf(homePage))) {
-            mHomePages.add(line.substring(index + homePage.length()));
-        } else if (line.contains(tunnelStarted)) {
-            mTunnelStartedSignal.countDown();
-        }
-    }
-
-    private void writeConfigFile(Context context, File configFile)
-            throws IOException, JSONException, Utils.PsibotError {
-        // If we can obtain a DNS resolver for the active network,
-        // prefer that for DNS resolution in BindToDevice mode.
-        String dnsResolver = null;
-        try {
-            dnsResolver = Utils.getFirstActiveNetworkDnsResolver(context);
-        } catch (Utils.PsibotError e) {
-            Log.addEntry("failed to get active network DNS resolver: " + e.getMessage());
-            // Proceed with default value in config file
-        }
-
-        // Load settings from the raw resource JSON config file and
-        // update as necessary. Then write JSON to disk for the Go client.
-        String configFileContents = Utils.readInputStreamToString(
-                context.getResources().openRawResource(R.raw.psiphon_config));
-        JSONObject json = new JSONObject(configFileContents);
-        json.put("BindToDeviceServiceAddress", "@" + SocketProtector.SOCKET_PROTECTOR_ADDRESS);
-        if (dnsResolver != null) {
-            json.put("BindToDeviceDnsServer", dnsResolver);
-        }
-        Utils.copyStream(
-                new ByteArrayInputStream(json.toString().getBytes("UTF-8")),
-                new FileOutputStream(configFile));
-    }
-}

+ 133 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/Psiphon.java

@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.content.Context;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+import go.psi.Psi;
+
+public class Psiphon extends Psi.Listener.Stub {
+
+    private final Context mContext;
+    private final CountDownLatch mTunnelStartedSignal;
+    private int mLocalSocksProxyPort;
+    private int mLocalHttpProxyPort;
+    private Set<String> mHomePages;
+
+    public Psiphon(Context context, CountDownLatch tunnelStartedSignal) {
+        mContext = context;
+        mTunnelStartedSignal = tunnelStartedSignal;
+    }
+
+    @Override
+    public void Message(String line) {
+        line = line.trim();
+
+        android.util.Log.d("PSIPHON", line);
+
+        parseLine(line);
+        Log.addEntry(line);
+    }
+
+    public void start() throws Utils.PsibotError {
+        Psi.Stop();
+
+        mLocalSocksProxyPort = 0;
+        mLocalHttpProxyPort = 0;
+        mHomePages = new HashSet<String>();
+
+        try {
+            Psi.Start(loadConfig(mContext), this);
+        } catch (Exception e) {
+            throw new Utils.PsibotError("failed to start Psiphon", e);
+        }
+
+        Log.addEntry("Psiphon started");
+    }
+
+    public void stop() {
+        Psi.Stop();
+        Log.addEntry("Psiphon stopped");
+    }
+
+    public synchronized int getLocalSocksProxyPort() {
+        return mLocalSocksProxyPort;
+    }
+
+    public synchronized int getLocalHttpProxyPort() {
+        return mLocalHttpProxyPort;
+    }
+
+    public synchronized Set<String> getHomePages() {
+        return mHomePages != null ? new HashSet<String>(mHomePages) : new HashSet<String>();
+    }
+
+    private String loadConfig(Context context)
+            throws IOException, JSONException, Utils.PsibotError {
+
+        // If we can obtain a DNS resolver for the active network,
+        // prefer that for DNS resolution in BindToDevice mode.
+        String dnsResolver = null;
+        try {
+            dnsResolver = Utils.getFirstActiveNetworkDnsResolver(context);
+        } catch (Utils.PsibotError e) {
+            Log.addEntry("failed to get active network DNS resolver: " + e.getMessage());
+            // Proceed with default value in config file
+        }
+
+        // Load settings from the raw resource JSON config file and
+        // update as necessary. Then write JSON to disk for the Go client.
+        String configFileContents = Utils.readInputStreamToString(
+                context.getResources().openRawResource(R.raw.psiphon_config));
+        JSONObject json = new JSONObject(configFileContents);
+        json.put("BindToDeviceServiceAddress", "@" + SocketProtector.SOCKET_PROTECTOR_ADDRESS);
+        if (dnsResolver != null) {
+            json.put("BindToDeviceDnsServer", dnsResolver);
+        }
+
+        return json.toString();
+    }
+
+    private synchronized void parseLine(String line) {
+        // TODO: this is based on temporary log line formats
+        final String socksProxy = "SOCKS-PROXY local SOCKS proxy running at address 127.0.0.1:";
+        final String httpProxy = "HTTP-PROXY local HTTP proxy running at address 127.0.0.1:";
+        final String homePage = "HOMEPAGE ";
+        final String tunnelStarted = "TUNNELS 1";
+        int index;
+        if (-1 != (index = line.indexOf(socksProxy))) {
+            mLocalSocksProxyPort = Integer.parseInt(line.substring(index + socksProxy.length()));
+        } else if (-1 != (index = line.indexOf(httpProxy))) {
+            mLocalHttpProxyPort = Integer.parseInt(line.substring(index + httpProxy.length()));
+        } else if (-1 != (index = line.indexOf(homePage))) {
+            mHomePages.add(line.substring(index + homePage.length()));
+        } else if (line.contains(tunnelStarted)) {
+            mTunnelStartedSignal.countDown();
+        }
+    }
+}

+ 5 - 5
AndroidApp/app/src/main/java/ca/psiphon/psibot/Service.java

@@ -64,12 +64,12 @@ public class Service extends VpnService {
             public void run() {
                 CountDownLatch tunnelStartedSignal = new CountDownLatch(1);
                 SocketProtector socketProtector = new SocketProtector(Service.this);
-                Client client = new Client(Service.this, tunnelStartedSignal);
+                Psiphon psiphon = new Psiphon(Service.this, tunnelStartedSignal);
                 try {
                     socketProtector.start();
                     // TODO: what if client local proxies unbind? in this case it's better if Go client keeps its proxies up permanently.
                     // TODO: monitor tunnel messages and update notification UI when re-connecting, etc.
-                    client.start();
+                    psiphon.start();
                     while (true) {
                         if (tunnelStartedSignal.await(100, TimeUnit.MILLISECONDS)) {
                             break;
@@ -78,7 +78,7 @@ public class Service extends VpnService {
                             throw new Utils.PsibotError("stopped while waiting tunnel");
                         }
                     }
-                    int localSocksProxyPort = client.getLocalSocksProxyPort();
+                    int localSocksProxyPort = psiphon.getLocalSocksProxyPort();
                     runVpn(localSocksProxyPort);
                     mStopSignal.await();
                 } catch (Utils.PsibotError e) {
@@ -87,7 +87,7 @@ public class Service extends VpnService {
                     Thread.currentThread().interrupt();
                 }
                 stopVpn();
-                client.stop();
+                psiphon.stop();
                 socketProtector.stop();
                 stopSelf();
             }
@@ -172,7 +172,7 @@ public class Service extends VpnService {
 
             if (vpnInterfaceFileDescriptor == null) {
                 // as per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29
-                throw new Utils.PsibotError("application is not prepared or is revoked");
+                throw new Utils.PsibotError(errorMessage + ": application is not prepared or is revoked");
             }
         } catch(IllegalArgumentException e) {
             throw new Utils.PsibotError(errorMessage, e);

+ 1 - 8
AndroidApp/app/src/main/java/ca/psiphon/psibot/Utils.java

@@ -50,21 +50,14 @@ public class Utils {
     public static class PsibotError extends Exception {
         private static final long serialVersionUID = 1L;
 
-        public PsibotError() {
-            super();
-        }
-
         public PsibotError(String message) {
             super(message);
         }
 
         public PsibotError(String message, Throwable cause) {
-            super(message, cause);
+            super(message + ": " + cause.getMessage());
         }
 
-        public PsibotError(Throwable cause) {
-            super(cause);
-        }
     }
 
     public static void writeRawResourceFile(

+ 47 - 0
AndroidApp/app/src/main/java/go/Go.java

@@ -0,0 +1,47 @@
+package go;
+
+import android.content.Context;
+import android.os.Looper;
+import android.util.Log;
+
+// Go is an entry point for libraries compiled in Go.
+// In an app's Application.onCreate, call:
+//
+// 	Go.init(getApplicationContext());
+//
+// When the function returns, it is safe to start calling
+// Go code.
+public final class Go {
+	// init loads libgojni.so and starts the runtime.
+	public static void init(Context context) {
+		if (Looper.myLooper() != Looper.getMainLooper()) {
+			Log.wtf("Go", "Go.init must be called from main thread (looper="+Looper.myLooper().toString()+")");
+		}
+		if (running) {
+			return;
+		}
+		running = true;
+
+		// TODO(crawshaw): setenv TMPDIR to context.getCacheDir().getAbsolutePath()
+		// TODO(crawshaw): context.registerComponentCallbacks for runtime.GC
+
+		System.loadLibrary("gojni");
+
+		new Thread("GoMain") {
+			public void run() {
+				Go.run();
+			}
+		}.start();
+
+		Go.waitForRun();
+
+        new Thread("GoReceive") {
+            public void run() { Seq.receive(); }
+        }.start();
+	}
+
+	private static boolean running = false;
+
+	private static native void run();
+	private static native void waitForRun();
+}

+ 251 - 0
AndroidApp/app/src/main/java/go/Seq.java

@@ -0,0 +1,251 @@
+package go;
+
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+// Seq is a sequence of machine-dependent encoded values.
+// Used by automatically generated language bindings to talk to Go.
+public class Seq {
+	@SuppressWarnings("UnusedDeclaration")
+	private long memptr; // holds C-allocated pointer
+
+	public Seq() {
+		ensure(64);
+	}
+
+	// Ensure that at least size bytes can be written to the Seq.
+	// Any existing data in the buffer is preserved.
+	public native void ensure(int size);
+
+	// Moves the internal buffer offset back to zero.
+	// Length and contents are maintained. Data can be read after a reset.
+	public native void resetOffset();
+
+	public native void log(String label);
+
+	public native byte readInt8();
+	public native short readInt16();
+	public native int readInt32();
+	public native long readInt64();
+	public long readInt() { return readInt64(); }
+
+	public native float readFloat32();
+	public native double readFloat64();
+	public native String readUTF16();
+	public native byte[] readByteArray();
+
+	public native void writeInt8(byte v);
+	public native void writeInt16(short v);
+	public native void writeInt32(int v);
+	public native void writeInt64(long v);
+	public void writeInt(long v) { writeInt64(v); }
+
+	public native void writeFloat32(float v);
+	public native void writeFloat64(double v);
+	public native void writeUTF16(String v);
+	public native void writeByteArray(byte[] v);
+
+	public void writeRef(Ref ref) {
+		writeInt32(ref.refnum);
+	}
+
+	public Ref readRef() {
+		int refnum = readInt32();
+		return tracker.get(refnum);
+	}
+
+	// Informs the Go ref tracker that Java is done with this ref.
+	static native void destroyRef(int refnum);
+
+	// createRef creates a Ref to a Java object.
+	public static Ref createRef(Seq.Object o) {
+		return tracker.createRef(o);
+	}
+
+	// sends a function invocation request to Go.
+	//
+	// Blocks until the function completes.
+	// If the request is for a method, the first element in src is
+	// a Ref to the receiver.
+	public static native void send(String descriptor, int code, Seq src, Seq dst);
+
+	// recv returns the next request from Go for a Java call.
+	static native void recv(Seq in, Receive params);
+
+	// recvRes sends the result of a Java call back to Go.
+	static native void recvRes(int handle, Seq out);
+
+	static final class Receive {
+		int refnum;
+		int code;
+		int handle;
+	}
+
+	protected void finalize() throws Throwable {
+		super.finalize();
+		free();
+	}
+	private native void free();
+
+	private static final ExecutorService receivePool = Executors.newCachedThreadPool();
+
+	// receive listens for callback requests from Go, invokes them on a thread
+	// pool and sends the responses.
+	public static void receive() {
+		Seq.Receive params = new Seq.Receive();
+		while (true) {
+			final Seq in = new Seq();
+			Seq.recv(in, params);
+
+			final int code = params.code;
+			final int handle = params.handle;
+			final int refnum = params.refnum;
+
+			if (code == -1) {
+				// Special signal from seq.FinalizeRef.
+				tracker.dec(refnum);
+				Seq out = new Seq();
+				Seq.recvRes(handle, out);
+				continue;
+			}
+
+			receivePool.execute(new Runnable() {
+				public void run() {
+					Ref r = tracker.get(refnum);
+					Seq out = new Seq();
+					r.obj.call(code, in, out);
+					Seq.recvRes(handle, out);
+				}
+			});
+		}
+	}
+
+	// An Object is a Java object that matches a Go object.
+	// The implementation of the object may be in either Java or Go,
+	// with a proxy instance in the other language passing calls
+	// through to the other language.
+	//
+	// Don't implement an Object directly. Instead, look for the
+	// generated abstract Stub.
+	public interface Object {
+		public Ref ref();
+		public void call(int code, Seq in, Seq out);
+	}
+
+	// A Ref is an object tagged with an integer for passing back and
+	// forth across the language boundary.
+	//
+	// A Ref may represent either an instance of a Java Object subclass,
+	// or an instance of a Go object. The explicit allocation of a Ref
+	// is used to pin Go object instances when they are passed to Java.
+	// The Go Seq library maintains a reference to the instance in a map
+	// keyed by the Ref number. When the JVM calls finalize, we ask Go
+	// to clear the entry in the map.
+	public static final class Ref {
+		// ref < 0: Go object tracked by Java
+		// ref > 0: Java object tracked by Go
+		int refnum;
+		public Seq.Object obj;
+
+		private Ref(int refnum, Seq.Object o) {
+			this.refnum = refnum;
+			this.obj = o;
+			tracker.inc(refnum);
+		}
+
+		@Override
+		protected void finalize() throws Throwable {
+			tracker.dec(refnum);
+			super.finalize();
+		}
+	}
+
+	static final RefTracker tracker = new RefTracker();
+
+	static final class RefTracker {
+		// Next Java object reference number.
+		//
+		// Reference numbers are positive for Java objects,
+		// and start, arbitrarily at a different offset to Go
+		// to make debugging by reading Seq hex a little easier.
+		private int next = 42; // next Java object ref
+
+		// TODO(crawshaw): We could cut down allocations for frequently
+		// sent Go objects by maintaining a map to weak references. This
+		// however, would require allocating two objects per reference
+		// instead of one. It also introduces weak references, the bane
+		// of any Java debugging session.
+		//
+		// When we have real code, examine the tradeoffs.
+
+		// Number of active references to a Go object. refnum -> count
+		private SparseIntArray goObjs = new SparseIntArray();
+
+		// Java objects that have been passed to Go. refnum -> Ref
+		// The Ref obj field is non-null.
+		// This map pins Java objects so they don't get GCed while the
+		// only reference to them is held by Go code.
+		private SparseArray<Ref> javaObjs = new SparseArray<Ref>();
+
+		// inc increments the reference count to a Go object.
+		synchronized void inc(int refnum) {
+			if (refnum > 0) {
+				return; // we don't count java objects
+			}
+			int count = goObjs.get(refnum);
+			if (count == Integer.MAX_VALUE) {
+				throw new RuntimeException("refnum " + refnum + " overflow");
+			}
+			goObjs.put(refnum, count+1);
+		}
+
+		// dec decrements the reference count to a Go object.
+		// If the count reaches zero, the Go reference tracker is informed.
+		synchronized void dec(int refnum) {
+			if (refnum > 0) {
+				// Java objects are removed on request of Go.
+				javaObjs.remove(refnum);
+				return;
+			}
+			int count = goObjs.get(refnum);
+			if (count == 0) {
+				throw new RuntimeException("refnum " + refnum + " underflow");
+			}
+			count--;
+			if (count <= 0) {
+				goObjs.delete(refnum);
+				Seq.destroyRef(refnum);
+			} else {
+				goObjs.put(refnum, count);
+			}
+		}
+
+		synchronized Ref createRef(Seq.Object o) {
+			// TODO(crawshaw): use single Ref for null.
+			if (next == Integer.MAX_VALUE) {
+				throw new RuntimeException("createRef overflow for " + o);
+			}
+			int refnum = next++;
+			Ref ref = new Ref(refnum, o);
+			javaObjs.put(refnum, ref);
+			return ref;
+		}
+
+		// get returns an existing Ref to either a Java or Go object.
+		// It may be the first time we have seen the Go object.
+		synchronized Ref get(int refnum) {
+			if (refnum > 0) {
+				Ref ref = javaObjs.get(refnum);
+				if (ref == null) {
+					throw new RuntimeException("unknown java Ref: "+refnum);
+				}
+				return ref;
+			}
+			return new Ref(refnum, null);
+		}
+	}
+}

+ 84 - 0
AndroidApp/app/src/main/java/go/psi/Psi.java

@@ -0,0 +1,84 @@
+// Java Package psi is a proxy for talking to a Go program.
+//   gobind -lang=java github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/psi
+//
+// File is generated by gobind. Do not edit.
+package go.psi;
+
+import go.Seq;
+
+public abstract class Psi {
+    private Psi() {} // uninstantiable
+    
+    public interface Listener extends go.Seq.Object {
+        public void Message(String message);
+        
+        public static abstract class Stub implements Listener {
+            static final String DESCRIPTOR = "go.psi.Listener";
+            
+            private final go.Seq.Ref ref;
+            public Stub() {
+                ref = go.Seq.createRef(this);
+            }
+            
+            public go.Seq.Ref ref() { return ref; }
+            
+            public void call(int code, go.Seq in, go.Seq out) {
+                switch (code) {
+                case Proxy.CALL_Message: {
+                    String param_message = in.readUTF16();
+                    this.Message(param_message);
+                    return;
+                }
+                default:
+                    throw new RuntimeException("unknown code: "+ code);
+                }
+            }
+        }
+        
+        static final class Proxy implements Listener {
+            static final String DESCRIPTOR = Stub.DESCRIPTOR;
+        
+            private go.Seq.Ref ref;
+        
+            Proxy(go.Seq.Ref ref) { this.ref = ref; }
+        
+            public go.Seq.Ref ref() { return ref; }
+        
+            public void call(int code, go.Seq in, go.Seq out) {
+                throw new RuntimeException("cycle: cannot call proxy");
+            }
+        
+            public void Message(String message) {
+                go.Seq _in = new go.Seq();
+                go.Seq _out = new go.Seq();
+                _in.writeRef(ref);
+                _in.writeUTF16(message);
+                Seq.send(DESCRIPTOR, CALL_Message, _in, _out);
+            }
+            
+            static final int CALL_Message = 0x10a;
+        }
+    }
+    
+    public static void Start(String configJson, Listener listener) throws Exception {
+        go.Seq _in = new go.Seq();
+        go.Seq _out = new go.Seq();
+        _in.writeUTF16(configJson);
+        _in.writeRef(listener.ref());
+        Seq.send(DESCRIPTOR, CALL_Start, _in, _out);
+        String _err = _out.readUTF16();
+        if (_err != null) {
+            throw new Exception(_err);
+        }
+    }
+    
+    public static void Stop() {
+        go.Seq _in = new go.Seq();
+        go.Seq _out = new go.Seq();
+        Seq.send(DESCRIPTOR, CALL_Stop, _in, _out);
+    }
+    
+    private static final int CALL_Start = 1;
+    private static final int CALL_Stop = 2;
+    private static final String DESCRIPTOR = "psi";
+}

BIN
AndroidApp/app/src/main/res/raw/psiphon_tunnel_core_arm → AndroidApp/app/src/main/jniLibs/armeabi-v7a/libgojni.so


+ 4 - 0
AndroidApp/app/src/main/res/raw/psiphon_config → AndroidApp/app/src/main/res/raw/psiphon_config_stub

@@ -3,12 +3,16 @@
     "SponsorId" : "<placeholder>",
     "RemoteServerListUrl" : "<placeholder>",
     "RemoteServerListSignaturePublicKey" : "<placeholder>",
+    "DataStoreFilename" : "/data/data/ca.psiphon.psibot/psiphon.db",
     "LogFilename" : "",
     "LocalHttpProxyPort" : 0,
     "LocalSocksProxyPort" : 0,
     "EgressRegion" : "",
     "TunnelProtocol" : "",
     "ConnectionWorkerPoolSize" : 10,
+    "TunnelPoolSize" : 1,
+    "PortForwardFailureThreshold" : 10,
+    "UpstreamHttpProxyAddress" : "",
     "BindToDeviceServiceAddress" : "",
     "BindToDeviceDnsServer" : "8.8.4.4"
 }

+ 1 - 1
AndroidApp/build.gradle

@@ -5,7 +5,7 @@ buildscript {
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:0.12.2'
+        classpath 'com.android.tools.build:gradle:1.0.0-rc1'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files

+ 2 - 2
AndroidApp/gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,6 @@
-#Wed Apr 10 15:27:10 PDT 2013
+#Thu Dec 04 12:03:49 EST 2014
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip

+ 78 - 0
AndroidLibrary/README.md

@@ -0,0 +1,78 @@
+Psiphon Library for Android README
+================================================================================
+
+Overview
+--------------------------------------------------------------------------------
+
+Psiphon Library for Android enables you to easily embed Psiphon in your Android
+app. The Psiphon Library for Android is implemented in Go and follows the standard
+conventions for using a Go library in an Android app.
+
+Status
+--------------------------------------------------------------------------------
+
+* Pre-release
+
+Building From Source
+--------------------------------------------------------------------------------
+
+Follow Go Android documentation:
+* [Overview README](https://code.google.com/p/go/source/browse/README?repo=mobile)
+* [Sample JNI App README](https://code.google.com/p/go/source/browse/example/libhello/README?repo=mobile)
+* [gobind documentation](http://godoc.org/golang.org/x/mobile/cmd/gobind)
+
+```
+/AndroidLibrary
+  README.md                - this file
+  libgojni.so              - build binary output
+  /psi
+    psi.go                 - main library source
+  /go_psi
+    go_psi.go              - gobind output
+  /java_psi/go/psi
+    Psi.java               - gobind output
+  /java_golang/go
+    Go.java                - fork of Go/Java integration file
+    Seq.java               - fork of Go/Java integration file
+  /libpsi
+    main.go                - stub main package for library
+```
+
+NOTE: may change after Go 1.4 is released.
+
+* Requires Go 1.4 or later
+  * At this time, android/arm support is not yet released but.
+  * Install Go from source. The Android instructions are here:
+[https://code.google.com/p/go/source/browse/README?repo=mobile](https://code.google.com/p/go/source/browse/README?repo=mobile).
+  * In summary, download and install the Android NDK, use a script to make a [standalone toolchain](https://developer.android.com/tools/sdk/ndk/index.html#Docs), and use that toolchain to build android/arm support within the Go source install. Then cross compile as usual.
+* `$GOPATH/bin/gobind -lang=go github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/psi > go_psi/go_psi.go`
+* `$GOPATH/bin/gobind -lang=java github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/psi > java_psi/go/psi/Psi.java`
+* In `/libpsi` `CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 go build -ldflags="-shared"` and copy output file to `gojni.so`
+
+Using
+--------------------------------------------------------------------------------
+
+1. Build from source or use the provided shared object binary and Java source files
+1. Add Go/Java integration files `java_golang/go/*.java` to your `$src/go`
+1. Add `java_psi/go/psi/Psi.java` to your `$src/go/psi`
+1. Add `gojni.so
+
+NOTE: may change to Psiphon-specific library name and init.
+
+[AndroidApp README](../AndroidApp/README.md)
+
+See sample usage in [Psiphon.java](../AndroidApp/app/src/main/java/ca/psiphon/psibot/Psiphon.java). Uses `gobind` conventions for data passing.
+
+1. Embed a [config file](../README.md#setup)
+1. Call `Go.init(getApplicationContext());` in `Application.onCreate()`
+1. Extend `Psi.Listener.Stub` to receive messages in `Message(String line)`
+1. Call `Psi.Start(configFile, Psi.Listener)` to start Psiphon. Catch `Exception` to receive errors.
+1. Call `Psi.Stop()` to stop Psiphon.
+1. Sample shows how to monitor messages and detect which proxy ports to use and when the tunnel is active.
+
+NOTE: may add more explicit interface for state change events.
+
+Limitations
+--------------------------------------------------------------------------------
+
+* Only supports one concurrent instance of Psiphon.

+ 49 - 0
AndroidLibrary/go_psi/go_psi.go

@@ -0,0 +1,49 @@
+// Package go_psi is an autogenerated binder stub for package psi.
+//   gobind -lang=go github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/psi
+//
+// File is generated by gobind. Do not edit.
+package go_psi
+
+import (
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/psi"
+	"golang.org/x/mobile/bind/seq"
+)
+
+const (
+	proxyListenerDescriptor  = "go.psi.Listener"
+	proxyListenerMessageCode = 0x10a
+)
+
+type proxyListener seq.Ref
+
+func (p *proxyListener) Message(message string) {
+	out := new(seq.Buffer)
+	out.WriteUTF16(message)
+	seq.Transact((*seq.Ref)(p), proxyListenerMessageCode, out)
+}
+
+func proxy_Start(out, in *seq.Buffer) {
+	param_configJson := in.ReadUTF16()
+	var param_listener psi.Listener
+	param_listener_ref := in.ReadRef()
+	if param_listener_ref.Num < 0 {
+		param_listener = param_listener_ref.Get().(psi.Listener)
+	} else {
+		param_listener = (*proxyListener)(param_listener_ref)
+	}
+	err := psi.Start(param_configJson, param_listener)
+	if err == nil {
+		out.WriteUTF16("")
+	} else {
+		out.WriteUTF16(err.Error())
+	}
+}
+
+func proxy_Stop(out, in *seq.Buffer) {
+	psi.Stop()
+}
+
+func init() {
+	seq.Register("psi", 1, proxy_Start)
+	seq.Register("psi", 2, proxy_Stop)
+}

+ 28 - 0
AndroidLibrary/java_golang/LICENSE

@@ -0,0 +1,28 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+

+ 47 - 0
AndroidLibrary/java_golang/go/Go.java

@@ -0,0 +1,47 @@
+package go;
+
+import android.content.Context;
+import android.os.Looper;
+import android.util.Log;
+
+// Go is an entry point for libraries compiled in Go.
+// In an app's Application.onCreate, call:
+//
+// 	Go.init(getApplicationContext());
+//
+// When the function returns, it is safe to start calling
+// Go code.
+public final class Go {
+	// init loads libgojni.so and starts the runtime.
+	public static void init(Context context) {
+		if (Looper.myLooper() != Looper.getMainLooper()) {
+			Log.wtf("Go", "Go.init must be called from main thread (looper="+Looper.myLooper().toString()+")");
+		}
+		if (running) {
+			return;
+		}
+		running = true;
+
+		// TODO(crawshaw): setenv TMPDIR to context.getCacheDir().getAbsolutePath()
+		// TODO(crawshaw): context.registerComponentCallbacks for runtime.GC
+
+		System.loadLibrary("gojni");
+
+		new Thread("GoMain") {
+			public void run() {
+				Go.run();
+			}
+		}.start();
+
+		Go.waitForRun();
+
+        new Thread("GoReceive") {
+            public void run() { Seq.receive(); }
+        }.start();
+	}
+
+	private static boolean running = false;
+
+	private static native void run();
+	private static native void waitForRun();
+}

+ 251 - 0
AndroidLibrary/java_golang/go/Seq.java

@@ -0,0 +1,251 @@
+package go;
+
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+// Seq is a sequence of machine-dependent encoded values.
+// Used by automatically generated language bindings to talk to Go.
+public class Seq {
+	@SuppressWarnings("UnusedDeclaration")
+	private long memptr; // holds C-allocated pointer
+
+	public Seq() {
+		ensure(64);
+	}
+
+	// Ensure that at least size bytes can be written to the Seq.
+	// Any existing data in the buffer is preserved.
+	public native void ensure(int size);
+
+	// Moves the internal buffer offset back to zero.
+	// Length and contents are maintained. Data can be read after a reset.
+	public native void resetOffset();
+
+	public native void log(String label);
+
+	public native byte readInt8();
+	public native short readInt16();
+	public native int readInt32();
+	public native long readInt64();
+	public long readInt() { return readInt64(); }
+
+	public native float readFloat32();
+	public native double readFloat64();
+	public native String readUTF16();
+	public native byte[] readByteArray();
+
+	public native void writeInt8(byte v);
+	public native void writeInt16(short v);
+	public native void writeInt32(int v);
+	public native void writeInt64(long v);
+	public void writeInt(long v) { writeInt64(v); }
+
+	public native void writeFloat32(float v);
+	public native void writeFloat64(double v);
+	public native void writeUTF16(String v);
+	public native void writeByteArray(byte[] v);
+
+	public void writeRef(Ref ref) {
+		writeInt32(ref.refnum);
+	}
+
+	public Ref readRef() {
+		int refnum = readInt32();
+		return tracker.get(refnum);
+	}
+
+	// Informs the Go ref tracker that Java is done with this ref.
+	static native void destroyRef(int refnum);
+
+	// createRef creates a Ref to a Java object.
+	public static Ref createRef(Seq.Object o) {
+		return tracker.createRef(o);
+	}
+
+	// sends a function invocation request to Go.
+	//
+	// Blocks until the function completes.
+	// If the request is for a method, the first element in src is
+	// a Ref to the receiver.
+	public static native void send(String descriptor, int code, Seq src, Seq dst);
+
+	// recv returns the next request from Go for a Java call.
+	static native void recv(Seq in, Receive params);
+
+	// recvRes sends the result of a Java call back to Go.
+	static native void recvRes(int handle, Seq out);
+
+	static final class Receive {
+		int refnum;
+		int code;
+		int handle;
+	}
+
+	protected void finalize() throws Throwable {
+		super.finalize();
+		free();
+	}
+	private native void free();
+
+	private static final ExecutorService receivePool = Executors.newCachedThreadPool();
+
+	// receive listens for callback requests from Go, invokes them on a thread
+	// pool and sends the responses.
+	public static void receive() {
+		Seq.Receive params = new Seq.Receive();
+		while (true) {
+			final Seq in = new Seq();
+			Seq.recv(in, params);
+
+			final int code = params.code;
+			final int handle = params.handle;
+			final int refnum = params.refnum;
+
+			if (code == -1) {
+				// Special signal from seq.FinalizeRef.
+				tracker.dec(refnum);
+				Seq out = new Seq();
+				Seq.recvRes(handle, out);
+				continue;
+			}
+
+			receivePool.execute(new Runnable() {
+				public void run() {
+					Ref r = tracker.get(refnum);
+					Seq out = new Seq();
+					r.obj.call(code, in, out);
+					Seq.recvRes(handle, out);
+				}
+			});
+		}
+	}
+
+	// An Object is a Java object that matches a Go object.
+	// The implementation of the object may be in either Java or Go,
+	// with a proxy instance in the other language passing calls
+	// through to the other language.
+	//
+	// Don't implement an Object directly. Instead, look for the
+	// generated abstract Stub.
+	public interface Object {
+		public Ref ref();
+		public void call(int code, Seq in, Seq out);
+	}
+
+	// A Ref is an object tagged with an integer for passing back and
+	// forth across the language boundary.
+	//
+	// A Ref may represent either an instance of a Java Object subclass,
+	// or an instance of a Go object. The explicit allocation of a Ref
+	// is used to pin Go object instances when they are passed to Java.
+	// The Go Seq library maintains a reference to the instance in a map
+	// keyed by the Ref number. When the JVM calls finalize, we ask Go
+	// to clear the entry in the map.
+	public static final class Ref {
+		// ref < 0: Go object tracked by Java
+		// ref > 0: Java object tracked by Go
+		int refnum;
+		public Seq.Object obj;
+
+		private Ref(int refnum, Seq.Object o) {
+			this.refnum = refnum;
+			this.obj = o;
+			tracker.inc(refnum);
+		}
+
+		@Override
+		protected void finalize() throws Throwable {
+			tracker.dec(refnum);
+			super.finalize();
+		}
+	}
+
+	static final RefTracker tracker = new RefTracker();
+
+	static final class RefTracker {
+		// Next Java object reference number.
+		//
+		// Reference numbers are positive for Java objects,
+		// and start, arbitrarily at a different offset to Go
+		// to make debugging by reading Seq hex a little easier.
+		private int next = 42; // next Java object ref
+
+		// TODO(crawshaw): We could cut down allocations for frequently
+		// sent Go objects by maintaining a map to weak references. This
+		// however, would require allocating two objects per reference
+		// instead of one. It also introduces weak references, the bane
+		// of any Java debugging session.
+		//
+		// When we have real code, examine the tradeoffs.
+
+		// Number of active references to a Go object. refnum -> count
+		private SparseIntArray goObjs = new SparseIntArray();
+
+		// Java objects that have been passed to Go. refnum -> Ref
+		// The Ref obj field is non-null.
+		// This map pins Java objects so they don't get GCed while the
+		// only reference to them is held by Go code.
+		private SparseArray<Ref> javaObjs = new SparseArray<Ref>();
+
+		// inc increments the reference count to a Go object.
+		synchronized void inc(int refnum) {
+			if (refnum > 0) {
+				return; // we don't count java objects
+			}
+			int count = goObjs.get(refnum);
+			if (count == Integer.MAX_VALUE) {
+				throw new RuntimeException("refnum " + refnum + " overflow");
+			}
+			goObjs.put(refnum, count+1);
+		}
+
+		// dec decrements the reference count to a Go object.
+		// If the count reaches zero, the Go reference tracker is informed.
+		synchronized void dec(int refnum) {
+			if (refnum > 0) {
+				// Java objects are removed on request of Go.
+				javaObjs.remove(refnum);
+				return;
+			}
+			int count = goObjs.get(refnum);
+			if (count == 0) {
+				throw new RuntimeException("refnum " + refnum + " underflow");
+			}
+			count--;
+			if (count <= 0) {
+				goObjs.delete(refnum);
+				Seq.destroyRef(refnum);
+			} else {
+				goObjs.put(refnum, count);
+			}
+		}
+
+		synchronized Ref createRef(Seq.Object o) {
+			// TODO(crawshaw): use single Ref for null.
+			if (next == Integer.MAX_VALUE) {
+				throw new RuntimeException("createRef overflow for " + o);
+			}
+			int refnum = next++;
+			Ref ref = new Ref(refnum, o);
+			javaObjs.put(refnum, ref);
+			return ref;
+		}
+
+		// get returns an existing Ref to either a Java or Go object.
+		// It may be the first time we have seen the Go object.
+		synchronized Ref get(int refnum) {
+			if (refnum > 0) {
+				Ref ref = javaObjs.get(refnum);
+				if (ref == null) {
+					throw new RuntimeException("unknown java Ref: "+refnum);
+				}
+				return ref;
+			}
+			return new Ref(refnum, null);
+		}
+	}
+}

+ 84 - 0
AndroidLibrary/java_psi/go/psi/Psi.java

@@ -0,0 +1,84 @@
+// Java Package psi is a proxy for talking to a Go program.
+//   gobind -lang=java github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/psi
+//
+// File is generated by gobind. Do not edit.
+package go.psi;
+
+import go.Seq;
+
+public abstract class Psi {
+    private Psi() {} // uninstantiable
+    
+    public interface Listener extends go.Seq.Object {
+        public void Message(String message);
+        
+        public static abstract class Stub implements Listener {
+            static final String DESCRIPTOR = "go.psi.Listener";
+            
+            private final go.Seq.Ref ref;
+            public Stub() {
+                ref = go.Seq.createRef(this);
+            }
+            
+            public go.Seq.Ref ref() { return ref; }
+            
+            public void call(int code, go.Seq in, go.Seq out) {
+                switch (code) {
+                case Proxy.CALL_Message: {
+                    String param_message = in.readUTF16();
+                    this.Message(param_message);
+                    return;
+                }
+                default:
+                    throw new RuntimeException("unknown code: "+ code);
+                }
+            }
+        }
+        
+        static final class Proxy implements Listener {
+            static final String DESCRIPTOR = Stub.DESCRIPTOR;
+        
+            private go.Seq.Ref ref;
+        
+            Proxy(go.Seq.Ref ref) { this.ref = ref; }
+        
+            public go.Seq.Ref ref() { return ref; }
+        
+            public void call(int code, go.Seq in, go.Seq out) {
+                throw new RuntimeException("cycle: cannot call proxy");
+            }
+        
+            public void Message(String message) {
+                go.Seq _in = new go.Seq();
+                go.Seq _out = new go.Seq();
+                _in.writeRef(ref);
+                _in.writeUTF16(message);
+                Seq.send(DESCRIPTOR, CALL_Message, _in, _out);
+            }
+            
+            static final int CALL_Message = 0x10a;
+        }
+    }
+    
+    public static void Start(String configJson, Listener listener) throws Exception {
+        go.Seq _in = new go.Seq();
+        go.Seq _out = new go.Seq();
+        _in.writeUTF16(configJson);
+        _in.writeRef(listener.ref());
+        Seq.send(DESCRIPTOR, CALL_Start, _in, _out);
+        String _err = _out.readUTF16();
+        if (_err != null) {
+            throw new Exception(_err);
+        }
+    }
+    
+    public static void Stop() {
+        go.Seq _in = new go.Seq();
+        go.Seq _out = new go.Seq();
+        Seq.send(DESCRIPTOR, CALL_Stop, _in, _out);
+    }
+    
+    private static final int CALL_Start = 1;
+    private static final int CALL_Stop = 2;
+    private static final String DESCRIPTOR = "psi";
+}

BIN
AndroidLibrary/libgojni.so


+ 20 - 0
AndroidLibrary/libpsi/main.go

@@ -0,0 +1,20 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is the Go entry point for the libpsi app.
+// It is invoked from Java.
+//
+// See README for details.
+package main
+
+import (
+	"golang.org/x/mobile/app"
+
+	_ "github.com/Psiphon-Labs/psiphon-tunnel-core/AndroidLibrary/go_psi"
+	_ "golang.org/x/mobile/bind/java"
+)
+
+func main() {
+	app.Run(app.Callbacks{})
+}

+ 91 - 0
AndroidLibrary/psi/psi.go

@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package psi
+
+// This package is a shim between Java and the "psiphon" package. Due to limitations
+// on what Go types may be exposed (http://godoc.org/golang.org/x/mobile/cmd/gobind),
+// a psiphon.Controller cannot be directly used by Java. This shim exposes a trivial
+// Start/Stop interface on top of a single Controller instance.
+
+import (
+	"fmt"
+	"log"
+	"sync"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+)
+
+type Listener interface {
+	Message(message string)
+}
+
+type logRelay struct {
+	listener Listener
+}
+
+func (lr *logRelay) Write(p []byte) (n int, err error) {
+	// TODO: buffer incomplete lines
+	lr.listener.Message(string(p))
+	return len(p), nil
+}
+
+var controller *psiphon.Controller
+var shutdownBroadcast chan struct{}
+var controllerWaitGroup *sync.WaitGroup
+
+func Start(configJson string, listener Listener) error {
+
+	if controller != nil {
+		return fmt.Errorf("already started")
+	}
+
+	config, err := psiphon.LoadConfig([]byte(configJson))
+	if err != nil {
+		return fmt.Errorf("error loading configuration file: %s", err)
+	}
+
+	err = psiphon.InitDataStore(config.DataStoreFilename)
+	if err != nil {
+		return fmt.Errorf("error initializing datastore: %s", err)
+	}
+
+	log.SetOutput(&logRelay{listener: listener})
+
+	controller = psiphon.NewController(config)
+	shutdownBroadcast = make(chan struct{})
+	controllerWaitGroup = new(sync.WaitGroup)
+	controllerWaitGroup.Add(1)
+	go func() {
+		defer controllerWaitGroup.Done()
+		controller.Run(shutdownBroadcast)
+	}()
+
+	return nil
+}
+
+func Stop() {
+	if controller != nil {
+		close(shutdownBroadcast)
+		controllerWaitGroup.Wait()
+		controller = nil
+		shutdownBroadcast = nil
+		controllerWaitGroup = nil
+	}
+}

+ 13 - 2
psiphonClient.go → ConsoleClient/psiphonClient.go

@@ -21,11 +21,13 @@ package main
 
 import (
 	"flag"
-	psiphon "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"io/ioutil"
 	"log"
 	"os"
 	"os/signal"
 	"sync"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
 )
 
 func main() {
@@ -36,10 +38,19 @@ func main() {
 	if configFilename == "" {
 		log.Fatalf("configuration file is required")
 	}
-	config, err := psiphon.LoadConfig(configFilename)
+	configFileContents, err := ioutil.ReadFile(configFilename)
 	if err != nil {
 		log.Fatalf("error loading configuration file: %s", err)
 	}
+	config, err := psiphon.LoadConfig(configFileContents)
+	if err != nil {
+		log.Fatalf("error processing configuration file: %s", err)
+	}
+
+	err = psiphon.InitDataStore(config.DataStoreFilename)
+	if err != nil {
+		return fmt.Errorf("error initializing datastore: %s", err)
+	}
 
 	if config.LogFilename != "" {
 		logFile, err := os.OpenFile(config.LogFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)

+ 1 - 0
README.md

@@ -28,6 +28,7 @@ Setup
         "SponsorId" : "<placeholder>",
         "RemoteServerListUrl" : "<placeholder>",
         "RemoteServerListSignaturePublicKey" : "<placeholder>",
+        "DataStoreFilename" : "",
         "LogFilename" : "",
         "LocalHttpProxyPort" : 0,
         "LocalSocksProxyPort" : 0,

+ 9 - 9
psiphon/config.go

@@ -22,11 +22,11 @@ package psiphon
 import (
 	"encoding/json"
 	"errors"
-	"io/ioutil"
 )
 
 type Config struct {
 	LogFilename                        string
+	DataStoreFilename                  string
 	PropagationChannelId               string
 	SponsorId                          string
 	RemoteServerListUrl                string
@@ -46,15 +46,11 @@ type Config struct {
 	UpstreamHttpProxyAddress           string
 }
 
-// LoadConfig reads, and parse, and validates a JSON format Psiphon config
-// file and returns a Config struct populated with config values.
-func LoadConfig(filename string) (*Config, error) {
-	fileContents, err := ioutil.ReadFile(filename)
-	if err != nil {
-		return nil, ContextError(err)
-	}
+// LoadConfig parses and validates a JSON format Psiphon config JSON
+// string and returns a Config struct populated with config values.
+func LoadConfig(configJson []byte) (*Config, error) {
 	var config Config
-	err = json.Unmarshal(fileContents, &config)
+	err := json.Unmarshal(configJson, &config)
 	if err != nil {
 		return nil, ContextError(err)
 	}
@@ -77,6 +73,10 @@ func LoadConfig(filename string) (*Config, error) {
 			errors.New("remote server list signature public key is missing from the configuration file"))
 	}
 
+	if config.DataStoreFilename == "" {
+		config.DataStoreFilename = DATA_STORE_FILENAME
+	}
+
 	if config.TunnelProtocol != "" {
 		if !Contains(SupportedTunnelProtocols, config.TunnelProtocol) {
 			return nil, ContextError(

+ 3 - 3
psiphon/controller.go

@@ -266,7 +266,7 @@ func (controller *Controller) registerTunnel(tunnel *Tunnel) bool {
 		}
 	}
 	controller.tunnels = append(controller.tunnels, tunnel)
-	Notice(NOTICE_TUNNEL, "%d tunnels", len(controller.tunnels))
+	Notice(NOTICE_TUNNELS, "%d", len(controller.tunnels))
 	return true
 }
 
@@ -294,7 +294,7 @@ func (controller *Controller) terminateTunnel(tunnel *Tunnel) {
 				controller.nextTunnel = 0
 			}
 			activeTunnel.Close()
-			Notice(NOTICE_TUNNEL, "%d tunnels", len(controller.tunnels))
+			Notice(NOTICE_TUNNELS, "%d", len(controller.tunnels))
 			break
 		}
 	}
@@ -310,7 +310,7 @@ func (controller *Controller) terminateAllTunnels() {
 	}
 	controller.tunnels = make([]*Tunnel, 0)
 	controller.nextTunnel = 0
-	Notice(NOTICE_TUNNEL, "%d tunnels", len(controller.tunnels))
+	Notice(NOTICE_TUNNELS, "%d", len(controller.tunnels))
 }
 
 // getNextActiveTunnel returns the next tunnel from the pool of active

+ 27 - 11
psiphon/dataStore.go

@@ -37,10 +37,15 @@ type dataStore struct {
 
 var singleton dataStore
 
-// initDataStore initializes the singleton instance of dataStore. This
+// InitDataStore initializes the singleton instance of dataStore. This
 // function uses a sync.Once and is safe for use by concurrent goroutines.
 // The underlying sql.DB connection pool is also safe.
-func initDataStore() {
+//
+// Note: the sync.Once was more useful when initDataStore was private and
+// called on-demand by the public functions below. Now we require an explicit
+// InitDataStore() call with the filename passed in. The on-demand calls
+// have been replaced by checkInitDataStore() to assert that Init was called.
+func InitDataStore(filename string) (err error) {
 	singleton.init.Do(func() {
 		const schema = `
         create table if not exists serverEntry
@@ -57,18 +62,29 @@ func initDataStore() {
              value text not null);
         pragma journal_mode=WAL;
         `
-		db, err := sql.Open(
+		var db *sql.DB
+		db, err = sql.Open(
 			"sqlite3",
-			fmt.Sprintf("file:%s?cache=private&mode=rwc", DATA_STORE_FILENAME))
+			fmt.Sprintf("file:%s?cache=private&mode=rwc", filename))
 		if err != nil {
-			Fatal("initDataStore failed to open database: %s", err)
+			// Note: intending to set the err return value for InitDataStore
+			err = fmt.Errorf("initDataStore failed to open database: %s", err)
+			return
 		}
 		_, err = db.Exec(schema)
 		if err != nil {
-			Fatal("initDataStore failed to initialize schema: %s", err)
+			err = fmt.Errorf("initDataStore failed to initialize schema: %s", err)
+			return
 		}
 		singleton.db = db
 	})
+	return err
+}
+
+func checkInitDataStore() {
+	if singleton.db == nil {
+		panic("checkInitDataStore: datastore not initialized")
+	}
 }
 
 func canRetry(err error) bool {
@@ -82,7 +98,7 @@ func canRetry(err error) bool {
 // transactionWithRetry will retry a write transaction if sqlite3
 // reports a table is locked by another writer.
 func transactionWithRetry(updater func(*sql.Tx) error) error {
-	initDataStore()
+	checkInitDataStore()
 	for i := 0; i < 10; i++ {
 		if i > 0 {
 			// Delay on retry
@@ -221,7 +237,7 @@ type ServerEntryIterator struct {
 
 // NewServerEntryIterator creates a new NewServerEntryIterator
 func NewServerEntryIterator(region, protocol string) (iterator *ServerEntryIterator, err error) {
-	initDataStore()
+	checkInitDataStore()
 	iterator = &ServerEntryIterator{
 		region:   region,
 		protocol: protocol,
@@ -338,7 +354,7 @@ func makeServerEntryWhereClause(
 // least one server entry (for the specified region and/or protocol,
 // when not blank).
 func HasServerEntries(region, protocol string) bool {
-	initDataStore()
+	checkInitDataStore()
 	var count int
 	whereClause, whereParams := makeServerEntryWhereClause(region, protocol, nil)
 	query := "select count(*) from serverEntry" + whereClause
@@ -364,7 +380,7 @@ func HasServerEntries(region, protocol string) bool {
 // GetServerEntryIpAddresses returns an array containing
 // all stored server IP addresses.
 func GetServerEntryIpAddresses() (ipAddresses []string, err error) {
-	initDataStore()
+	checkInitDataStore()
 	ipAddresses = make([]string, 0)
 	rows, err := singleton.db.Query("select id from serverEntry;")
 	if err != nil {
@@ -403,7 +419,7 @@ func SetKeyValue(key, value string) error {
 // GetKeyValue retrieves the value for a given key. If not found,
 // it returns an empty string value.
 func GetKeyValue(key string) (value string, err error) {
-	initDataStore()
+	checkInitDataStore()
 	rows := singleton.db.QueryRow("select value from keyValue where key = ?;", key)
 	err = rows.Scan(&value)
 	if err == sql.ErrNoRows {

+ 1 - 1
psiphon/defaults.go

@@ -24,7 +24,7 @@ import (
 )
 
 const (
-	VERSION                                  = "0.0.3"
+	VERSION                                  = "0.0.4"
 	DATA_STORE_FILENAME                      = "psiphon.db"
 	CONNECTION_WORKER_POOL_SIZE              = 10
 	TUNNEL_POOL_SIZE                         = 1

+ 1 - 1
psiphon/notice.go

@@ -28,7 +28,7 @@ const (
 	NOTICE_INFO            = "INFO"
 	NOTICE_ALERT           = "ALERT"
 	NOTICE_VERSION         = "VERSION"
-	NOTICE_TUNNEL          = "TUNNEL"
+	NOTICE_TUNNELS         = "TUNNELS"
 	NOTICE_SOCKS_PROXY     = "SOCKS-PROXY"
 	NOTICE_HTTP_PROXY      = "HTTP-PROXY"
 	NOTICE_UPGRADE         = "UPGRADE"