diff options
Diffstat (limited to 'src/de/blinkt/openvpn/core')
-rw-r--r-- | src/de/blinkt/openvpn/core/CIDRIP.java | 66 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/ConfigParser.java | 640 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/NetworkSateReceiver.java | 90 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/OpenVPN.java | 427 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/OpenVPNMangement.java | 14 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/OpenVPNThread.java | 136 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/OpenVpnManagementThread.java | 482 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/OpenVpnService.java | 625 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/ProfileManager.java | 223 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/ProxyDetection.java | 55 | ||||
-rw-r--r-- | src/de/blinkt/openvpn/core/VPNLaunchHelper.java | 76 |
11 files changed, 2834 insertions, 0 deletions
diff --git a/src/de/blinkt/openvpn/core/CIDRIP.java b/src/de/blinkt/openvpn/core/CIDRIP.java new file mode 100644 index 00000000..27ce414c --- /dev/null +++ b/src/de/blinkt/openvpn/core/CIDRIP.java @@ -0,0 +1,66 @@ +package de.blinkt.openvpn.core; + +import java.util.Locale; + +class CIDRIP{ + String mIp; + int len; + + + public CIDRIP(String ip, String mask){ + mIp=ip; + long netmask=getInt(mask); + + // Add 33. bit to ensure the loop terminates + netmask += 1l << 32; + + int lenZeros = 0; + while((netmask & 0x1) == 0) { + lenZeros++; + netmask = netmask >> 1; + } + // Check if rest of netmask is only 1s + if(netmask != (0x1ffffffffl >> lenZeros)) { + // Asume no CIDR, set /32 + len=32; + } else { + len =32 -lenZeros; + } + + } + public CIDRIP(String address, int prefix_length) { + len = prefix_length; + mIp = address; + } + @Override + public String toString() { + return String.format(Locale.ENGLISH,"%s/%d",mIp,len); + } + + public boolean normalise(){ + long ip=getInt(mIp); + + long newip = ip & (0xffffffffl << (32 -len)); + if (newip != ip){ + mIp = String.format("%d.%d.%d.%d", (newip & 0xff000000) >> 24,(newip & 0xff0000) >> 16, (newip & 0xff00) >> 8 ,newip & 0xff); + return true; + } else { + return false; + } + } + static long getInt(String ipaddr) { + String[] ipt = ipaddr.split("\\."); + long ip=0; + + ip += Long.parseLong(ipt[0])<< 24; + ip += Integer.parseInt(ipt[1])<< 16; + ip += Integer.parseInt(ipt[2])<< 8; + ip += Integer.parseInt(ipt[3]); + + return ip; + } + public long getInt() { + return getInt(mIp); + } + +}
\ No newline at end of file diff --git a/src/de/blinkt/openvpn/core/ConfigParser.java b/src/de/blinkt/openvpn/core/ConfigParser.java new file mode 100644 index 00000000..9faebfb6 --- /dev/null +++ b/src/de/blinkt/openvpn/core/ConfigParser.java @@ -0,0 +1,640 @@ +package de.blinkt.openvpn.core; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Vector; + +import de.blinkt.openvpn.VpnProfile; + +//! Openvpn Config FIle Parser, probably not 100% accurate but close enough + +// And rember, this is valid :) +// --<foo> +// bar +// </foo> +public class ConfigParser { + + + public static final String CONVERTED_PROFILE = "converted Profile"; + private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>(); + private HashMap<String, Vector<String>> meta = new HashMap<String, Vector<String>>(); + + + private boolean extraRemotesAsCustom=false; + + public void parseConfig(Reader reader) throws IOException, ConfigParseError { + + + BufferedReader br =new BufferedReader(reader); + + while (true){ + String line = br.readLine(); + if(line==null) + break; + + // Check for OpenVPN Access Server Meta information + if (line.startsWith("# OVPN_ACCESS_SERVER_")) { + Vector<String> metaarg = parsemeta(line); + meta.put(metaarg.get(0),metaarg); + continue; + } + Vector<String> args = parseline(line); + + if(args.size() ==0) + continue; + + + if(args.get(0).startsWith("--")) + args.set(0, args.get(0).substring(2)); + + checkinlinefile(args,br); + + String optionname = args.get(0); + if(!options.containsKey(optionname)) { + options.put(optionname, new Vector<Vector<String>>()); + } + options.get(optionname).add(args); + } + } + + private Vector<String> parsemeta(String line) { + String meta = line.split("#\\sOVPN_ACCESS_SERVER_", 2)[1]; + String[] parts = meta.split("=",2); + Vector<String> rval = new Vector<String>(); + for(String p:parts) + rval.add(p); + return rval; + + } + + private void checkinlinefile(Vector<String> args, BufferedReader br) throws IOException, ConfigParseError { + String arg0 = args.get(0); + // CHeck for <foo> + if(arg0.startsWith("<") && arg0.endsWith(">")) { + String argname = arg0.substring(1, arg0.length()-1); + String inlinefile = VpnProfile.INLINE_TAG; + + String endtag = String.format("</%s>",argname); + do { + String line = br.readLine(); + if(line==null){ + throw new ConfigParseError(String.format("No endtag </%s> for starttag <%s> found",argname,argname)); + } + if(line.equals(endtag)) + break; + else { + inlinefile+=line; + inlinefile+= "\n"; + } + } while(true); + + args.clear(); + args.add(argname); + args.add(inlinefile); + } + + } + + enum linestate { + initial, + readin_single_quote + , reading_quoted, reading_unquoted, done} + + private boolean space(char c) { + // I really hope nobody is using zero bytes inside his/her config file + // to sperate parameter but here we go: + return Character.isWhitespace(c) || c == '\0'; + + } + + public class ConfigParseError extends Exception { + private static final long serialVersionUID = -60L; + + public ConfigParseError(String msg) { + super(msg); + } + } + + + // adapted openvpn's parse function to java + private Vector<String> parseline(String line) throws ConfigParseError { + Vector<String> parameters = new Vector<String>(); + + if (line.length()==0) + return parameters; + + + linestate state = linestate.initial; + boolean backslash = false; + char out=0; + + int pos=0; + String currentarg=""; + + do { + // Emulate the c parsing ... + char in; + if(pos < line.length()) + in = line.charAt(pos); + else + in = '\0'; + + if (!backslash && in == '\\' && state != linestate.readin_single_quote) + { + backslash = true; + } + else + { + if (state == linestate.initial) + { + if (!space (in)) + { + if (in == ';' || in == '#') /* comment */ + break; + if (!backslash && in == '\"') + state = linestate.reading_quoted; + else if (!backslash && in == '\'') + state = linestate.readin_single_quote; + else + { + out = in; + state = linestate.reading_unquoted; + } + } + } + else if (state == linestate.reading_unquoted) + { + if (!backslash && space (in)) + state = linestate.done; + else + out = in; + } + else if (state == linestate.reading_quoted) + { + if (!backslash && in == '\"') + state = linestate.done; + else + out = in; + } + else if (state == linestate.readin_single_quote) + { + if (in == '\'') + state = linestate.done; + else + out = in; + } + + if (state == linestate.done) + { + /* ASSERT (parm_len > 0); */ + state = linestate.initial; + parameters.add(currentarg); + currentarg = ""; + out =0; + } + + if (backslash && out!=0) + { + if (!(out == '\\' || out == '\"' || space (out))) + { + throw new ConfigParseError("Options warning: Bad backslash ('\\') usage"); + } + } + backslash = false; + } + + /* store parameter character */ + if (out!=0) + { + currentarg+=out; + } + } while (pos++ < line.length()); + + return parameters; + } + + + final String[] unsupportedOptions = { "config", + "connection", + "proto-force", + "remote-random", + "tls-server" + + }; + + // Ignore all scripts + // in most cases these won't work and user who wish to execute scripts will + // figure out themselves + final String[] ignoreOptions = { "tls-client", + "askpass", + "auth-nocache", + "up", + "down", + "route-up", + "ipchange", + "route-up", + "route-pre-down", + "auth-user-pass-verify", + "dhcp-release", + "dhcp-renew", + "dh", + "ip-win32", + "management-hold", + "management", + "management-query-passwords", + "pause-exit", + "persist-key", + "register-dns", + "route-delay", + "route-gateway", + "route-metric", + "route-method", + "status", + "script-security", + "show-net-up", + "suppress-timestamps", + "tmp-dir", + "tun-ipv6", + "topology", + "win-sys", + }; + + + // This method is far too long + public VpnProfile convertProfile() throws ConfigParseError{ + boolean noauthtypeset=true; + VpnProfile np = new VpnProfile(CONVERTED_PROFILE); + // Pull, client, tls-client + np.clearDefaults(); + + if(options.containsKey("client") || options.containsKey("pull")) { + np.mUsePull=true; + options.remove("pull"); + options.remove("client"); + } + + Vector<String> secret = getOption("secret", 1, 2); + if(secret!=null) + { + np.mAuthenticationType=VpnProfile.TYPE_STATICKEYS; + noauthtypeset=false; + np.mUseTLSAuth=true; + np.mTLSAuthFilename=secret.get(1); + if(secret.size()==3) + np.mTLSAuthDirection=secret.get(2); + + } + + Vector<Vector<String>> routes = getAllOption("route", 1, 4); + if(routes!=null) { + String routeopt = ""; + for(Vector<String> route:routes){ + String netmask = "255.255.255.255"; + if(route.size() >= 3) + netmask = route.get(2); + String net = route.get(1); + try { + CIDRIP cidr = new CIDRIP(net, netmask); + routeopt+=cidr.toString() + " "; + } catch (ArrayIndexOutOfBoundsException aioob) { + throw new ConfigParseError("Could not parse netmask of route " + netmask); + } catch (NumberFormatException ne) { + throw new ConfigParseError("Could not parse netmask of route " + netmask); + } + + } + np.mCustomRoutes=routeopt; + } + + // Also recognize tls-auth [inline] direction ... + Vector<Vector<String>> tlsauthoptions = getAllOption("tls-auth", 1, 2); + if(tlsauthoptions!=null) { + for(Vector<String> tlsauth:tlsauthoptions) { + if(tlsauth!=null) + { + if(!tlsauth.get(1).equals("[inline]")) { + np.mTLSAuthFilename=tlsauth.get(1); + np.mUseTLSAuth=true; + } + if(tlsauth.size()==3) + np.mTLSAuthDirection=tlsauth.get(2); + } + } + } + + Vector<String> direction = getOption("key-direction", 1, 1); + if(direction!=null) + np.mTLSAuthDirection=direction.get(1); + + + if(getAllOption("redirect-gateway", 0, 5) != null) + np.mUseDefaultRoute=true; + + Vector<String> dev =getOption("dev",1,1); + Vector<String> devtype =getOption("dev-type",1,1); + + if( (devtype !=null && devtype.get(1).equals("tun")) || + (dev!=null && dev.get(1).startsWith("tun")) || + (devtype ==null && dev == null) ) { + //everything okay + } else { + throw new ConfigParseError("Sorry. Only tun mode is supported. See the FAQ for more detail"); + } + + + + Vector<String> mode =getOption("mode",1,1); + if (mode != null){ + if(!mode.get(1).equals("p2p")) + throw new ConfigParseError("Invalid mode for --mode specified, need p2p"); + } + + Vector<String> port = getOption("port", 1,1); + if(port!=null){ + np.mServerPort = port.get(1); + } + + Vector<String> proto = getOption("proto", 1,1); + if(proto!=null){ + np.mUseUdp=isUdpProto(proto.get(1));; + } + + // Parse remote config + Vector<Vector<String>> remotes = getAllOption("remote",1,3); + + if(remotes!=null && remotes.size()>=1 ) { + Vector<String> remote = remotes.get(0); + switch (remote.size()) { + case 4: + np.mUseUdp=isUdpProto(remote.get(3)); + case 3: + np.mServerPort = remote.get(2); + case 2: + np.mServerName = remote.get(1); + } + } + + + + Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2); + if(dhcpoptions!=null) { + for(Vector<String> dhcpoption:dhcpoptions) { + String type=dhcpoption.get(1); + String arg = dhcpoption.get(2); + if(type.equals("DOMAIN")) { + np.mSearchDomain=dhcpoption.get(2); + } else if(type.equals("DNS")) { + np.mOverrideDNS=true; + if(np.mDNS1.equals(VpnProfile.DEFAULT_DNS1)) + np.mDNS1=arg; + else + np.mDNS2=arg; + } + } + } + + Vector<String> ifconfig = getOption("ifconfig", 2, 2); + if(ifconfig!=null) { + CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2)); + np.mIPv4Address=cidr.toString(); + } + + if(getOption("remote-random-hostname", 0, 0)!=null) + np.mUseRandomHostname=true; + + if(getOption("float", 0, 0)!=null) + np.mUseFloat=true; + + if(getOption("comp-lzo", 0, 1)!=null) + np.mUseLzo=true; + + Vector<String> cipher = getOption("cipher", 1, 1); + if(cipher!=null) + np.mCipher= cipher.get(1); + + Vector<String> auth = getOption("auth", 1, 1); + if(auth!=null) + np.mAuth = auth.get(1); + + + Vector<String> ca = getOption("ca",1,1); + if(ca!=null){ + np.mCaFilename = ca.get(1); + } + + Vector<String> cert = getOption("cert",1,1); + if(cert!=null){ + np.mClientCertFilename = cert.get(1); + np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES; + noauthtypeset=false; + } + Vector<String> key= getOption("key",1,1); + if(key!=null) + np.mClientKeyFilename=key.get(1); + + Vector<String> pkcs12 = getOption("pkcs12",1,1); + if(pkcs12!=null) { + np.mPKCS12Filename = pkcs12.get(1); + np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE; + noauthtypeset=false; + } + + + Vector<String> compatnames = getOption("compat-names",1,2); + Vector<String> nonameremapping = getOption("no-name-remapping",1,1); + Vector<String> tlsremote = getOption("tls-remote",1,1); + if(tlsremote!=null){ + np.mRemoteCN = tlsremote.get(1); + np.mCheckRemoteCN=true; + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE; + + if((compatnames!=null && compatnames.size() > 2) || + (nonameremapping!=null)) + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING; + } + + Vector<String> verifyx509name = getOption("verify-x509-name",1,2); + if(verifyx509name!=null){ + np.mRemoteCN = verifyx509name.get(1); + np.mCheckRemoteCN=true; + if(verifyx509name.size()>2) { + if (verifyx509name.get(2).equals("name")) + np.mX509AuthType=VpnProfile.X509_VERIFY_TLSREMOTE_RDN; + else if (verifyx509name.get(2).equals("name-prefix")) + np.mX509AuthType=VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX; + else + throw new ConfigParseError("Unknown parameter to x509-verify-name: " + verifyx509name.get(2) ); + } else { + np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN; + } + + } + + + Vector<String> verb = getOption("verb",1,1); + if(verb!=null){ + np.mVerb=verb.get(1); + } + + + if(getOption("nobind", 0, 0) != null) + np.mNobind=true; + + if(getOption("persist-tun", 0,0) != null) + np.mPersistTun=true; + + Vector<String> connectretry = getOption("connect-retry", 1, 1); + if(connectretry!=null) + np.mConnectRetry =connectretry.get(1); + + Vector<String> connectretrymax = getOption("connect-retry-max", 1, 1); + if(connectretrymax!=null) + np.mConnectRetryMax =connectretrymax.get(1); + + Vector<Vector<String>> remotetls = getAllOption("remote-cert-tls", 1, 1); + if(remotetls!=null) + if(remotetls.get(0).get(1).equals("server")) + np.mExpectTLSCert=true; + else + options.put("remotetls",remotetls); + + Vector<String> authuser = getOption("auth-user-pass",0,1); + if(authuser !=null){ + if(noauthtypeset) { + np.mAuthenticationType=VpnProfile.TYPE_USERPASS; + } else if(np.mAuthenticationType==VpnProfile.TYPE_CERTIFICATES) { + np.mAuthenticationType=VpnProfile.TYPE_USERPASS_CERTIFICATES; + } else if(np.mAuthenticationType==VpnProfile.TYPE_KEYSTORE) { + np.mAuthenticationType=VpnProfile.TYPE_USERPASS_KEYSTORE; + } + if(authuser.size()>1) { + // Set option value to password get to get cance to embed later. + np.mUsername=null; + np.mPassword=authuser.get(1); + useEmbbedUserAuth(np,authuser.get(1)); + } + } + + // Parse OpenVPN Access Server extra + Vector<String> friendlyname = meta.get("FRIENDLY_NAME"); + if(friendlyname !=null && friendlyname.size() > 1) + np.mName=friendlyname.get(1); + + + Vector<String> ocusername = meta.get("USERNAME"); + if(ocusername !=null && ocusername.size() > 1) + np.mUsername=ocusername.get(1); + + // Check the other options + if(remotes !=null && remotes.size()>1 && extraRemotesAsCustom) { + // first is already added + remotes.remove(0); + np.mCustomConfigOptions += getOptionStrings(remotes); + np.mUseCustomConfig=true; + + } + checkIgnoreAndInvalidOptions(np); + fixup(np); + + return np; + } + + public void useExtraRemotesAsCustom(boolean b) { + this.extraRemotesAsCustom = b; + } + + private boolean isUdpProto(String proto) throws ConfigParseError { + boolean isudp; + if(proto.equals("udp") || proto.equals("udp6")) + isudp=true; + else if (proto.equals("tcp-client") || + proto.equals("tcp") || + proto.equals("tcp6") || + proto.endsWith("tcp6-client")) + isudp =false; + else + throw new ConfigParseError("Unsupported option to --proto " + proto); + return isudp; + } + + static public void useEmbbedUserAuth(VpnProfile np,String inlinedata) + { + String data = inlinedata.replace(VpnProfile.INLINE_TAG, ""); + String[] parts = data.split("\n"); + if(parts.length >= 2) { + np.mUsername=parts[0]; + np.mPassword=parts[1]; + } + } + + private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError { + for(String option:unsupportedOptions) + if(options.containsKey(option)) + throw new ConfigParseError(String.format("Unsupported Option %s encountered in config file. Aborting",option)); + + for(String option:ignoreOptions) + // removing an item which is not in the map is no error + options.remove(option); + + if(options.size()> 0) { + np.mCustomConfigOptions += "# These Options were found in the config file do not map to config settings:\n"; + + for(Vector<Vector<String>> option:options.values()) { + np.mCustomConfigOptions += getOptionStrings(option); + + } + np.mUseCustomConfig=true; + + } + } + + private String getOptionStrings( Vector<Vector<String>> option) { + String custom=""; + for(Vector<String> optionsline: option) { + for (String arg : optionsline) + custom+= VpnProfile.openVpnEscape(arg) + " "; + custom+="\n"; + } + return custom; + } + + + private void fixup(VpnProfile np) { + if(np.mRemoteCN.equals(np.mServerName)) { + np.mRemoteCN=""; + } + } + + private Vector<String> getOption(String option, int minarg, int maxarg) throws ConfigParseError { + Vector<Vector<String>> alloptions = getAllOption(option, minarg, maxarg); + if(alloptions==null) + return null; + else + return alloptions.lastElement(); + } + + + private Vector<Vector<String>> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError { + Vector<Vector<String>> args = options.get(option); + if(args==null) + return null; + + for(Vector<String> optionline:args) + + if(optionline.size()< (minarg+1) || optionline.size() > maxarg+1) { + String err = String.format(Locale.getDefault(),"Option %s has %d parameters, expected between %d and %d", + option,optionline.size()-1,minarg,maxarg ); + throw new ConfigParseError(err); + } + options.remove(option); + return args; + } + +} + + + + diff --git a/src/de/blinkt/openvpn/core/NetworkSateReceiver.java b/src/de/blinkt/openvpn/core/NetworkSateReceiver.java new file mode 100644 index 00000000..aa828495 --- /dev/null +++ b/src/de/blinkt/openvpn/core/NetworkSateReceiver.java @@ -0,0 +1,90 @@ +package de.blinkt.openvpn.core;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.preference.PreferenceManager;
+import de.blinkt.openvpn.R;
+
+public class NetworkSateReceiver extends BroadcastReceiver {
+ private int lastNetwork=-1;
+ private OpenVPNMangement mManangement;
+
+ private String lastStateMsg=null;
+
+ public NetworkSateReceiver(OpenVPNMangement magnagement) {
+ super();
+ mManangement = magnagement;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NetworkInfo networkInfo = getCurrentNetworkInfo(context);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean sendusr1 = prefs.getBoolean("netchangereconnect", true);
+
+ String netstatestring;
+ if(networkInfo==null)
+ netstatestring = "not connected";
+ else {
+ String subtype = networkInfo.getSubtypeName();
+ if(subtype==null)
+ subtype = "";
+ String extrainfo = networkInfo.getExtraInfo();
+ if(extrainfo==null)
+ extrainfo="";
+
+ /*
+ if(networkInfo.getType()==android.net.ConnectivityManager.TYPE_WIFI) {
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiinfo = wifiMgr.getConnectionInfo();
+ extrainfo+=wifiinfo.getBSSID();
+
+ subtype += wifiinfo.getNetworkId();
+ }*/
+
+
+
+ netstatestring = String.format("%2$s %4$s to %1$s %3$s",networkInfo.getTypeName(),
+ networkInfo.getDetailedState(),extrainfo,subtype );
+ }
+
+
+
+ if(networkInfo!=null && networkInfo.getState() == State.CONNECTED) {
+ int newnet = networkInfo.getType();
+
+ if(sendusr1 && lastNetwork!=newnet) {
+ if (lastNetwork==-1)
+ mManangement.resume();
+ else
+ mManangement.reconnect();
+ }
+
+ lastNetwork = newnet;
+ } else if (networkInfo==null) {
+ // Not connected, stop openvpn, set last connected network to no network
+ lastNetwork=-1;
+ if(sendusr1)
+ mManangement.pause();
+ }
+
+ if(!netstatestring.equals(lastStateMsg))
+ OpenVPN.logInfo(R.string.netstatus, netstatestring);
+ lastStateMsg=netstatestring;
+
+ }
+
+ private NetworkInfo getCurrentNetworkInfo(Context context) {
+ ConnectivityManager conn = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ NetworkInfo networkInfo = conn.getActiveNetworkInfo();
+ return networkInfo;
+ }
+
+}
diff --git a/src/de/blinkt/openvpn/core/OpenVPN.java b/src/de/blinkt/openvpn/core/OpenVPN.java new file mode 100644 index 00000000..aba3ef0c --- /dev/null +++ b/src/de/blinkt/openvpn/core/OpenVPN.java @@ -0,0 +1,427 @@ +package de.blinkt.openvpn.core; + +import java.io.ByteArrayInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Vector; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.Signature; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import de.blinkt.openvpn.R; + +public class OpenVPN { + + + public static LinkedList<LogItem> logbuffer; + + private static Vector<LogListener> logListener; + private static Vector<StateListener> stateListener; + private static Vector<ByteCountListener> byteCountListener; + + private static String[] mBconfig; + + private static String mLaststatemsg=""; + + private static String mLaststate = "NOPROCESS"; + + private static int mLastStateresid=R.string.state_noprocess; + + private static long mlastByteCount[]={0,0,0,0}; + + + + public enum ConnectionStatus { + LEVEL_NONETWORK (3), + LEVEL_NOTCONNECTED (4), + LEVEL_AUTH_FAILED ( 5), + LEVEL_CONNECTING_SERVER_REPLIED ( 1), + LEVEL_CONNECTING_NO_SERVER_REPLY_YET (2), + LEVEL_CONNECTED (0), UNKNOWN_LEVEL(-1); + + public final int level; + + ConnectionStatus(int level){ + this.level = level; + } + } + + public static final byte[] officalkey = {-58, -42, -44, -106, 90, -88, -87, -88, -52, -124, 84, 117, 66, 79, -112, -111, -46, 86, -37, 109}; + public static final byte[] officaldebugkey = {-99, -69, 45, 71, 114, -116, 82, 66, -99, -122, 50, -70, -56, -111, 98, -35, -65, 105, 82, 43}; + public static final byte[] amazonkey = {-116, -115, -118, -89, -116, -112, 120, 55, 79, -8, -119, -23, 106, -114, -85, -56, -4, 105, 26, -57}; + + private static ConnectionStatus mLastLevel=ConnectionStatus.LEVEL_NOTCONNECTED; + + static { + logbuffer = new LinkedList<LogItem>(); + logListener = new Vector<OpenVPN.LogListener>(); + stateListener = new Vector<OpenVPN.StateListener>(); + byteCountListener = new Vector<OpenVPN.ByteCountListener>(); + logInformation(); + } + + + public static class LogItem implements Parcelable { + public static final int ERROR = 1; + public static final int INFO = 2; + public static final int VERBOSE = 3; + + private Object [] mArgs = null; + private String mMessage = null; + private int mRessourceId; + // Default log priority + int mLevel = INFO; + private long logtime = System.currentTimeMillis(); + + public LogItem(int ressourceId, Object[] args) { + mRessourceId = ressourceId; + mArgs = args; + } + + @Override + public int describeContents() { + return 0; + } + + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeArray(mArgs); + dest.writeString(mMessage); + dest.writeInt(mRessourceId); + dest.writeInt(mLevel); + dest.writeLong(logtime); + } + + public LogItem(Parcel in) { + mArgs = in.readArray(Object.class.getClassLoader()); + mMessage = in.readString(); + mRessourceId = in.readInt(); + mLevel = in.readInt(); + logtime = in.readLong(); + } + + public static final Parcelable.Creator<LogItem> CREATOR + = new Parcelable.Creator<LogItem>() { + public LogItem createFromParcel(Parcel in) { + return new LogItem(in); + } + + public LogItem[] newArray(int size) { + return new LogItem[size]; + } + }; + + public LogItem(int loglevel,int ressourceId, Object[] args) { + mRessourceId = ressourceId; + mArgs = args; + mLevel = loglevel; + } + + + public LogItem(String message) { + mMessage = message; + } + + public LogItem(int loglevel, String msg) { + mLevel = loglevel; + mMessage = msg; + } + + + public LogItem(int loglevel, int ressourceId) { + mRessourceId =ressourceId; + mLevel = loglevel; + } + + public String getString(Context c) { + if(mMessage !=null) { + return mMessage; + } else { + if(c!=null) { + if(mRessourceId==R.string.mobile_info) + return getMobileInfoString(c); + if(mArgs == null) + return c.getString(mRessourceId); + else + return c.getString(mRessourceId,mArgs); + } else { + String str = String.format(Locale.ENGLISH,"Log (no context) resid %d", mRessourceId); + if(mArgs !=null) + for(Object o:mArgs) + str += "|" + o.toString(); + + return str; + } + } + } + + // The lint is wrong here + @SuppressLint("StringFormatMatches") + private String getMobileInfoString(Context c) { + c.getPackageManager(); + String apksign="error getting package signature"; + + String version="error getting version"; + try { + Signature raw = c.getPackageManager().getPackageInfo(c.getPackageName(), PackageManager.GET_SIGNATURES).signatures[0]; + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(raw.toByteArray())); + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + + if (Arrays.equals(digest, officalkey)) + apksign = c.getString(R.string.official_build); + else if (Arrays.equals(digest, officaldebugkey)) + apksign = c.getString(R.string.debug_build); + else if (Arrays.equals(digest, amazonkey)) + apksign = "amazon version"; + else + apksign = c.getString(R.string.built_by,cert.getSubjectX500Principal().getName()); + + PackageInfo packageinfo = c.getPackageManager().getPackageInfo(c.getPackageName(), 0); + version = packageinfo.versionName; + + } catch (NameNotFoundException e) { + } catch (CertificateException e) { + } catch (NoSuchAlgorithmException e) { + } + + Object[] argsext = Arrays.copyOf(mArgs, mArgs.length+2); + argsext[argsext.length-1]=apksign; + argsext[argsext.length-2]=version; + + return c.getString(R.string.mobile_info_extended, argsext); + + } + + public long getLogtime() { + return logtime; + } + + + } + + private static final int MAXLOGENTRIES = 500; + + public static final String MANAGMENT_PREFIX = "M:"; + + + public interface LogListener { + void newLog(LogItem logItem); + } + + public interface StateListener { + void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level); + } + + public interface ByteCountListener { + void updateByteCount(long in, long out, long diffin, long diffout); + } + + public synchronized static void logMessage(int level,String prefix, String message) + { + newlogItem(new LogItem(prefix + message)); + + } + + public synchronized static void clearLog() { + logbuffer.clear(); + logInformation(); + } + + private static void logInformation() { + + + logInfo(R.string.mobile_info,Build.MODEL, Build.BOARD,Build.BRAND,Build.VERSION.SDK_INT); + } + + public synchronized static void addLogListener(LogListener ll){ + logListener.add(ll); + } + + public synchronized static void removeLogListener(LogListener ll) { + logListener.remove(ll); + } + + public synchronized static void addByteCountListener(ByteCountListener bcl) { + bcl.updateByteCount(mlastByteCount[0], mlastByteCount[1], mlastByteCount[2], mlastByteCount[3]); + byteCountListener.add(bcl); + } + + public synchronized static void removeByteCountListener(ByteCountListener bcl) { + byteCountListener.remove(bcl); + } + + + public synchronized static void addStateListener(StateListener sl){ + if(!stateListener.contains(sl)){ + stateListener.add(sl); + if(mLaststate!=null) + sl.updateState(mLaststate, mLaststatemsg, mLastStateresid, mLastLevel); + } + } + + private static int getLocalizedState(String state){ + if (state.equals("CONNECTING")) + return R.string.state_connecting; + else if (state.equals("WAIT")) + return R.string.state_wait; + else if (state.equals("AUTH")) + return R.string.state_auth; + else if (state.equals("GET_CONFIG")) + return R.string.state_get_config; + else if (state.equals("ASSIGN_IP")) + return R.string.state_assign_ip; + else if (state.equals("ADD_ROUTES")) + return R.string.state_add_routes; + else if (state.equals("CONNECTED")) + return R.string.state_connected; + else if (state.equals("DISCONNECTED")) + return R.string.state_disconnected; + else if (state.equals("RECONNECTING")) + return R.string.state_reconnecting; + else if (state.equals("EXITING")) + return R.string.state_exiting; + else if (state.equals("RESOLVE")) + return R.string.state_resolve; + else if (state.equals("TCP_CONNECT")) + return R.string.state_tcp_connect; + else + return R.string.unknown_state; + + } + + private static ConnectionStatus getLevel(String state){ + String[] noreplyet = {"CONNECTING","WAIT", "RECONNECTING", "RESOLVE", "TCP_CONNECT"}; + String[] reply = {"AUTH","GET_CONFIG","ASSIGN_IP","ADD_ROUTES"}; + String[] connected = {"CONNECTED"}; + String[] notconnected = {"DISCONNECTED", "EXITING"}; + + for(String x:noreplyet) + if(state.equals(x)) + return ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET; + + for(String x:reply) + if(state.equals(x)) + return ConnectionStatus.LEVEL_CONNECTING_SERVER_REPLIED; + + for(String x:connected) + if(state.equals(x)) + return ConnectionStatus.LEVEL_CONNECTED; + + for(String x:notconnected) + if(state.equals(x)) + return ConnectionStatus.LEVEL_NOTCONNECTED; + + return ConnectionStatus.UNKNOWN_LEVEL; + + } + + + + + public synchronized static void removeStateListener(StateListener sl) { + stateListener.remove(sl); + } + + + synchronized public static LogItem[] getlogbuffer() { + + // The stoned way of java to return an array from a vector + // brought to you by eclipse auto complete + return (LogItem[]) logbuffer.toArray(new LogItem[logbuffer.size()]); + + } + public static void logBuilderConfig(String[] bconfig) { + mBconfig = bconfig; + } + public static void triggerLogBuilderConfig() { + if(mBconfig==null) { + logMessage(0, "", "No active interface"); + } else { + for (String item : mBconfig) { + logMessage(0, "", item); + } + } + + } + + public static void updateStateString (String state, String msg) { + int rid = getLocalizedState(state); + ConnectionStatus level = getLevel(state); + updateStateString(state, msg, rid, level); + } + + public synchronized static void updateStateString(String state, String msg, int resid, ConnectionStatus level) { + mLaststate= state; + mLaststatemsg = msg; + mLastStateresid = resid; + mLastLevel = level; + + for (StateListener sl : stateListener) { + sl.updateState(state,msg,resid,level); + } + } + + public static void logInfo(String message) { + newlogItem(new LogItem(LogItem.INFO, message)); + } + + public static void logInfo(int ressourceId, Object... args) { + newlogItem(new LogItem(LogItem.INFO, ressourceId, args)); + } + + private synchronized static void newlogItem(LogItem logItem) { + logbuffer.addLast(logItem); + if(logbuffer.size()>MAXLOGENTRIES) + logbuffer.removeFirst(); + + for (LogListener ll : logListener) { + ll.newLog(logItem); + } + } + + public static void logError(String msg) { + newlogItem(new LogItem(LogItem.ERROR, msg)); + + } + + public static void logError(int ressourceId) { + newlogItem(new LogItem(LogItem.ERROR, ressourceId)); + } + public static void logError(int ressourceId, Object... args) { + newlogItem(new LogItem(LogItem.ERROR, ressourceId,args)); + } + + public static synchronized void updateByteCount(long in, long out) { + long lastIn = mlastByteCount[0]; + long lastOut = mlastByteCount[1]; + long diffin = mlastByteCount[2] = in - lastIn; + long diffout = mlastByteCount[3] = out - lastOut; + + + + mlastByteCount = new long[] {in,out,diffin,diffout}; + for(ByteCountListener bcl:byteCountListener){ + bcl.updateByteCount(in, out, diffin,diffout); + } + } + + + +} diff --git a/src/de/blinkt/openvpn/core/OpenVPNMangement.java b/src/de/blinkt/openvpn/core/OpenVPNMangement.java new file mode 100644 index 00000000..a1334ac2 --- /dev/null +++ b/src/de/blinkt/openvpn/core/OpenVPNMangement.java @@ -0,0 +1,14 @@ +package de.blinkt.openvpn.core; + +public interface OpenVPNMangement { + int mBytecountinterval=2; + + void reconnect(); + + void pause(); + + void resume(); + + boolean stopVPN(); + +} diff --git a/src/de/blinkt/openvpn/core/OpenVPNThread.java b/src/de/blinkt/openvpn/core/OpenVPNThread.java new file mode 100644 index 00000000..9d6d8e77 --- /dev/null +++ b/src/de/blinkt/openvpn/core/OpenVPNThread.java @@ -0,0 +1,136 @@ +package de.blinkt.openvpn.core;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.Locale;
+
+import android.util.Log;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.OpenVPN.ConnectionStatus;
+import de.blinkt.openvpn.core.OpenVPN.LogItem;
+
+public class OpenVPNThread implements Runnable {
+ private static final String DUMP_PATH_STRING = "Dump path: ";
+ private static final String TAG = "OpenVPN";
+ private String[] mArgv;
+ private Process mProcess;
+ private String mNativeDir;
+ private OpenVpnService mService;
+ private String mDumpPath;
+
+ public OpenVPNThread(OpenVpnService service,String[] argv, String nativelibdir)
+ {
+ mArgv = argv;
+ mNativeDir = nativelibdir;
+ mService = service;
+ }
+
+ public void stopProcess() {
+ mProcess.destroy();
+ }
+
+
+
+ @Override
+ public void run() {
+ try {
+ Log.i(TAG, "Starting openvpn");
+ startOpenVPNThreadArgs(mArgv);
+ Log.i(TAG, "Giving up");
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG, "OpenVPNThread Got " + e.toString());
+ } finally {
+ int exitvalue = 0;
+ try {
+ exitvalue = mProcess.waitFor();
+ } catch ( IllegalThreadStateException ite) {
+ OpenVPN.logError("Illegal Thread state: " + ite.getLocalizedMessage());
+ } catch (InterruptedException ie) {
+ OpenVPN.logError("InterruptedException: " + ie.getLocalizedMessage());
+ }
+ if( exitvalue != 0)
+ OpenVPN.logError("Process exited with exit value " + exitvalue);
+
+ OpenVPN.updateStateString("NOPROCESS","No process running.", R.string.state_noprocess,ConnectionStatus.LEVEL_NOTCONNECTED);
+ if(mDumpPath!=null) {
+ try {
+ BufferedWriter logout = new BufferedWriter(new FileWriter(mDumpPath + ".log"));
+ SimpleDateFormat timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.GERMAN);
+ for(LogItem li :OpenVPN.getlogbuffer()){
+ String time = timeformat.format(new Date(li.getLogtime()));
+ logout.write(time +" " + li.getString(mService) + "\n");
+ }
+ logout.close();
+ OpenVPN.logError(R.string.minidump_generated);
+ } catch (IOException e) {
+ OpenVPN.logError("Writing minidump log: " +e.getLocalizedMessage());
+ }
+ }
+
+ mService.processDied();
+ Log.i(TAG, "Exiting");
+ }
+ }
+
+ private void startOpenVPNThreadArgs(String[] argv) {
+ LinkedList<String> argvlist = new LinkedList<String>();
+
+ for(String arg:argv)
+ argvlist.add(arg);
+
+ ProcessBuilder pb = new ProcessBuilder(argvlist);
+ // Hack O rama
+
+ // Hack until I find a good way to get the real library path
+ String applibpath = argv[0].replace("/cache/" + VpnProfile.MINIVPN , "/lib");
+
+ String lbpath = pb.environment().get("LD_LIBRARY_PATH");
+ if(lbpath==null)
+ lbpath = applibpath;
+ else
+ lbpath = lbpath + ":" + applibpath;
+
+ if (!applibpath.equals(mNativeDir)) {
+ lbpath = lbpath + ":" + mNativeDir;
+ }
+
+ pb.environment().put("LD_LIBRARY_PATH", lbpath);
+ pb.redirectErrorStream(true);
+ try {
+ mProcess = pb.start();
+ // Close the output, since we don't need it
+ mProcess.getOutputStream().close();
+ InputStream in = mProcess.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(in));
+
+ while(true) {
+ String logline = br.readLine();
+ if(logline==null)
+ return;
+
+ if (logline.startsWith(DUMP_PATH_STRING))
+ mDumpPath = logline.substring(DUMP_PATH_STRING.length());
+
+
+ OpenVPN.logMessage(0, "P:", logline);
+ }
+
+
+ } catch (IOException e) {
+ OpenVPN.logMessage(0, "", "Error reading from output of OpenVPN process"+ e.getLocalizedMessage());
+ e.printStackTrace();
+ stopProcess();
+ }
+
+
+ }
+}
diff --git a/src/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/src/de/blinkt/openvpn/core/OpenVpnManagementThread.java new file mode 100644 index 00000000..a44f744e --- /dev/null +++ b/src/de/blinkt/openvpn/core/OpenVpnManagementThread.java @@ -0,0 +1,482 @@ +package de.blinkt.openvpn.core;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Vector;
+
+import android.content.SharedPreferences;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.os.ParcelFileDescriptor;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import de.blinkt.openvpn.R;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.OpenVPN.ConnectionStatus;
+
+public class OpenVpnManagementThread implements Runnable, OpenVPNMangement {
+
+ private static final String TAG = "openvpn";
+ private LocalSocket mSocket;
+ private VpnProfile mProfile;
+ private OpenVpnService mOpenVPNService;
+ private LinkedList<FileDescriptor> mFDList=new LinkedList<FileDescriptor>();
+ private LocalServerSocket mServerSocket;
+ private boolean mReleaseHold=true;
+ private boolean mWaitingForRelease=false;
+ private long mLastHoldRelease=0;
+
+ private static Vector<OpenVpnManagementThread> active=new Vector<OpenVpnManagementThread>();
+
+ static private native void jniclose(int fdint);
+
+ public OpenVpnManagementThread(VpnProfile profile, LocalServerSocket mgmtsocket, OpenVpnService openVpnService) {
+ mProfile = profile;
+ mServerSocket = mgmtsocket;
+ mOpenVPNService = openVpnService;
+
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(openVpnService);
+ boolean managemeNetworkState = prefs.getBoolean("netchangereconnect", true);
+ if(managemeNetworkState)
+ mReleaseHold=false;
+
+ }
+
+ static {
+ System.loadLibrary("opvpnutil");
+ }
+
+ public void managmentCommand(String cmd) {
+ if(mSocket!=null) {
+ try {
+ mSocket.getOutputStream().write(cmd.getBytes());
+ mSocket.getOutputStream().flush();
+ } catch (IOException e) {
+ // Ignore socket stack traces
+ }
+ }
+ }
+
+
+ @Override
+ public void run() {
+ Log.i(TAG, "Managment Socket Thread started");
+ byte [] buffer =new byte[2048];
+ // mSocket.setSoTimeout(5); // Setting a timeout cannot be that bad
+
+ String pendingInput="";
+ active.add(this);
+
+ try {
+ // Wait for a client to connect
+ mSocket= mServerSocket.accept();
+ InputStream instream = mSocket.getInputStream();
+
+ while(true) {
+ int numbytesread = instream.read(buffer);
+ if(numbytesread==-1)
+ return;
+
+ FileDescriptor[] fds = null;
+ try {
+ fds = mSocket.getAncillaryFileDescriptors();
+ } catch (IOException e) {
+ OpenVPN.logMessage(0, "", "Error reading fds from socket" + e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ if(fds!=null){
+
+ for (FileDescriptor fd : fds) {
+
+ mFDList.add(fd);
+ }
+ }
+
+ String input = new String(buffer,0,numbytesread,"UTF-8");
+
+ pendingInput += input;
+
+ pendingInput=processInput(pendingInput);
+
+
+
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ active.remove(this);
+ }
+
+ //! Hack O Rama 2000!
+ private void protectFileDescriptor(FileDescriptor fd) {
+ Exception exp=null;
+ try {
+ Method getInt = FileDescriptor.class.getDeclaredMethod("getInt$");
+ int fdint = (Integer) getInt.invoke(fd);
+
+ // You can even get more evil by parsing toString() and extract the int from that :)
+
+ mOpenVPNService.protect(fdint);
+
+ //ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(fdint);
+ //pfd.close();
+ jniclose(fdint);
+ return;
+ } catch (NoSuchMethodException e) {
+ exp =e;
+ } catch (IllegalArgumentException e) {
+ exp =e;
+ } catch (IllegalAccessException e) {
+ exp =e;
+ } catch (InvocationTargetException e) {
+ exp =e;
+ } catch (NullPointerException e) {
+ exp =e;
+ }
+ if(exp!=null) {
+ exp.printStackTrace();
+ Log.d("Openvpn", "Failed to retrieve fd from socket: " + fd);
+ OpenVPN.logMessage(0, "", "Failed to retrieve fd from socket: " + exp.getLocalizedMessage());
+ }
+ }
+
+ private String processInput(String pendingInput) {
+
+
+ while(pendingInput.contains("\n")) {
+ String[] tokens = pendingInput.split("\\r?\\n", 2);
+ processCommand(tokens[0]);
+ if(tokens.length == 1)
+ // No second part, newline was at the end
+ pendingInput="";
+ else
+ pendingInput=tokens[1];
+ }
+ return pendingInput;
+ }
+
+
+ private void processCommand(String command) {
+ if (command.startsWith(">") && command.contains(":")) {
+ String[] parts = command.split(":",2);
+ String cmd = parts[0].substring(1);
+ String argument = parts[1];
+
+
+ if(cmd.equals("INFO")) {
+ // Ignore greeting from mgmt
+ //logStatusMessage(command);
+ }else if (cmd.equals("PASSWORD")) {
+ processPWCommand(argument);
+ } else if (cmd.equals("HOLD")) {
+ handleHold();
+ } else if (cmd.equals("NEED-OK")) {
+ processNeedCommand(argument);
+ } else if (cmd.equals("BYTECOUNT")){
+ processByteCount(argument);
+ } else if (cmd.equals("STATE")) {
+ processState(argument);
+ } else if (cmd.equals("PROXY")) {
+ processProxyCMD(argument);
+ } else if (cmd.equals("LOG")) {
+ String[] args = argument.split(",",3);
+ // 0 unix time stamp
+ // 1 log level N,I,E etc.
+ // 2 log message
+ OpenVPN.logMessage(0, "", args[2]);
+ } else if (cmd.equals("RSA_SIGN")) {
+ processSignCommand(argument);
+ } else {
+ OpenVPN.logMessage(0, "MGMT:", "Got unrecognized command" + command);
+ Log.i(TAG, "Got unrecognized command" + command);
+ }
+ } else if (command.startsWith("SUCCESS:")) {
+ // ignore
+ } else {
+ Log.i(TAG, "Got unrecognized line from managment" + command);
+ OpenVPN.logMessage(0, "MGMT:", "Got unrecognized line from management:" + command);
+ }
+ }
+ private void handleHold() {
+ if(mReleaseHold) {
+ releaseHoldCmd();
+ } else {
+ mWaitingForRelease=true;
+ OpenVPN.updateStateString("NONETWORK", "",R.string.state_nonetwork,ConnectionStatus.LEVEL_NONETWORK);
+ }
+ }
+ private void releaseHoldCmd() {
+ if ((System.currentTimeMillis()- mLastHoldRelease) < 5000) {
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {}
+
+ }
+ mWaitingForRelease=false;
+ mLastHoldRelease = System.currentTimeMillis();
+ managmentCommand("hold release\n");
+ managmentCommand("bytecount " + mBytecountinterval + "\n");
+ managmentCommand("state on\n");
+ }
+
+ public void releaseHold() {
+ mReleaseHold=true;
+ if(mWaitingForRelease)
+ releaseHoldCmd();
+
+ }
+
+ private void processProxyCMD(String argument) {
+ String[] args = argument.split(",",3);
+ SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile);
+
+
+ if(args.length >= 2) {
+ String proto = args[1];
+ if(proto.equals("UDP")) {
+ proxyaddr=null;
+ }
+ }
+
+ if(proxyaddr instanceof InetSocketAddress ){
+ InetSocketAddress isa = (InetSocketAddress) proxyaddr;
+
+ OpenVPN.logInfo(R.string.using_proxy, isa.getHostName(),isa.getPort());
+
+ String proxycmd = String.format(Locale.ENGLISH,"proxy HTTP %s %d\n", isa.getHostName(),isa.getPort());
+ managmentCommand(proxycmd);
+ } else {
+ managmentCommand("proxy NONE\n");
+ }
+
+ }
+ private void processState(String argument) {
+ String[] args = argument.split(",",3);
+ String currentstate = args[1];
+ if(args[2].equals(",,"))
+ OpenVPN.updateStateString(currentstate,"");
+ else
+ OpenVPN.updateStateString(currentstate,args[2]);
+ }
+
+
+ private void processByteCount(String argument) {
+ // >BYTECOUNT:{BYTES_IN},{BYTES_OUT}
+ int comma = argument.indexOf(',');
+ long in = Long.parseLong(argument.substring(0, comma));
+ long out = Long.parseLong(argument.substring(comma+1));
+
+ OpenVPN.updateByteCount(in,out);
+
+ }
+
+
+
+ private void processNeedCommand(String argument) {
+ int p1 =argument.indexOf('\'');
+ int p2 = argument.indexOf('\'',p1+1);
+
+ String needed = argument.substring(p1+1, p2);
+ String extra = argument.split(":",2)[1];
+
+ String status = "ok";
+
+
+ if (needed.equals("PROTECTFD")) {
+ FileDescriptor fdtoprotect = mFDList.pollFirst();
+ protectFileDescriptor(fdtoprotect);
+ } else if (needed.equals("DNSSERVER")) {
+ mOpenVPNService.addDNS(extra);
+ }else if (needed.equals("DNSDOMAIN")){
+ mOpenVPNService.setDomain(extra);
+ } else if (needed.equals("ROUTE")) {
+ String[] routeparts = extra.split(" ");
+ mOpenVPNService.addRoute(routeparts[0], routeparts[1]);
+ } else if (needed.equals("ROUTE6")) {
+ mOpenVPNService.addRoutev6(extra);
+ } else if (needed.equals("IFCONFIG")) {
+ String[] ifconfigparts = extra.split(" ");
+ int mtu = Integer.parseInt(ifconfigparts[2]);
+ mOpenVPNService.setLocalIP(ifconfigparts[0], ifconfigparts[1],mtu,ifconfigparts[3]);
+ } else if (needed.equals("IFCONFIG6")) {
+ mOpenVPNService.setLocalIPv6(extra);
+
+ } else if (needed.equals("OPENTUN")) {
+ if(sendTunFD(needed,extra))
+ return;
+ else
+ status="cancel";
+ // This not nice or anything but setFileDescriptors accepts only FilDescriptor class :(
+
+ } else {
+ Log.e(TAG,"Unkown needok command " + argument);
+ return;
+ }
+
+ String cmd = String.format("needok '%s' %s\n", needed, status);
+ managmentCommand(cmd);
+ }
+
+ private boolean sendTunFD (String needed, String extra) {
+ Exception exp = null;
+ if(!extra.equals("tun")) {
+ // We only support tun
+ String errmsg = String.format("Devicetype %s requested, but only tun is possible with the Android API, sorry!",extra);
+ OpenVPN.logMessage(0, "", errmsg );
+
+ return false;
+ }
+ ParcelFileDescriptor pfd = mOpenVPNService.openTun();
+ if(pfd==null)
+ return false;
+
+ Method setInt;
+ int fdint = pfd.getFd();
+ try {
+ setInt = FileDescriptor.class.getDeclaredMethod("setInt$",int.class);
+ FileDescriptor fdtosend = new FileDescriptor();
+
+ setInt.invoke(fdtosend,fdint);
+
+ FileDescriptor[] fds = {fdtosend};
+ mSocket.setFileDescriptorsForSend(fds);
+
+ Log.d("Openvpn", "Sending FD tosocket: " + fdtosend + " " + fdint + " " + pfd);
+ // Trigger a send so we can close the fd on our side of the channel
+ // The API documentation fails to mention that it will not reset the file descriptor to
+ // be send and will happily send the file descriptor on every write ...
+ String cmd = String.format("needok '%s' %s\n", needed, "ok");
+ managmentCommand(cmd);
+
+ // Set the FileDescriptor to null to stop this mad behavior
+ mSocket.setFileDescriptorsForSend(null);
+
+ pfd.close();
+
+ return true;
+ } catch (NoSuchMethodException e) {
+ exp =e;
+ } catch (IllegalArgumentException e) {
+ exp =e;
+ } catch (IllegalAccessException e) {
+ exp =e;
+ } catch (InvocationTargetException e) {
+ exp =e;
+ } catch (IOException e) {
+ exp =e;
+ }
+ if(exp!=null) {
+ OpenVPN.logMessage(0,"", "Could not send fd over socket:" + exp.getLocalizedMessage());
+ exp.printStackTrace();
+ }
+ return false;
+ }
+
+ private void processPWCommand(String argument) {
+ //argument has the form Need 'Private Key' password
+ // or ">PASSWORD:Verification Failed: '%s' ['%s']"
+ String needed;
+
+
+
+ try{
+
+ int p1 = argument.indexOf('\'');
+ int p2 = argument.indexOf('\'',p1+1);
+ needed = argument.substring(p1+1, p2);
+ if (argument.startsWith("Verification Failed")) {
+ proccessPWFailed(needed, argument.substring(p2+1));
+ return;
+ }
+ } catch (StringIndexOutOfBoundsException sioob) {
+ OpenVPN.logMessage(0, "", "Could not parse management Password command: " + argument);
+ return;
+ }
+
+ String pw=null;
+
+ if(needed.equals("Private Key")) {
+ pw = mProfile.getPasswordPrivateKey();
+ } else if (needed.equals("Auth")) {
+ String usercmd = String.format("username '%s' %s\n",
+ needed, VpnProfile.openVpnEscape(mProfile.mUsername));
+ managmentCommand(usercmd);
+ pw = mProfile.getPasswordAuth();
+ }
+ if(pw!=null) {
+ String cmd = String.format("password '%s' %s\n", needed, VpnProfile.openVpnEscape(pw));
+ managmentCommand(cmd);
+ } else {
+ OpenVPN.logMessage(0, OpenVPN.MANAGMENT_PREFIX, String.format("Openvpn requires Authentication type '%s' but no password/key information available", needed));
+ }
+
+ }
+
+
+
+
+ private void proccessPWFailed(String needed, String args) {
+ OpenVPN.updateStateString("AUTH_FAILED", needed + args,R.string.state_auth_failed,ConnectionStatus.LEVEL_AUTH_FAILED);
+ }
+ private void logStatusMessage(String command) {
+ OpenVPN.logMessage(0,"MGMT:", command);
+ }
+
+
+ private static boolean stopOpenVPN() {
+ boolean sendCMD=false;
+ for (OpenVpnManagementThread mt: active){
+ mt.managmentCommand("signal SIGINT\n");
+ sendCMD=true;
+ try {
+ if(mt.mSocket !=null)
+ mt.mSocket.close();
+ } catch (IOException e) {
+ // Ignore close error on already closed socket
+ }
+ }
+ return sendCMD;
+ }
+
+ public void signalusr1() {
+ mReleaseHold=false;
+ if(!mWaitingForRelease)
+ managmentCommand("signal SIGUSR1\n");
+ }
+
+ public void reconnect() {
+ signalusr1();
+ releaseHold();
+ }
+
+ private void processSignCommand(String b64data) {
+
+ String signed_string = mProfile.getSignedData(b64data);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+ }
+
+ @Override
+ public void pause() {
+ signalusr1();
+ }
+
+ @Override
+ public void resume() {
+ releaseHold();
+ }
+
+ @Override
+ public boolean stopVPN() {
+ return stopOpenVPN();
+ }
+}
diff --git a/src/de/blinkt/openvpn/core/OpenVpnService.java b/src/de/blinkt/openvpn/core/OpenVpnService.java new file mode 100644 index 00000000..a6ec1a1d --- /dev/null +++ b/src/de/blinkt/openvpn/core/OpenVpnService.java @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.blinkt.openvpn.core;import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; +import java.util.Vector; + +import android.Manifest.permission; +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.net.VpnService; +import android.os.Binder; +import android.os.Build; +import android.os.Handler.Callback; +import android.os.IBinder; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.preference.PreferenceManager; +import de.blinkt.openvpn.LogWindow; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.OpenVPN.ByteCountListener; +import de.blinkt.openvpn.core.OpenVPN.ConnectionStatus; +import de.blinkt.openvpn.core.OpenVPN.StateListener; + +public class OpenVpnService extends VpnService implements StateListener, Callback, ByteCountListener { + public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE"; + public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY"; + public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE"; + + + private Thread mProcessThread=null; + + private Vector<String> mDnslist=new Vector<String>(); + + private VpnProfile mProfile; + + private String mDomain=null; + + private Vector<CIDRIP> mRoutes=new Vector<CIDRIP>(); + private Vector<String> mRoutesv6=new Vector<String>(); + + private CIDRIP mLocalIP=null; + + private int mMtu; + private String mLocalIPv6=null; + private NetworkSateReceiver mNetworkStateReceiver; + + private boolean mDisplayBytecount=false; + + private boolean mStarting=false; + + private long mConnecttime; + + + private static final int OPENVPN_STATUS = 1; + + public static final int PROTECT_FD = 0; + + private static boolean mNotificationalwaysVisible=false; + + private final IBinder mBinder = new LocalBinder(); + private boolean mOvpn3; + private Thread mSocketManagerThread; + private OpenVPNMangement mManagement; + + public class LocalBinder extends Binder { + public OpenVpnService getService() { + // Return this instance of LocalService so clients can call public methods + return OpenVpnService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + String action = intent.getAction(); + if( action !=null && action.equals(START_SERVICE)) + return mBinder; + else + return super.onBind(intent); + } + + @Override + public void onRevoke() { + mManagement.stopVPN(); + endVpnService(); + } + + // Similar to revoke but do not try to stop process + public void processDied() { + endVpnService(); + } + + private void endVpnService() { + mProcessThread=null; + OpenVPN.logBuilderConfig(null); + OpenVPN.removeByteCountListener(this); + unregisterNetworkStateReceiver(); + ProfileManager.setConntectedVpnProfileDisconnected(this); + if(!mStarting) { + stopForeground(!mNotificationalwaysVisible); + + if( !mNotificationalwaysVisible) { + stopSelf(); + OpenVPN.removeStateListener(this); + } + } + } + + private void showNotification(String msg, String tickerText, boolean lowpriority, long when, ConnectionStatus level) { + String ns = Context.NOTIFICATION_SERVICE; + NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns); + + + int icon = R.drawable.ic_notification_icon; + android.app.Notification.Builder nbuilder = new Notification.Builder(this); + + if(mProfile!=null) + nbuilder.setContentTitle(getString(R.string.notifcation_title,mProfile.mName)); + else + nbuilder.setContentTitle(getString(R.string.notifcation_title_notconnect)); + + nbuilder.setContentText(msg); + nbuilder.setOnlyAlertOnce(true); + nbuilder.setOngoing(true); + nbuilder.setContentIntent(getLogPendingIntent()); + nbuilder.setSmallIcon(icon,level.level); + if(when !=0) + nbuilder.setWhen(when); + + + // Try to set the priority available since API 16 (Jellybean) + jbNotificationExtras(lowpriority, nbuilder); + if(tickerText!=null && !tickerText.equals("")) + nbuilder.setTicker(tickerText); + + @SuppressWarnings("deprecation") + Notification notification = nbuilder.getNotification(); + + + mNotificationManager.notify(OPENVPN_STATUS, notification); + startForeground(OPENVPN_STATUS, notification); + } + + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void jbNotificationExtras(boolean lowpriority, + android.app.Notification.Builder nbuilder) { + try { + if(lowpriority) { + Method setpriority = nbuilder.getClass().getMethod("setPriority", int.class); + // PRIORITY_MIN == -2 + setpriority.invoke(nbuilder, -2 ); + + Method setUsesChronometer = nbuilder.getClass().getMethod("setUsesChronometer", boolean.class); + setUsesChronometer.invoke(nbuilder,true); + + /* PendingIntent cancelconnet=null; + + nbuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, + getString(R.string.cancel_connection),cancelconnet); */ + } + + //ignore exception + } catch (NoSuchMethodException nsm) { + } catch (IllegalArgumentException e) { + } catch (IllegalAccessException e) { + } catch (InvocationTargetException e) { + } + + } + + PendingIntent getLogPendingIntent() { + // Let the configure Button show the Log + Intent intent = new Intent(getBaseContext(),LogWindow.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + PendingIntent startLW = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + return startLW; + + } + + + private LocalServerSocket openManagmentInterface(int tries) { + // Could take a while to open connection + String socketname = (getCacheDir().getAbsolutePath() + "/" + "mgmtsocket"); + // The sock is transfered to the LocalServerSocket, ignore warning + @SuppressWarnings("resource") + LocalSocket sock = new LocalSocket(); + + while(tries > 0 && !sock.isConnected()) { + try { + sock.bind(new LocalSocketAddress(socketname, + LocalSocketAddress.Namespace.FILESYSTEM)); + } catch (IOException e) { + // wait 300 ms before retrying + try { Thread.sleep(300); + } catch (InterruptedException e1) {} + + } + tries--; + } + + try { + LocalServerSocket lss = new LocalServerSocket(sock.getFileDescriptor()); + return lss; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + + + } + + synchronized void registerNetworkStateReceiver(OpenVPNMangement magnagement) { + // Registers BroadcastReceiver to track network connection changes. + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + mNetworkStateReceiver = new NetworkSateReceiver(magnagement); + registerReceiver(mNetworkStateReceiver, filter); + } + + synchronized void unregisterNetworkStateReceiver() { + if(mNetworkStateReceiver!=null) + this.unregisterReceiver(mNetworkStateReceiver); + mNetworkStateReceiver=null; + } + + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + + if(intent != null && intent.getBooleanExtra(ALWAYS_SHOW_NOTIFICATION, false)) + mNotificationalwaysVisible=true; + + OpenVPN.addStateListener(this); + OpenVPN.addByteCountListener(this); + + if(intent != null && intent.getAction() !=null &&intent.getAction().equals(START_SERVICE)) + return START_NOT_STICKY; + if(intent != null && intent.getAction() !=null &&intent.getAction().equals(START_SERVICE_STICKY)) { + return START_REDELIVER_INTENT; + } + + + // Extract information from the intent. + String prefix = getPackageName(); + String[] argv = intent.getStringArrayExtra(prefix + ".ARGV"); + String nativelibdir = intent.getStringExtra(prefix + ".nativelib"); + String profileUUID = intent.getStringExtra(prefix + ".profileUUID"); + + mProfile = ProfileManager.get(profileUUID); + + showNotification("Starting VPN " + mProfile.mName,"Starting VPN " + mProfile.mName, + false,0,ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET); + + // Set a flag that we are starting a new VPN + mStarting=true; + // Stop the previous session by interrupting the thread. + if(mManagement!=null && mManagement.stopVPN()) + // an old was asked to exit, wait 1s + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + + + if (mProcessThread!=null) { + mProcessThread.interrupt(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + } + // An old running VPN should now be exited + mStarting=false; + + + // Open the Management Interface + if(!mOvpn3) { + LocalServerSocket mgmtsocket = openManagmentInterface(8); + + if(mgmtsocket!=null) { + // start a Thread that handles incoming messages of the managment socket + OpenVpnManagementThread ovpnmgmthread = new OpenVpnManagementThread(mProfile,mgmtsocket,this); + mSocketManagerThread = new Thread(ovpnmgmthread,"OpenVPNMgmtThread"); + mSocketManagerThread.start(); + mManagement= ovpnmgmthread; + OpenVPN.logInfo("started Socket Thread"); + } + } + + // Start a new session by creating a new thread. + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + mOvpn3 = prefs.getBoolean("ovpn3", false); + mOvpn3 = false; + + Runnable processThread; + if(mOvpn3) { + + OpenVPNMangement mOpenVPN3 = instantiateOpenVPN3Core(); + processThread = (Runnable) mOpenVPN3; + mManagement = mOpenVPN3; + + + } else { + processThread = new OpenVPNThread(this, argv,nativelibdir); + } + + mProcessThread = new Thread(processThread, "OpenVPNProcessThread"); + mProcessThread.start(); + + registerNetworkStateReceiver(mManagement); + + + ProfileManager.setConnectedVpnProfile(this, mProfile); + + return START_NOT_STICKY; + } + + private OpenVPNMangement instantiateOpenVPN3Core() { + return null; + } + + @Override + public void onDestroy() { + if (mProcessThread != null) { + mManagement.stopVPN(); + + mProcessThread.interrupt(); + } + if (mNetworkStateReceiver!= null) { + this.unregisterReceiver(mNetworkStateReceiver); + } + // Just in case unregister for state + OpenVPN.removeStateListener(this); + + } + + + + public ParcelFileDescriptor openTun() { + Builder builder = new Builder(); + + if(mLocalIP==null && mLocalIPv6==null) { + OpenVPN.logMessage(0, "", getString(R.string.opentun_no_ipaddr)); + return null; + } + + if(mLocalIP!=null) { + builder.addAddress(mLocalIP.mIp, mLocalIP.len); + } + + if(mLocalIPv6!=null) { + String[] ipv6parts = mLocalIPv6.split("/"); + builder.addAddress(ipv6parts[0],Integer.parseInt(ipv6parts[1])); + } + + + for (String dns : mDnslist ) { + try { + builder.addDnsServer(dns); + } catch (IllegalArgumentException iae) { + OpenVPN.logError(R.string.dns_add_error, dns,iae.getLocalizedMessage()); + } + } + + + builder.setMtu(mMtu); + + + for (CIDRIP route:mRoutes) { + try { + builder.addRoute(route.mIp, route.len); + } catch (IllegalArgumentException ia) { + OpenVPN.logMessage(0, "", getString(R.string.route_rejected) + route + " " + ia.getLocalizedMessage()); + } + } + + for(String v6route:mRoutesv6) { + try { + String[] v6parts = v6route.split("/"); + builder.addRoute(v6parts[0],Integer.parseInt(v6parts[1])); + } catch (IllegalArgumentException ia) { + OpenVPN.logMessage(0, "", getString(R.string.route_rejected) + v6route + " " + ia.getLocalizedMessage()); + } + } + + if(mDomain!=null) + builder.addSearchDomain(mDomain); + + String bconfig[] = new String[6]; + + bconfig[0]= getString(R.string.last_openvpn_tun_config); + bconfig[1] = getString(R.string.local_ip_info,mLocalIP.mIp,mLocalIP.len,mLocalIPv6, mMtu); + bconfig[2] = getString(R.string.dns_server_info, joinString(mDnslist)); + bconfig[3] = getString(R.string.dns_domain_info, mDomain); + bconfig[4] = getString(R.string.routes_info, joinString(mRoutes)); + bconfig[5] = getString(R.string.routes_info6, joinString(mRoutesv6)); + + String session = mProfile.mName; + if(mLocalIP!=null && mLocalIPv6!=null) + session = getString(R.string.session_ipv6string,session, mLocalIP, mLocalIPv6); + else if (mLocalIP !=null) + session= getString(R.string.session_ipv4string, session, mLocalIP); + + builder.setSession(session); + + + OpenVPN.logBuilderConfig(bconfig); + + // No DNS Server, log a warning + if(mDnslist.size()==0) + OpenVPN.logInfo(R.string.warn_no_dns); + + // Reset information + mDnslist.clear(); + mRoutes.clear(); + mRoutesv6.clear(); + mLocalIP=null; + mLocalIPv6=null; + mDomain=null; + + builder.setConfigureIntent(getLogPendingIntent()); + + try { + ParcelFileDescriptor pfd = builder.establish(); + return pfd; + } catch (Exception e) { + OpenVPN.logMessage(0, "", getString(R.string.tun_open_error)); + OpenVPN.logMessage(0, "", getString(R.string.error) + e.getLocalizedMessage()); + OpenVPN.logMessage(0, "", getString(R.string.tun_error_helpful)); + return null; + } + + } + + + // Ugly, but java has no such method + private <T> String joinString(Vector<T> vec) { + String ret = ""; + if(vec.size() > 0){ + ret = vec.get(0).toString(); + for(int i=1;i < vec.size();i++) { + ret = ret + ", " + vec.get(i).toString(); + } + } + return ret; + } + + + + + + + public void addDNS(String dns) { + mDnslist.add(dns); + } + + + public void setDomain(String domain) { + if(mDomain==null) { + mDomain=domain; + } + } + + + public void addRoute(CIDRIP route) + { + mRoutes.add(route ); + } + public void addRoute(String dest, String mask) { + CIDRIP route = new CIDRIP(dest, mask); + if(route.len == 32 && !mask.equals("255.255.255.255")) { + OpenVPN.logMessage(0, "", getString(R.string.route_not_cidr,dest,mask)); + } + + if(route.normalise()) + OpenVPN.logMessage(0, "", getString(R.string.route_not_netip,dest,route.len,route.mIp)); + + mRoutes.add(route); + } + + public void addRoutev6(String extra) { + mRoutesv6.add(extra); + } + + public void setMtu(int mtu) { + mMtu=mtu; + } + + public void setLocalIP(CIDRIP cdrip) + { + mLocalIP=cdrip; + } + + + public void setLocalIP(String local, String netmask,int mtu, String mode) { + mLocalIP = new CIDRIP(local, netmask); + mMtu = mtu; + + if(mLocalIP.len == 32 && !netmask.equals("255.255.255.255")) { + // get the netmask as IP + long netint = CIDRIP.getInt(netmask); + if(Math.abs(netint - mLocalIP.getInt()) ==1) { + if("net30".equals(mode)) + mLocalIP.len=30; + else + mLocalIP.len=31; + } else { + OpenVPN.logMessage(0, "", getString(R.string.ip_not_cidr, local,netmask,mode)); + } + } + } + + public void setLocalIPv6(String ipv6addr) { + mLocalIPv6 = ipv6addr; + } + + @Override + public void updateState(String state,String logmessage, int resid, ConnectionStatus level) { + // If the process is not running, ignore any state, + // Notification should be invisible in this state + doSendBroadcast(state, level); + if(mProcessThread==null && !mNotificationalwaysVisible) + return; + + // Display byte count only after being connected + + { + if(level == ConnectionStatus.LEVEL_CONNECTED) { + mDisplayBytecount = true; + mConnecttime = System.currentTimeMillis(); + } else { + mDisplayBytecount = false; + } + + // Other notifications are shown, + // This also mean we are no longer connected, ignore bytecount messages until next + // CONNECTED + String ticker = getString(resid); + showNotification(getString(resid) +" " + logmessage,ticker,false,0, level); + + } + } + + private void doSendBroadcast(String state, ConnectionStatus level) { + Intent vpnstatus = new Intent(); + vpnstatus.setAction("de.blinkt.openvpn.VPN_STATUS"); + vpnstatus.putExtra("status", level.toString()); + vpnstatus.putExtra("detailstatus", state); + sendBroadcast(vpnstatus, permission.ACCESS_NETWORK_STATE); + } + + @Override + public void updateByteCount(long in, long out, long diffin, long diffout) { + if(mDisplayBytecount) { + String netstat = String.format(getString(R.string.statusline_bytecount), + humanReadableByteCount(in, false), + humanReadableByteCount(diffin/OpenVPNMangement.mBytecountinterval, true), + humanReadableByteCount(out, false), + humanReadableByteCount(diffout/OpenVPNMangement.mBytecountinterval, true)); + + boolean lowpriority = !mNotificationalwaysVisible; + showNotification(netstat,null,lowpriority,mConnecttime, ConnectionStatus.LEVEL_CONNECTED); + } + + } + + // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java + public static String humanReadableByteCount(long bytes, boolean mbit) { + if(mbit) + bytes = bytes *8; + int unit = mbit ? 1000 : 1024; + if (bytes < unit) + return bytes + (mbit ? " bit" : " B"); + + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (mbit ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (mbit ? "" : ""); + if(mbit) + return String.format(Locale.getDefault(),"%.1f %sbit", bytes / Math.pow(unit, exp), pre); + else + return String.format(Locale.getDefault(),"%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + @Override + public boolean handleMessage(Message msg) { + Runnable r = msg.getCallback(); + if(r!=null){ + r.run(); + return true; + } else { + return false; + } + } + + public OpenVPNMangement getManagement() { + return mManagement; + } +} diff --git a/src/de/blinkt/openvpn/core/ProfileManager.java b/src/de/blinkt/openvpn/core/ProfileManager.java new file mode 100644 index 00000000..d1c4afc1 --- /dev/null +++ b/src/de/blinkt/openvpn/core/ProfileManager.java @@ -0,0 +1,223 @@ +package de.blinkt.openvpn.core; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.StreamCorruptedException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import de.blinkt.openvpn.VpnProfile; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; + +public class ProfileManager { + private static final String PREFS_NAME = "VPNList"; + + + + private static final String ONBOOTPROFILE = "onBootProfile"; + + + + private static ProfileManager instance; + + + + private static VpnProfile mLastConnectedVpn=null; + private HashMap<String,VpnProfile> profiles=new HashMap<String, VpnProfile>(); + private static VpnProfile tmpprofile=null; + + + public static VpnProfile get(String key) { + if (tmpprofile!=null && tmpprofile.getUUIDString().equals(key)) + return tmpprofile; + + if(instance==null) + return null; + return instance.profiles.get(key); + + } + + + + private ProfileManager() { } + + private static void checkInstance(Context context) { + if(instance == null) { + instance = new ProfileManager(); + instance.loadVPNList(context); + } + } + + synchronized public static ProfileManager getInstance(Context context) { + checkInstance(context); + return instance; + } + + public static void setConntectedVpnProfileDisconnected(Context c) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); + Editor prefsedit = prefs.edit(); + prefsedit.putString(ONBOOTPROFILE, null); + prefsedit.apply(); + + } + + public static void setConnectedVpnProfile(Context c, VpnProfile connectedrofile) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); + Editor prefsedit = prefs.edit(); + + prefsedit.putString(ONBOOTPROFILE, connectedrofile.getUUIDString()); + prefsedit.apply(); + mLastConnectedVpn=connectedrofile; + + } + + public static VpnProfile getOnBootProfile(Context c) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); + + boolean useStartOnBoot = prefs.getBoolean("restartvpnonboot", false); + + + String mBootProfileUUID = prefs.getString(ONBOOTPROFILE,null); + if(useStartOnBoot && mBootProfileUUID!=null) + return get(c, mBootProfileUUID); + else + return null; + } + + + + + public Collection<VpnProfile> getProfiles() { + return profiles.values(); + } + + public VpnProfile getProfileByName(String name) { + for (VpnProfile vpnp : profiles.values()) { + if(vpnp.getName().equals(name)) { + return vpnp; + } + } + return null; + } + + public void saveProfileList(Context context) { + SharedPreferences sharedprefs = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE); + Editor editor = sharedprefs.edit(); + editor.putStringSet("vpnlist", profiles.keySet()); + + // For reasing I do not understand at all + // Android saves my prefs file only one time + // if I remove the debug code below :( + int counter = sharedprefs.getInt("counter", 0); + editor.putInt("counter", counter+1); + editor.apply(); + + } + + public void addProfile(VpnProfile profile) { + profiles.put(profile.getUUID().toString(),profile); + + } + + public static void setTemporaryProfile(VpnProfile tmp) { + ProfileManager.tmpprofile = tmp; + } + + + public void saveProfile(Context context,VpnProfile profile) { + // First let basic settings save its state + + ObjectOutputStream vpnfile; + try { + vpnfile = new ObjectOutputStream(context.openFileOutput((profile.getUUID().toString() + ".vp"),Activity.MODE_PRIVATE)); + + vpnfile.writeObject(profile); + vpnfile.flush(); + vpnfile.close(); + } catch (FileNotFoundException e) { + + e.printStackTrace(); + throw new RuntimeException(e); + } catch (IOException e) { + + e.printStackTrace(); + throw new RuntimeException(e); + + } + } + + + private void loadVPNList(Context context) { + profiles = new HashMap<String, VpnProfile>(); + SharedPreferences listpref = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE); + Set<String> vlist = listpref.getStringSet("vpnlist", null); + Exception exp =null; + if(vlist==null){ + vlist = new HashSet<String>(); + } + + for (String vpnentry : vlist) { + try { + ObjectInputStream vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp")); + VpnProfile vp = ((VpnProfile) vpnfile.readObject()); + + // Sanity check + if(vp==null || vp.mName==null || vp.getUUID()==null) + continue; + + profiles.put(vp.getUUID().toString(), vp); + + } catch (StreamCorruptedException e) { + exp=e; + } catch (FileNotFoundException e) { + exp=e; + } catch (IOException e) { + exp=e; + } catch (ClassNotFoundException e) { + exp=e; + } + if(exp!=null) { + exp.printStackTrace(); + } + } + } + + public int getNumberOfProfiles() { + return profiles.size(); + } + + + + public void removeProfile(Context context,VpnProfile profile) { + String vpnentry = profile.getUUID().toString(); + profiles.remove(vpnentry); + saveProfileList(context); + context.deleteFile(vpnentry + ".vp"); + if(mLastConnectedVpn==profile) + mLastConnectedVpn=null; + + } + + + + public static VpnProfile get(Context context, String profileUUID) { + checkInstance(context); + return get(profileUUID); + } + + + + public static VpnProfile getLastConnectedVpn() { + return mLastConnectedVpn; + } + +} diff --git a/src/de/blinkt/openvpn/core/ProxyDetection.java b/src/de/blinkt/openvpn/core/ProxyDetection.java new file mode 100644 index 00000000..bc8bf293 --- /dev/null +++ b/src/de/blinkt/openvpn/core/ProxyDetection.java @@ -0,0 +1,55 @@ +package de.blinkt.openvpn.core; + +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; + +public class ProxyDetection { + static SocketAddress detectProxy(VpnProfile vp) { + // Construct a new url with https as protocol + try { + URL url = new URL(String.format("https://%s:%s",vp.mServerName,vp.mServerPort)); + Proxy proxy = getFirstProxy(url); + + if(proxy==null) + return null; + SocketAddress addr = proxy.address(); + if (addr instanceof InetSocketAddress) { + return addr; + } + + } catch (MalformedURLException e) { + OpenVPN.logError(R.string.getproxy_error,e.getLocalizedMessage()); + } catch (URISyntaxException e) { + OpenVPN.logError(R.string.getproxy_error,e.getLocalizedMessage()); + } + return null; + } + + static Proxy getFirstProxy(URL url) throws URISyntaxException { + System.setProperty("java.net.useSystemProxies", "true"); + + List<Proxy> proxylist = ProxySelector.getDefault().select(url.toURI()); + + + if (proxylist != null) { + for (Proxy proxy: proxylist) { + SocketAddress addr = proxy.address(); + + if (addr != null) { + return proxy; + } + } + + } + return null; + } +}
\ No newline at end of file diff --git a/src/de/blinkt/openvpn/core/VPNLaunchHelper.java b/src/de/blinkt/openvpn/core/VPNLaunchHelper.java new file mode 100644 index 00000000..7d14ee6b --- /dev/null +++ b/src/de/blinkt/openvpn/core/VPNLaunchHelper.java @@ -0,0 +1,76 @@ +package de.blinkt.openvpn.core; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; + +public class VPNLaunchHelper { + static private boolean writeMiniVPN(Context context) { + File mvpnout = new File(context.getCacheDir(),VpnProfile.MINIVPN); + if (mvpnout.exists() && mvpnout.canExecute()) + return true; + + IOException e2 = null; + + try { + InputStream mvpn; + + try { + mvpn = context.getAssets().open("minivpn." + Build.CPU_ABI); + } + catch (IOException errabi) { + OpenVPN.logInfo("Failed getting assets for archicture " + Build.CPU_ABI); + e2=errabi; + mvpn = context.getAssets().open("minivpn." + Build.CPU_ABI2); + + } + + + FileOutputStream fout = new FileOutputStream(mvpnout); + + byte buf[]= new byte[4096]; + + int lenread = mvpn.read(buf); + while(lenread> 0) { + fout.write(buf, 0, lenread); + lenread = mvpn.read(buf); + } + fout.close(); + + if(!mvpnout.setExecutable(true)) { + OpenVPN.logMessage(0, "","Failed to set minivpn executable"); + return false; + } + + + return true; + } catch (IOException e) { + if(e2!=null) + OpenVPN.logMessage(0, "",e2.getLocalizedMessage()); + OpenVPN.logMessage(0, "",e.getLocalizedMessage()); + e.printStackTrace(); + return false; + } + } + + + public static void startOpenVpn(VpnProfile startprofile, Context context) { + if(!writeMiniVPN(context)) { + OpenVPN.logMessage(0, "", "Error writing minivpn binary"); + return; + } + OpenVPN.logMessage(0, "", context.getString(R.string.building_configration)); + + Intent startVPN = startprofile.prepareIntent(context); + if(startVPN!=null) + context.startService(startVPN); + + } +} |