Просмотр исходного кода

Add AndroidLibrary, a Java/Go binding that enables use of psiphon-tunnel-core within an Android app; modify sample AndroidApp to use AndroidLibrary (and remove embedded binary + subprocess method); move sample console app to its own subdir

Rod Hynes 11 лет назад
Родитель
Сommit
4419eb2d27

+ 3 - 10
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.
-
-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.
+Uses the [Psiphon Android Library](../AndroidLibrary/README.md).
 
 In `app\src\main\res\raw\psiphon_config`, placeholders must be replaced with valid values.

+ 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));
-    }
-}

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

@@ -0,0 +1,131 @@
+/*
+ * 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) {
+        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));
+    }
+
+    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();
+        }
+    }
+}

+ 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


+ 74 - 0
AndroidLibrary/README.md

@@ -0,0 +1,74 @@
+Psiphon Library for Android README
+================================================================================
+
+Overview
+--------------------------------------------------------------------------------
+
+Psiphon Library for Android enables you to easily embed Psiphon in your Android
+app.
+
+Status
+--------------------------------------------------------------------------------
+
+* Pre-release
+
+Building From Source
+--------------------------------------------------------------------------------
+
+Review Go Android documentation:
+* [Main 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
+  gojni.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
+```
+
+* 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, 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. 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";
+}

+ 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{})
+}

+ 83 - 0
AndroidLibrary/psi/psi.go

@@ -0,0 +1,83 @@
+/*
+ * 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"
+	psiphon "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"log"
+	"sync"
+)
+
+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)
+	}
+
+	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() {
+	close(shutdownBroadcast)
+	controllerWaitGroup.Wait()
+	controller = nil
+	shutdownBroadcast = nil
+	controllerWaitGroup = nil
+}

+ 6 - 1
psiphonClient.go → ConsoleClient/psiphonClient.go

@@ -22,6 +22,7 @@ package main
 import (
 	"flag"
 	psiphon "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"io/ioutil"
 	"log"
 	"os"
 	"os/signal"
@@ -36,10 +37,14 @@ 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)
+	}
 
 	if config.LogFilename != "" {
 		logFile, err := os.OpenFile(config.LogFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)

+ 4 - 9
psiphon/config.go

@@ -22,7 +22,6 @@ package psiphon
 import (
 	"encoding/json"
 	"errors"
-	"io/ioutil"
 )
 
 type Config struct {
@@ -46,15 +45,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)
 	}

+ 3 - 3
psiphon/controller.go

@@ -264,7 +264,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
 }
 
@@ -292,7 +292,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
 		}
 	}
@@ -308,7 +308,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

+ 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"