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

Convert Psibot to use PsiphonTunnel.java in place of PsiphonVpn.java

* PsiphonTunnel.java is the new tunnel-core/VPN "glue" module that
  is to be included in the Psiphon Android Library
Rod Hynes 10 лет назад
Родитель
Сommit
384b0f1095

+ 109 - 72
SampleApps/Psibot/app/src/main/java/ca/psiphon/PsiphonVpn.java → SampleApps/Psibot/app/src/main/java/ca/psiphon/PsiphonTunnel.java

@@ -29,12 +29,11 @@ import android.os.Build;
 import android.os.ParcelFileDescriptor;
 
 import org.apache.http.conn.util.InetAddressUtils;
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.InetAddress;
@@ -50,16 +49,27 @@ import java.util.Map;
 
 import go.psi.Psi;
 
-public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
+public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
 
     public interface HostService {
         public String getAppName();
+        public Context getContext();
         public VpnService getVpnService();
         public VpnService.Builder newVpnServiceBuilder();
-        public InputStream getPsiphonConfigResource();
-        public void customizeConfigParameters(JSONObject config);
-        public void logWarning(String message);
-        public void logInfo(String message);
+        public String getPsiphonConfig();
+        public void onDiagnosticMessage(String message);
+        public void onAvailableEgressRegions(List<String> regions);
+        public void onSocksProxyPortInUse(int port);
+        public void onHttpProxyPortInUse(int port);
+        public void onListeningSocksProxyPort(int port);
+        public void onListeningHttpProxyPort(int port);
+        public void onUpstreamProxyError(String message);
+        public void onConnecting();
+        public void onConnected();
+        public void onHomepage(String url);
+        public void onClientUpgradeDownloaded(String filename);
+        public void onSplitTunnelRegion(String region);
+        public void onUntunneledAddress(String address);
     }
 
     private final HostService mHostService;
@@ -71,19 +81,19 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
 
     // Only one PsiphonVpn instance may exist at a time, as the underlying
     // go.psi.Psi and tun2socks implementations each contain global state.
-    private static PsiphonVpn mPsiphonVpn;
+    private static PsiphonTunnel mPsiphonTunnel;
 
