Merge "Showcase HTTP proxy usage in ToyVPN."
This commit is contained in:
@@ -31,6 +31,30 @@
|
|||||||
<TextView style="@style/item" android:text="@string/secret"/>
|
<TextView style="@style/item" android:text="@string/secret"/>
|
||||||
<EditText style="@style/item" android:id="@+id/secret" android:password="true"/>
|
<EditText style="@style/item" android:id="@+id/secret" android:password="true"/>
|
||||||
|
|
||||||
|
<TextView style="@style/item" android:text="@string/proxyhost"/>
|
||||||
|
<EditText style="@style/item" android:id="@+id/proxyhost"/>
|
||||||
|
|
||||||
|
<TextView style="@style/item" android:text="@string/proxyport"/>
|
||||||
|
<EditText style="@style/item" android:id="@+id/proxyport" android:inputType="number"/>
|
||||||
|
|
||||||
|
<TextView style="@style/item" android:text="@string/packages"/>
|
||||||
|
<RadioGroup
|
||||||
|
style="@style/item"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/allowed"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/allowed"/>
|
||||||
|
<RadioButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/disallowed"/>
|
||||||
|
</RadioGroup>
|
||||||
|
<EditText style="@style/item" android:id="@+id/packages"/>
|
||||||
|
|
||||||
<Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/>
|
<Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/>
|
||||||
<Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>
|
<Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,20 @@
|
|||||||
<string name="secret">Shared Secret:</string>
|
<string name="secret">Shared Secret:</string>
|
||||||
<string name="connect">Connect!</string>
|
<string name="connect">Connect!</string>
|
||||||
<string name="disconnect">Disconnect!</string>
|
<string name="disconnect">Disconnect!</string>
|
||||||
|
<string name="proxyhost">HTTP proxy hostname</string>
|
||||||
|
<string name="proxyport">HTTP proxy port</string>
|
||||||
|
|
||||||
|
<string name="packages">Packages (comma separated):</string>
|
||||||
|
<string name="allowed">Allow</string>
|
||||||
|
<string name="disallowed">Disallow</string>
|
||||||
|
|
||||||
<string name="connecting">ToyVPN is connecting...</string>
|
<string name="connecting">ToyVPN is connecting...</string>
|
||||||
<string name="connected">ToyVPN is connected!</string>
|
<string name="connected">ToyVPN is connected!</string>
|
||||||
<string name="disconnected">ToyVPN is disconnected!</string>
|
<string name="disconnected">ToyVPN is disconnected!</string>
|
||||||
|
<string name="incomplete_proxy_settings">
|
||||||
|
Incomplete proxy settings. For HTTP proxy we require both hostname and port settings.
|
||||||
|
</string>
|
||||||
|
<string name="unknown_package_names">
|
||||||
|
Some of the specified package names do not correspond to any installed packages.
|
||||||
|
</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -21,7 +21,14 @@ import android.content.Intent;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.VpnService;
|
import android.net.VpnService;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.widget.RadioButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ToyVpnClient extends Activity {
|
public class ToyVpnClient extends Activity {
|
||||||
public interface Prefs {
|
public interface Prefs {
|
||||||
@@ -29,6 +36,10 @@ public class ToyVpnClient extends Activity {
|
|||||||
String SERVER_ADDRESS = "server.address";
|
String SERVER_ADDRESS = "server.address";
|
||||||
String SERVER_PORT = "server.port";
|
String SERVER_PORT = "server.port";
|
||||||
String SHARED_SECRET = "shared.secret";
|
String SHARED_SECRET = "shared.secret";
|
||||||
|
String PROXY_HOSTNAME = "proxyhost";
|
||||||
|
String PROXY_PORT = "proxyport";
|
||||||
|
String ALLOW = "allow";
|
||||||
|
String PACKAGES = "packages";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -36,22 +47,64 @@ public class ToyVpnClient extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.form);
|
setContentView(R.layout.form);
|
||||||
|
|
||||||
final TextView serverAddress = (TextView) findViewById(R.id.address);
|
final TextView serverAddress = findViewById(R.id.address);
|
||||||
final TextView serverPort = (TextView) findViewById(R.id.port);
|
final TextView serverPort = findViewById(R.id.port);
|
||||||
final TextView sharedSecret = (TextView) findViewById(R.id.secret);
|
final TextView sharedSecret = findViewById(R.id.secret);
|
||||||
|
final TextView proxyHost = findViewById(R.id.proxyhost);
|
||||||
|
final TextView proxyPort = findViewById(R.id.proxyport);
|
||||||
|
|
||||||
|
final RadioButton allowed = findViewById(R.id.allowed);
|
||||||
|
final TextView packages = findViewById(R.id.packages);
|
||||||
|
|
||||||
final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
|
final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
|
||||||
serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, ""));
|
serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, ""));
|
||||||
serverPort.setText(prefs.getString(Prefs.SERVER_PORT, ""));
|
int serverPortPrefValue = prefs.getInt(Prefs.SERVER_PORT, 0);
|
||||||
|
serverPort.setText(String.valueOf(serverPortPrefValue == 0 ? "" : serverPortPrefValue));
|
||||||
sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, ""));
|
sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, ""));
|
||||||
|
proxyHost.setText(prefs.getString(Prefs.PROXY_HOSTNAME, ""));
|
||||||
|
int proxyPortPrefValue = prefs.getInt(Prefs.PROXY_PORT, 0);
|
||||||
|
proxyPort.setText(proxyPortPrefValue == 0 ? "" : String.valueOf(proxyPortPrefValue));
|
||||||
|
|
||||||
|
allowed.setChecked(prefs.getBoolean(Prefs.ALLOW, true));
|
||||||
|
packages.setText(String.join(", ", prefs.getStringSet(
|
||||||
|
Prefs.PACKAGES, Collections.emptySet())));
|
||||||
|
|
||||||
findViewById(R.id.connect).setOnClickListener(v -> {
|
findViewById(R.id.connect).setOnClickListener(v -> {
|
||||||
|
if (!checkProxyConfigs(proxyHost.getText().toString(),
|
||||||
|
proxyPort.getText().toString())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> packageSet =
|
||||||
|
Arrays.stream(packages.getText().toString().split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (!checkPackages(packageSet)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int serverPortNum;
|
||||||
|
try {
|
||||||
|
serverPortNum = Integer.parseInt(serverPort.getText().toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
serverPortNum = 0;
|
||||||
|
}
|
||||||
|
int proxyPortNum;
|
||||||
|
try {
|
||||||
|
proxyPortNum = Integer.parseInt(proxyPort.getText().toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
proxyPortNum = 0;
|
||||||
|
}
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString())
|
.putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString())
|
||||||
.putString(Prefs.SERVER_PORT, serverPort.getText().toString())
|
.putInt(Prefs.SERVER_PORT, serverPortNum)
|
||||||
.putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString())
|
.putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString())
|
||||||
|
.putString(Prefs.PROXY_HOSTNAME, proxyHost.getText().toString())
|
||||||
|
.putInt(Prefs.PROXY_PORT, proxyPortNum)
|
||||||
|
.putBoolean(Prefs.ALLOW, allowed.isChecked())
|
||||||
|
.putStringSet(Prefs.PACKAGES, packageSet)
|
||||||
.commit();
|
.commit();
|
||||||
|
|
||||||
Intent intent = VpnService.prepare(ToyVpnClient.this);
|
Intent intent = VpnService.prepare(ToyVpnClient.this);
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
startActivityForResult(intent, 0);
|
startActivityForResult(intent, 0);
|
||||||
@@ -64,6 +117,26 @@ public class ToyVpnClient extends Activity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkProxyConfigs(String proxyHost, String proxyPort) {
|
||||||
|
final boolean hasIncompleteProxyConfigs = proxyHost.isEmpty() != proxyPort.isEmpty();
|
||||||
|
if (hasIncompleteProxyConfigs) {
|
||||||
|
Toast.makeText(this, R.string.incomplete_proxy_settings, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
return !hasIncompleteProxyConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkPackages(Set<String> packageNames) {
|
||||||
|
final boolean hasCorrectPackageNames = packageNames.isEmpty() ||
|
||||||
|
getPackageManager().getInstalledPackages(0).stream()
|
||||||
|
.map(pi -> pi.packageName)
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
.containsAll(packageNames);
|
||||||
|
if (!hasCorrectPackageNames) {
|
||||||
|
Toast.makeText(this, R.string.unknown_package_names, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
return hasCorrectPackageNames;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int request, int result, Intent data) {
|
protected void onActivityResult(int request, int result, Intent data) {
|
||||||
if (result == RESULT_OK) {
|
if (result == RESULT_OK) {
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ package com.example.android.toyvpn;
|
|||||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.ProxyInfo;
|
||||||
import android.net.VpnService;
|
import android.net.VpnService;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@@ -31,6 +34,7 @@ import java.net.SocketAddress;
|
|||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.DatagramChannel;
|
import java.nio.channels.DatagramChannel;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class ToyVpnConnection implements Runnable {
|
public class ToyVpnConnection implements Runnable {
|
||||||
@@ -83,14 +87,34 @@ public class ToyVpnConnection implements Runnable {
|
|||||||
private PendingIntent mConfigureIntent;
|
private PendingIntent mConfigureIntent;
|
||||||
private OnEstablishListener mOnEstablishListener;
|
private OnEstablishListener mOnEstablishListener;
|
||||||
|
|
||||||
|
// Proxy settings
|
||||||
|
private String mProxyHostName;
|
||||||
|
private int mProxyHostPort;
|
||||||
|
|
||||||
|
// Allowed/Disallowed packages for VPN usage
|
||||||
|
private final boolean mAllow;
|
||||||
|
private final Set<String> mPackages;
|
||||||
|
|
||||||
public ToyVpnConnection(final VpnService service, final int connectionId,
|
public ToyVpnConnection(final VpnService service, final int connectionId,
|
||||||
final String serverName, final int serverPort, final byte[] sharedSecret) {
|
final String serverName, final int serverPort, final byte[] sharedSecret,
|
||||||
|
final String proxyHostName, final int proxyHostPort, boolean allow,
|
||||||
|
final Set<String> packages) {
|
||||||
mService = service;
|
mService = service;
|
||||||
mConnectionId = connectionId;
|
mConnectionId = connectionId;
|
||||||
|
|
||||||
mServerName = serverName;
|
mServerName = serverName;
|
||||||
mServerPort= serverPort;
|
mServerPort= serverPort;
|
||||||
mSharedSecret = sharedSecret;
|
mSharedSecret = sharedSecret;
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(proxyHostName)) {
|
||||||
|
mProxyHostName = proxyHostName;
|
||||||
|
}
|
||||||
|
if (proxyHostPort > 0) {
|
||||||
|
// The port value is always an integer due to the configured inputType.
|
||||||
|
mProxyHostPort = proxyHostPort;
|
||||||
|
}
|
||||||
|
mAllow = allow;
|
||||||
|
mPackages = packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,11 +333,23 @@ public class ToyVpnConnection implements Runnable {
|
|||||||
|
|
||||||
// Create a new interface using the builder and save the parameters.
|
// Create a new interface using the builder and save the parameters.
|
||||||
final ParcelFileDescriptor vpnInterface;
|
final ParcelFileDescriptor vpnInterface;
|
||||||
|
for (String packageName : mPackages) {
|
||||||
|
try {
|
||||||
|
if (mAllow) {
|
||||||
|
builder.addAllowedApplication(packageName);
|
||||||
|
} else {
|
||||||
|
builder.addDisallowedApplication(packageName);
|
||||||
|
}
|
||||||
|
} catch (PackageManager.NameNotFoundException e){
|
||||||
|
Log.w(getTag(), "Package not available: " + packageName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setSession(mServerName).setConfigureIntent(mConfigureIntent);
|
||||||
|
if (!TextUtils.isEmpty(mProxyHostName)) {
|
||||||
|
builder.setHttpProxy(ProxyInfo.buildDirectProxy(mProxyHostName, mProxyHostPort));
|
||||||
|
}
|
||||||
synchronized (mService) {
|
synchronized (mService) {
|
||||||
vpnInterface = builder
|
vpnInterface = builder.establish();
|
||||||
.setSession(mServerName)
|
|
||||||
.setConfigureIntent(mConfigureIntent)
|
|
||||||
.establish();
|
|
||||||
if (mOnEstablishListener != null) {
|
if (mOnEstablishListener != null) {
|
||||||
mOnEstablishListener.onEstablish(vpnInterface);
|
mOnEstablishListener.onEstablish(vpnInterface);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
package com.example.android.toyvpn;
|
package com.example.android.toyvpn;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.VpnService;
|
import android.net.VpnService;
|
||||||
@@ -30,6 +31,8 @@ import android.util.Pair;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
@@ -101,17 +104,15 @@ public class ToyVpnService extends VpnService implements Handler.Callback {
|
|||||||
final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
|
final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
|
||||||
final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
|
final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
|
||||||
final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
|
final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
|
||||||
final int port;
|
final boolean allow = prefs.getBoolean(ToyVpnClient.Prefs.ALLOW, true);
|
||||||
try {
|
final Set<String> packages =
|
||||||
port = Integer.parseInt(prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, ""));
|
prefs.getStringSet(ToyVpnClient.Prefs.PACKAGES, Collections.emptySet());
|
||||||
} catch (NumberFormatException e) {
|
final int port = prefs.getInt(ToyVpnClient.Prefs.SERVER_PORT, 0);
|
||||||
Log.e(TAG, "Bad port: " + prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, null), e);
|
final String proxyHost = prefs.getString(ToyVpnClient.Prefs.PROXY_HOSTNAME, "");
|
||||||
return;
|
final int proxyPort = prefs.getInt(ToyVpnClient.Prefs.PROXY_PORT, 0);
|
||||||
}
|
|
||||||
|
|
||||||
// Kick off a connection.
|
|
||||||
startConnection(new ToyVpnConnection(
|
startConnection(new ToyVpnConnection(
|
||||||
this, mNextConnectionId.getAndIncrement(), server, port, secret));
|
this, mNextConnectionId.getAndIncrement(), server, port, secret,
|
||||||
|
proxyHost, proxyPort, allow, packages));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startConnection(final ToyVpnConnection connection) {
|
private void startConnection(final ToyVpnConnection connection) {
|
||||||
@@ -121,13 +122,11 @@ public class ToyVpnService extends VpnService implements Handler.Callback {
|
|||||||
|
|
||||||
// Handler to mark as connected once onEstablish is called.
|
// Handler to mark as connected once onEstablish is called.
|
||||||
connection.setConfigureIntent(mConfigureIntent);
|
connection.setConfigureIntent(mConfigureIntent);
|
||||||
connection.setOnEstablishListener(new ToyVpnConnection.OnEstablishListener() {
|
connection.setOnEstablishListener(tunInterface -> {
|
||||||
public void onEstablish(ParcelFileDescriptor tunInterface) {
|
|
||||||
mHandler.sendEmptyMessage(R.string.connected);
|
mHandler.sendEmptyMessage(R.string.connected);
|
||||||
|
|
||||||
mConnectingThread.compareAndSet(thread, null);
|
mConnectingThread.compareAndSet(thread, null);
|
||||||
setConnection(new Connection(thread, tunInterface));
|
setConnection(new Connection(thread, tunInterface));
|
||||||
}
|
|
||||||
});
|
});
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
@@ -159,7 +158,13 @@ public class ToyVpnService extends VpnService implements Handler.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateForegroundNotification(final int message) {
|
private void updateForegroundNotification(final int message) {
|
||||||
startForeground(1, new Notification.Builder(this)
|
final String NOTIFICATION_CHANNEL_ID = "ToyVpn";
|
||||||
|
NotificationManager mNotificationManager = (NotificationManager) getSystemService(
|
||||||
|
NOTIFICATION_SERVICE);
|
||||||
|
mNotificationManager.createNotificationChannel(new NotificationChannel(
|
||||||
|
NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT));
|
||||||
|
startForeground(1, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_vpn)
|
.setSmallIcon(R.drawable.ic_vpn)
|
||||||
.setContentText(getString(message))
|
.setContentText(getString(message))
|
||||||
.setContentIntent(mConfigureIntent)
|
.setContentIntent(mConfigureIntent)
|
||||||
|
|||||||
Reference in New Issue
Block a user