-    public static synchronized PsiphonVpn newPsiphonVpn(HostService hostService) {
-        if (mPsiphonVpn != null) {
-            mPsiphonVpn.stop();
+    public static synchronized PsiphonTunnel newPsiphonVpn(HostService hostService) {
+        if (mPsiphonTunnel != null) {
+            mPsiphonTunnel.stop();
         }
         // Load the native go code embedded in psi.aar
         System.loadLibrary("gojni");
-        mPsiphonVpn = new PsiphonVpn(hostService);
-        return mPsiphonVpn;
+        mPsiphonTunnel = new PsiphonTunnel(hostService);
+        return mPsiphonTunnel;
     }
 
-    private PsiphonVpn(HostService hostService) {
+    private PsiphonTunnel(HostService hostService) {
         mHostService = hostService;
         mLocalSocksProxyPort = 0;
         mRoutingThroughTunnel = false;
@@ -110,17 +120,13 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
     // Throws an exception in error conditions. In the case of an exception, the routing
     // started by startRouting() is not immediately torn down (this allows the caller to control
     // exactly when VPN routing is stopped); caller should call stop() to clean up.
-    public synchronized void startTunneling() throws Exception {
-        if (mTunFd == null) {
-            // Most likely, startRouting() was not called before this function.
-            throw new Exception("startTunneling: missing tun fd");
-        }
-        startPsiphon();
+    public synchronized void startTunneling(String embeddedServerEntries) throws Exception {
+        startPsiphon(embeddedServerEntries);
     }
 
     public synchronized void restartPsiphon() throws Exception {
         stopPsiphon();
-        startPsiphon();
+        startPsiphon("");
     }
 
     public synchronized void stop() {
@@ -138,6 +144,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
     private final static int UDPGW_SERVER_PORT = 7300;
     private final static String DEFAULT_DNS_SERVER = "8.8.4.4";
 
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
     private boolean startVpn() throws Exception {
 
         mPrivateAddress = selectPrivateAddress();
@@ -162,7 +169,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
                 // this application is no longer prepared or was revoked.
                 return false;
             }
-            mHostService.logInfo("VPN established");
+            mHostService.onDiagnosticMessage("VPN established");
 
         } catch(IllegalArgumentException e) {
             throw new Exception(errorMessage, e);
@@ -198,7 +205,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
                 udpgwServerAddress,
                 true);
         mTunFd = null;
-        mHostService.logInfo("routing through tunnel");
+        mHostService.onDiagnosticMessage("routing through tunnel");
 
         // TODO: should double-check tunnel routing; see:
         // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/1dc5e4257dca99790109f3bf374e8ab3a0ead4d7/Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java?at=default#cl-779
@@ -212,10 +219,12 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
             }
             mTunFd = null;
         }
-        stopTun2Socks();
-        mRoutingThroughTunnel = false;
+        if (mRoutingThroughTunnel) {
+            stopTun2Socks();
+            mRoutingThroughTunnel = false;
+        }
     }
-
+    
     //----------------------------------------------------------------------------------------------
     // PsiphonProvider (Core support) interface implementation
     //----------------------------------------------------------------------------------------------
@@ -225,6 +234,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
         handlePsiphonNotice(noticeJSON);
     }
 
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
     @Override
     public void BindToDevice(long fileDescriptor) throws Exception {
         if (!mHostService.getVpnService().protect((int)fileDescriptor)) {
@@ -235,7 +245,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
     @Override
     public long HasNetworkConnectivity() {
         // TODO: change to bool return value once gobind supports that type
-        return hasNetworkConnectivity(mHostService.getVpnService()) ? 1 : 0;
+        return hasNetworkConnectivity(mHostService.getContext()) ? 1 : 0;
     }
 
     @Override
@@ -244,7 +254,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
         try {
             dnsResolver = getFirstActiveNetworkDnsResolver(mHostService.getVpnService());
         } catch (Exception e) {
-            mHostService.logWarning("failed to get active network DNS resolver: " + e.getMessage());
+            mHostService.onDiagnosticMessage("failed to get active network DNS resolver: " + e.getMessage());
             dnsResolver = DEFAULT_DNS_SERVER;
         }
         return dnsResolver;
@@ -254,24 +264,26 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
     // Psiphon Tunnel Core
     //----------------------------------------------------------------------------------------------
 
-    private void startPsiphon() throws Exception {
+    private void startPsiphon(String embeddedServerEntries) throws Exception {
         stopPsiphon();
-        mHostService.logInfo("starting Psiphon");
+        mHostService.onDiagnosticMessage("starting Psiphon library");
         try {
+            boolean useDeviceBinder = (mTunFd != null);
             Psi.Start(
-                loadPsiphonConfig(mHostService.getVpnService()),
-                "", // TODO: supply embedded server list
-                this);
+                loadPsiphonConfig(mHostService.getContext()),
+                embeddedServerEntries,
+                this,
+                useDeviceBinder);
         } catch (java.lang.Exception e) {
-            throw new Exception("failed to start Psiphon", e);
+            throw new Exception("failed to start Psiphon library", e);
         }
-        mHostService.logInfo("Psiphon started");
+        mHostService.onDiagnosticMessage("Psiphon library started");
     }
 
     private void stopPsiphon() {
-        mHostService.logInfo("stopping Psiphon");
+        mHostService.onDiagnosticMessage("stopping Psiphon library");
         Psi.Stop();
-        mHostService.logInfo("Psiphon stopped");
+        mHostService.onDiagnosticMessage("Psiphon library stopped");
     }
 
     private String loadPsiphonConfig(Context context)
@@ -279,9 +291,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
 
         // Load settings from the raw resource JSON config file and
         // update as necessary. Then write JSON to disk for the Go client.
-        JSONObject json = new JSONObject(
-                readInputStreamToString(
-                    mHostService.getPsiphonConfigResource()));
+        JSONObject json = new JSONObject(mHostService.getPsiphonConfig());
 
         // On Android, these directories must be set to the app private storage area.
         // The Psiphon library won't be able to use its current working directory
@@ -289,7 +299,8 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
         json.put("DataStoreDirectory", context.getFilesDir());
         json.put("DataStoreTempDirectory", context.getCacheDir());
 
-        mPsiphonVpn.mHostService.customizeConfigParameters(json);
+        // Note: onConnecting/onConnected logic assumes 1 tunnel connection
+        json.put("TunnelPoolSize", 1);
 
         if (mLocalSocksProxyPort != 0) {
             // When mLocalSocksProxyPort is set, tun2socks is already configured
@@ -304,22 +315,68 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
 
     private void handlePsiphonNotice(String noticeJSON) {
         try {
+            // All notices are sent on as diagnostic messages
+            // except those that may contain private user data.
+            boolean diagnostic = true;
+            
             JSONObject notice = new JSONObject(noticeJSON);
             String noticeType = notice.getString("noticeType");
+            
             if (noticeType.equals("Tunnels")) {
                 int count = notice.getJSONObject("data").getInt("count");
                 if (count > 0) {
-                    routeThroughTunnel();
+                    if (mTunFd != null) {
+                        routeThroughTunnel();
+                    }
+                    mHostService.onConnected();
+                } else {
+                    mHostService.onConnecting();
                 }
+
+            } else if (noticeType.equals("AvailableEgressRegions")) {
+                JSONArray egressRegions = notice.getJSONObject("data").getJSONArray("regions");
+                ArrayList<String> regions = new ArrayList<String>();
+                for (int i=0; i<egressRegions.length(); i++) {
+                    regions.add(egressRegions.getString(i));
+                }
+                mHostService.onAvailableEgressRegions(regions);
+                
+            } else if (noticeType.equals("SocksProxyPortInUse")) {
+                mHostService.onSocksProxyPortInUse(notice.getJSONObject("data").getInt("port"));
+
+            } else if (noticeType.equals("HttpProxyPortInUse")) {
+                mHostService.onHttpProxyPortInUse(notice.getJSONObject("data").getInt("port"));
+
             } else if (noticeType.equals("ListeningSocksProxyPort")) {
-                setLocalSocksProxyPort(notice.getJSONObject("data").getInt("port"));
-            /*
+                int port = notice.getJSONObject("data").getInt("port");
+                setLocalSocksProxyPort(port);
+                mHostService.onListeningSocksProxyPort(port);
+
+            } else if (noticeType.equals("ListeningHttpProxyPort")) {
+                int port = notice.getJSONObject("data").getInt("port");
+                mHostService.onListeningHttpProxyPort(port);
+
+            } else if (noticeType.equals("UpstreamProxyError")) {
+                mHostService.onUpstreamProxyError(notice.getJSONObject("data").getString("message"));
+
+            } else if (noticeType.equals("ClientUpgradeDownloaded")) {
+                mHostService.onHomepage(notice.getJSONObject("data").getString("filename"));
+
             } else if (noticeType.equals("Homepage")) {
-                String homePage = notice.getJSONObject("data").getString("url");
-            */
+                mHostService.onHomepage(notice.getJSONObject("data").getString("url"));
+
+            } else if (noticeType.equals("SplitTunnelRegion")) {
+                mHostService.onHomepage(notice.getJSONObject("data").getString("region"));
+
+            } else if (noticeType.equals("UntunneledAddress")) {
+                mHostService.onHomepage(notice.getJSONObject("data").getString("address"));
             }
-            String displayNotice = noticeType + " " + notice.getJSONObject("data").toString();
-            mHostService.logInfo(displayNotice);
+
+            if (diagnostic) {
+                String diagnosticMessage = noticeType + ": " + notice.getJSONObject("data").toString();
+                mHostService.onDiagnosticMessage(diagnosticMessage);
+            }
+
         } catch (JSONException e) {
             // Ignore notice
         }
@@ -329,6 +386,7 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
     // Tun2Socks
     //----------------------------------------------------------------------------------------------
 
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
     private void startTun2Socks(
             final ParcelFileDescriptor vpnInterfaceFileDescriptor,
             final int vpnInterfaceMTU,
@@ -351,9 +409,8 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
                         udpgwTransparentDNS ? 1 : 0);
             }
         });
-        mPsiphonVpn = this;
         mTun2SocksThread.start();
-        mHostService.logInfo("tun2socks started");
+        mHostService.onDiagnosticMessage("tun2socks started");
     }
 
     private void stopTun2Socks() {
@@ -365,13 +422,13 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
                 Thread.currentThread().interrupt();
             }
             mTun2SocksThread = null;
-            mHostService.logInfo("tun2socks stopped");
+            mHostService.onDiagnosticMessage("tun2socks stopped");
         }
     }
 
     public static void logTun2Socks(String level, String channel, String msg) {
         String logMsg = "tun2socks: " + level + "(" + channel + "): " + msg;
-        mPsiphonVpn.mHostService.logWarning(logMsg);
+        mPsiphonTunnel.mHostService.onDiagnosticMessage(logMsg);
     }
 
     private native static int runTun2Socks(
@@ -516,26 +573,6 @@ public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
         return dnsAddresses;
     }
 
-    //----------------------------------------------------------------------------------------------
-    // Implementation: Resource Utils
-    //----------------------------------------------------------------------------------------------
-
-    private static String readInputStreamToString(InputStream inputStream) throws IOException {
-        return new String(readInputStreamToBytes(inputStream), "UTF-8");
-    }
-
-    private static byte[] readInputStreamToBytes(InputStream inputStream) throws IOException {
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        int readCount;
-        byte[] buffer = new byte[16384];
-        while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) {
-            outputStream.write(buffer, 0, readCount);
-        }
-        outputStream.flush();
-        inputStream.close();
-        return outputStream.toByteArray();
-    }
-
     //----------------------------------------------------------------------------------------------
     // Exception
     //----------------------------------------------------------------------------------------------

+ 112 - 28
SampleApps/Psibot/app/src/main/java/ca/psiphon/psibot/Service.java

@@ -21,6 +21,7 @@ package ca.psiphon.psibot;
 
 import android.app.Notification;
 import android.app.PendingIntent;
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.net.VpnService;
@@ -29,27 +30,30 @@ import android.preference.PreferenceManager;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
 
-import ca.psiphon.PsiphonVpn;
+import ca.psiphon.PsiphonTunnel;
 
 public class Service extends VpnService
-        implements PsiphonVpn.HostService, SharedPreferences.OnSharedPreferenceChangeListener {
+        implements PsiphonTunnel.HostService, SharedPreferences.OnSharedPreferenceChangeListener {
 
-    private PsiphonVpn mPsiphonVpn;
+    private PsiphonTunnel mPsiphonTunnel;
 
     @Override
     public void onCreate() {
-        mPsiphonVpn = PsiphonVpn.newPsiphonVpn(this);
+        mPsiphonTunnel = PsiphonTunnel.newPsiphonVpn(this);
         startForeground(R.string.foregroundServiceNotificationId, makeForegroundNotification());
         try {
-            if (!mPsiphonVpn.startRouting()) {
-                throw new PsiphonVpn.Exception("VPN not prepared");
+            if (!mPsiphonTunnel.startRouting()) {
+                throw new PsiphonTunnel.Exception("VPN not prepared");
             }
-            mPsiphonVpn.startTunneling();
-        } catch (PsiphonVpn.Exception e) {
+            mPsiphonTunnel.startTunneling("");
+        } catch (PsiphonTunnel.Exception e) {
             Log.addEntry("failed to start Psiphon VPN: " + e.getMessage());
-            mPsiphonVpn.stop();
+            mPsiphonTunnel.stop();
             stopSelf();
         }
         PreferenceManager.getDefaultSharedPreferences(this).
@@ -60,17 +64,17 @@ public class Service extends VpnService
     public void onDestroy() {
         PreferenceManager.getDefaultSharedPreferences(this).
                 unregisterOnSharedPreferenceChangeListener(this);
-        mPsiphonVpn.stop();
+        mPsiphonTunnel.stop();
         stopForeground(true);
     }
 
     @Override
     public synchronized void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
         try {
-            mPsiphonVpn.restartPsiphon();
-        } catch (PsiphonVpn.Exception e) {
+            mPsiphonTunnel.restartPsiphon();
+        } catch (PsiphonTunnel.Exception e) {
             Log.addEntry("failed to restart Psiphon: " + e.getMessage());
-            mPsiphonVpn.stop();
+            mPsiphonTunnel.stop();
             stopSelf();
         }
     }
@@ -81,27 +85,31 @@ public class Service extends VpnService
     }
 
     @Override
-    public VpnService getVpnService() {
+    public Context getContext() {
         return this;
     }
 
     @Override
-    public VpnService.Builder newVpnServiceBuilder() {
-        return new VpnService.Builder();
+    public VpnService getVpnService() {
+        return this;
     }
 
     @Override
-    public InputStream getPsiphonConfigResource() {
-        return getResources().openRawResource(R.raw.psiphon_config);
+    public VpnService.Builder newVpnServiceBuilder() {
+        return new VpnService.Builder();
     }
 
     @Override
-    public void customizeConfigParameters(JSONObject config) {
-        // User-specified settings.
-        // Note: currently, validation is not comprehensive, and related errors are
-        // not directly parsed.
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+    public String getPsiphonConfig() {
         try {
+            JSONObject config = new JSONObject(
+                    readInputStreamToString(
+                        getResources().openRawResource(R.raw.psiphon_config)));
+
+            // Insert user-specified settings.
+            // Note: currently, validation is not comprehensive, and related errors are
+            // not directly parsed.
+            SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
             config.put("EgressRegion",
                     preferences.getString(
                             getString(R.string.preferenceEgressRegion),
@@ -139,21 +147,97 @@ public class Service extends VpnService
                             preferences.getString(
                                     getString(R.string.preferencePortForwardFailureThreshold),
                                     getString(R.string.preferencePortForwardFailureThresholdDefaultValue))));
+
+            return config.toString();
+
+        } catch (IOException e) {
+            Log.addEntry("error setting config parameters: " + e.getMessage());
         } catch (JSONException e) {
             Log.addEntry("error setting config parameters: " + e.getMessage());
         }
+        return "";
     }
 
     @Override
-    public void logWarning(String message) {
-        android.util.Log.w(getString(R.string.app_name), message);
+    public void onDiagnosticMessage(String message) {
+        android.util.Log.i(getString(R.string.app_name), message);
         Log.addEntry(message);
     }
 
     @Override
-    public void logInfo(String message) {
-        android.util.Log.i(getString(R.string.app_name), message);
-        Log.addEntry(message);
+    public void onAvailableEgressRegions(List<String> regions) {
+        // TODO: show only available regions in SettingActivity
+    }
+
+    @Override
+    public void onSocksProxyPortInUse(int port) {
+        Log.addEntry("local SOCKS proxy port in use: " + Integer.toString(port));
+    }
+
+    @Override
+    public void onHttpProxyPortInUse(int port) {
+        Log.addEntry("local HTTP proxy port in use: " + Integer.toString(port));
+    }
+
+    @Override
+    public void onListeningSocksProxyPort(int port) {
+        Log.addEntry("local SOCKS proxy listening on port: " + Integer.toString(port));
+    }
+
+    @Override
+    public void onListeningHttpProxyPort(int port) {
+        Log.addEntry("local HTTP proxy listening on port: " + Integer.toString(port));
+    }
+
+    @Override
+    public void onUpstreamProxyError(String message) {
+        Log.addEntry("upstream proxy error: " + message);
+    }
+
+    @Override
+    public void onConnecting() {
+        Log.addEntry("connecting...");
+    }
+
+    @Override
+    public void onConnected() {
+        Log.addEntry("connected");
+    }
+
+    @Override
+    public void onHomepage(String url) {
+        Log.addEntry("home page: " + url);
+    }
+
+    @Override
+    public void onClientUpgradeDownloaded(String filename) {
+        Log.addEntry("client upgrade downloaded");
+    }
+
+    @Override
+    public void onSplitTunnelRegion(String region) {
+        Log.addEntry("split tunnel region: " + region);
+    }
+
+    @Override
+    public void onUntunneledAddress(String address) {
+        Log.addEntry("untunneled address: " + address);
+    }
+
+    private static String readInputStreamToString(InputStream inputStream) throws IOException {
+        return new String(readInputStreamToBytes(inputStream), "UTF-8");
+    }
+
+    private static byte[] readInputStreamToBytes(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        int readCount;
+        byte[] buffer = new byte[16384];
+        while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) {
+            outputStream.write(buffer, 0, readCount);
+        }
+        outputStream.flush();
+        inputStream.close();
+        return outputStream.toByteArray();
     }
 
     private Notification makeForegroundNotification() {

BIN
SampleApps/Psibot/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so