summaryrefslogtreecommitdiff
path: root/src/se
diff options
context:
space:
mode:
Diffstat (limited to 'src/se')
-rw-r--r--src/se/leap/openvpn/AboutFragment.java60
-rw-r--r--src/se/leap/openvpn/CIDRIP.java58
-rw-r--r--src/se/leap/openvpn/ConfigConverter.java453
-rw-r--r--src/se/leap/openvpn/ConfigParser.java559
-rw-r--r--src/se/leap/openvpn/FaqFragment.java43
-rw-r--r--src/se/leap/openvpn/FileProvider.java156
-rw-r--r--src/se/leap/openvpn/FileSelect.java208
-rw-r--r--src/se/leap/openvpn/FileSelectLayout.java92
-rw-r--r--src/se/leap/openvpn/FileSelectionFragment.java265
-rw-r--r--src/se/leap/openvpn/GeneralSettings.java31
-rw-r--r--src/se/leap/openvpn/InlineFileTab.java66
-rw-r--r--src/se/leap/openvpn/LaunchVPN.java367
-rw-r--r--src/se/leap/openvpn/LogWindow.java351
-rw-r--r--src/se/leap/openvpn/MainActivity.java44
-rw-r--r--src/se/leap/openvpn/NetworkSateReceiver.java86
-rw-r--r--src/se/leap/openvpn/OnBootReceiver.java33
-rw-r--r--src/se/leap/openvpn/OpenVPN.java244
-rw-r--r--src/se/leap/openvpn/OpenVPNThread.java129
-rw-r--r--src/se/leap/openvpn/OpenVpnManagementThread.java577
-rw-r--r--src/se/leap/openvpn/OpenVpnPreferencesFragment.java52
-rw-r--r--src/se/leap/openvpn/OpenVpnService.java509
-rw-r--r--src/se/leap/openvpn/ProfileManager.java220
-rw-r--r--src/se/leap/openvpn/ProxyDetection.java53
-rw-r--r--src/se/leap/openvpn/SendDumpActivity.java59
-rw-r--r--src/se/leap/openvpn/Settings_Authentication.java136
-rw-r--r--src/se/leap/openvpn/Settings_Basic.java347
-rw-r--r--src/se/leap/openvpn/Settings_IP.java129
-rw-r--r--src/se/leap/openvpn/Settings_Obscure.java116
-rw-r--r--src/se/leap/openvpn/Settings_Routing.java73
-rw-r--r--src/se/leap/openvpn/ShowConfigFragment.java67
-rw-r--r--src/se/leap/openvpn/VPNLaunchHelper.java75
-rw-r--r--src/se/leap/openvpn/VPNPreferences.java138
-rw-r--r--src/se/leap/openvpn/VPNProfileList.java317
-rw-r--r--src/se/leap/openvpn/VpnProfile.java746
34 files changed, 6859 insertions, 0 deletions
diff --git a/src/se/leap/openvpn/AboutFragment.java b/src/se/leap/openvpn/AboutFragment.java
new file mode 100644
index 00000000..3563528b
--- /dev/null
+++ b/src/se/leap/openvpn/AboutFragment.java
@@ -0,0 +1,60 @@
+package se.leap.openvpn;
+
+import android.app.Fragment;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import se.leap.leapclient.R;
+
+public class AboutFragment extends Fragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v= inflater.inflate(R.layout.about, container, false);
+ TextView ver = (TextView) v.findViewById(R.id.version);
+
+ String version;
+ String name="Openvpn";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+
+ ver.setText(getString(R.string.version_info,name,version));
+
+ TextView paypal = (TextView) v.findViewById(R.id.donatestring);
+
+ String donatetext = getActivity().getString(R.string.donatewithpaypal);
+ Spanned htmltext = Html.fromHtml(donatetext);
+ paypal.setText(htmltext);
+ paypal.setMovementMethod(LinkMovementMethod.getInstance());
+
+ TextView translation = (TextView) v.findViewById(R.id.translation);
+
+ // Don't print a text for myself
+ if ( getString(R.string.translationby).contains("Arne Schwabe"))
+ translation.setText("");
+ else
+ translation.setText(R.string.translationby);
+ return v;
+ }
+
+}
diff --git a/src/se/leap/openvpn/CIDRIP.java b/src/se/leap/openvpn/CIDRIP.java
new file mode 100644
index 00000000..8c4b6709
--- /dev/null
+++ b/src/se/leap/openvpn/CIDRIP.java
@@ -0,0 +1,58 @@
+package se.leap.openvpn;
+
+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;
+ }
+
+ }
+ @Override
+ public String toString() {
+ return String.format("%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/se/leap/openvpn/ConfigConverter.java b/src/se/leap/openvpn/ConfigConverter.java
new file mode 100644
index 00000000..8b5f316d
--- /dev/null
+++ b/src/se/leap/openvpn/ConfigConverter.java
@@ -0,0 +1,453 @@
+
+package se.leap.openvpn;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Vector;
+import se.leap.leapclient.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.ListActivity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.Base64;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import se.leap.openvpn.ConfigParser.ConfigParseError;
+
+public class ConfigConverter extends ListActivity {
+
+ public static final String IMPORT_PROFILE = "se.leap.openvpn.IMPORT_PROFILE";
+
+ private VpnProfile mResult;
+ private ArrayAdapter<String> mArrayAdapter;
+
+ private List<String> mPathsegments;
+
+ private String mAliasName=null;
+
+ private int RESULT_INSTALLPKCS12 = 7;
+
+ private String mPossibleName=null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.config_converter);
+ }
+
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==R.id.cancel){
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ } else if(item.getItemId()==R.id.ok) {
+ if(mResult==null) {
+ log("Importing the config had error, cannot save it");
+ return true;
+ }
+
+ Intent in = installPKCS12();
+
+ if(in != null)
+ startActivityForResult(in, RESULT_INSTALLPKCS12);
+ else
+ saveProfile();
+
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if(requestCode==RESULT_INSTALLPKCS12) {
+ if(resultCode==Activity.RESULT_OK) {
+ showCertDialog();
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ private void saveProfile() {
+ Intent result = new Intent();
+ ProfileManager vpl = ProfileManager.getInstance(this);
+
+ if(((CheckBox)findViewById(R.id.correcttls)).isChecked() && isOldCNFormat()) {
+ convertTLSRemote();
+ }
+
+ setUniqueProfileName(vpl);
+ vpl.addProfile(mResult);
+ vpl.saveProfile(this, mResult);
+ vpl.saveProfileList(this);
+ result.putExtra(VpnProfile.EXTRA_PROFILEUUID,mResult.getUUID().toString());
+ setResult(Activity.RESULT_OK, result);
+ finish();
+ }
+
+
+
+ private void convertTLSRemote() {
+ if(mResult.mRemoteCN.startsWith("/"))
+ mResult.mRemoteCN = mResult.mRemoteCN.substring(1);
+ mResult.mRemoteCN = mResult.mRemoteCN.replace("/", ", ");
+ }
+
+ public void showCertDialog () {
+ try {
+ KeyChain.choosePrivateKeyAlias(this,
+ new KeyChainAliasCallback() {
+
+ public void alias(String alias) {
+ // Credential alias selected. Remember the alias selection for future use.
+ mResult.mAlias=alias;
+ saveProfile();
+ }
+
+
+ },
+ new String[] {"RSA"}, // List of acceptable key types. null for any
+ null, // issuer, null for any
+ mResult.mServerName, // host name of server requesting the cert, null if unavailable
+ -1, // port of server requesting the cert, -1 if unavailable
+ mAliasName); // alias to preselect, null if unavailable
+ } catch (ActivityNotFoundException anf) {
+ Builder ab = new AlertDialog.Builder(this);
+ ab.setTitle(R.string.broken_image_cert_title);
+ ab.setMessage(R.string.broken_image_cert);
+ ab.setPositiveButton(android.R.string.ok, null);
+ ab.show();
+ }
+ }
+
+
+ private Intent installPKCS12() {
+
+ if(!((CheckBox)findViewById(R.id.importpkcs12)).isChecked()) {
+ setAuthTypeToEmbeddedPKCS12();
+ return null;
+
+ }
+ String pkcs12datastr = mResult.mPKCS12Filename;
+ if(pkcs12datastr!=null && pkcs12datastr.startsWith(VpnProfile.INLINE_TAG)) {
+ Intent inkeyintent = KeyChain.createInstallIntent();
+
+ pkcs12datastr= pkcs12datastr.substring(VpnProfile.INLINE_TAG.length());
+
+
+ byte[] pkcs12data = Base64.decode(pkcs12datastr, Base64.DEFAULT);
+
+
+ inkeyintent.putExtra(KeyChain.EXTRA_PKCS12,pkcs12data );
+
+ if(mAliasName.equals(""))
+ mAliasName=null;
+
+ if(mAliasName!=null){
+ inkeyintent.putExtra(KeyChain.EXTRA_NAME, mAliasName);
+ }
+ return inkeyintent;
+
+ }
+ return null;
+ }
+
+
+
+ private void setAuthTypeToEmbeddedPKCS12() {
+ if(mResult.mPKCS12Filename!=null && mResult.mPKCS12Filename.startsWith(VpnProfile.INLINE_TAG)) {
+ if(mResult.mAuthenticationType==VpnProfile.TYPE_USERPASS_KEYSTORE)
+ mResult.mAuthenticationType=VpnProfile.TYPE_USERPASS_PKCS12;
+
+ if(mResult.mAuthenticationType==VpnProfile.TYPE_KEYSTORE)
+ mResult.mAuthenticationType=VpnProfile.TYPE_PKCS12;
+
+ }
+ }
+
+
+
+
+
+ private void setUniqueProfileName(ProfileManager vpl) {
+ int i=0;
+
+ String newname = mPossibleName;
+
+ while(vpl.getProfileByName(newname)!=null) {
+ i++;
+ if(i==1)
+ newname = getString(R.string.converted_profile);
+ else
+ newname = getString(R.string.converted_profile_i,i);
+ }
+
+ mResult.mName=newname;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.import_menu, menu);
+ return true;
+ }
+
+ private String embedFile(String filename) {
+ return embedFile(filename, false);
+ }
+
+ private String embedFile(String filename, boolean base64encode)
+ {
+ if(filename==null)
+ return null;
+
+ // Already embedded, nothing to do
+ if(filename.startsWith(VpnProfile.INLINE_TAG))
+ return filename;
+
+ File possibleFile = findFile(filename);
+ if(possibleFile==null)
+ return filename;
+ else
+ return readFileContent(possibleFile,base64encode);
+
+ }
+
+ private File findFile(String filename) {
+ File foundfile =findFileRaw(filename);
+
+ if (foundfile==null && filename!=null && !filename.equals(""))
+ log(R.string.import_could_not_open,filename);
+
+ return foundfile;
+ }
+
+
+
+ private File findFileRaw(String filename)
+ {
+ if(filename == null || filename.equals(""))
+ return null;
+
+ // Try diffent path relative to /mnt/sdcard
+ File sdcard = Environment.getExternalStorageDirectory();
+ File root = new File("/");
+
+ Vector<File> dirlist = new Vector<File>();
+
+ for(int i=mPathsegments.size()-1;i >=0 ;i--){
+ String path = "";
+ for (int j = 0;j<=i;j++) {
+ path += "/" + mPathsegments.get(j);
+ }
+ dirlist.add(new File(path));
+ }
+ dirlist.add(sdcard);
+ dirlist.add(root);
+
+
+ String[] fileparts = filename.split("/");
+ for(File rootdir:dirlist){
+ String suffix="";
+ for(int i=fileparts.length-1; i >=0;i--) {
+ if(i==fileparts.length-1)
+ suffix = fileparts[i];
+ else
+ suffix = fileparts[i] + "/" + suffix;
+
+ File possibleFile = new File(rootdir,suffix);
+ if(!possibleFile.canRead())
+ continue;
+
+ // read the file inline
+ return possibleFile;
+
+ }
+ }
+ return null;
+ }
+
+ String readFileContent(File possibleFile, boolean base64encode) {
+ byte [] filedata;
+ try {
+ filedata = readBytesFromFile(possibleFile);
+ } catch (IOException e) {
+ log(e.getLocalizedMessage());
+ return null;
+ }
+
+ String data;
+ if(base64encode) {
+ data = Base64.encodeToString(filedata, Base64.DEFAULT);
+ } else {
+ data = new String(filedata);
+
+ }
+ return VpnProfile.INLINE_TAG + data;
+
+ }
+
+
+ private byte[] readBytesFromFile(File file) throws IOException {
+ InputStream input = new FileInputStream(file);
+
+ long len= file.length();
+
+
+ // Create the byte array to hold the data
+ byte[] bytes = new byte[(int) len];
+
+ // Read in the bytes
+ int offset = 0;
+ int bytesRead = 0;
+ while (offset < bytes.length
+ && (bytesRead=input.read(bytes, offset, bytes.length-offset)) >= 0) {
+ offset += bytesRead;
+ }
+
+ input.close();
+ return bytes;
+ }
+
+ void embedFiles() {
+ // This where I would like to have a c++ style
+ // void embedFile(std::string & option)
+
+ if (mResult.mPKCS12Filename!=null) {
+ File pkcs12file = findFileRaw(mResult.mPKCS12Filename);
+ if(pkcs12file!=null) {
+ mAliasName = pkcs12file.getName().replace(".p12", "");
+ } else {
+ mAliasName = "Imported PKCS12";
+ }
+ }
+
+
+ mResult.mCaFilename = embedFile(mResult.mCaFilename);
+ mResult.mClientCertFilename = embedFile(mResult.mClientCertFilename);
+ mResult.mClientKeyFilename = embedFile(mResult.mClientKeyFilename);
+ mResult.mTLSAuthFilename = embedFile(mResult.mTLSAuthFilename);
+ mResult.mPKCS12Filename = embedFile(mResult.mPKCS12Filename,true);
+
+
+ if(mResult.mUsername == null && mResult.mPassword != null ){
+ String data =embedFile(mResult.mPassword);
+ ConfigParser.useEmbbedUserAuth(mResult, data);
+ }
+ }
+
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ mArrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
+ getListView().setAdapter(mArrayAdapter);
+ final android.content.Intent intent = getIntent ();
+
+ if (intent != null)
+ {
+ final android.net.Uri data = intent.getData ();
+ if (data != null)
+ {
+ //log(R.string.import_experimental);
+ log(R.string.importing_config,data.toString());
+ try {
+ if(data.getScheme().equals("file")) {
+ mPossibleName = data.getLastPathSegment();
+ if(mPossibleName!=null){
+ mPossibleName =mPossibleName.replace(".ovpn", "");
+ mPossibleName =mPossibleName.replace(".conf", "");
+ }
+ }
+ InputStream is = getContentResolver().openInputStream(data);
+ mPathsegments = data.getPathSegments();
+
+ doImport(is);
+ } catch (FileNotFoundException e) {
+ log(R.string.import_content_resolve_error);
+ }
+ }
+ }
+
+ return;
+ }
+
+ private void log(String logmessage) {
+ mArrayAdapter.add(logmessage);
+ }
+
+ private void doImport(InputStream is) {
+ ConfigParser cp = new ConfigParser();
+ try {
+ InputStreamReader isr = new InputStreamReader(is);
+
+ cp.parseConfig(isr);
+ VpnProfile vp = cp.convertProfile();
+ mResult = vp;
+ embedFiles();
+ displayWarnings();
+ log(R.string.import_done);
+ return;
+
+ } catch (IOException e) {
+ log(R.string.error_reading_config_file);
+ log(e.getLocalizedMessage());
+ } catch (ConfigParseError e) {
+ log(R.string.error_reading_config_file);
+ log(e.getLocalizedMessage());
+ }
+ mResult=null;
+
+ }
+
+ private void displayWarnings() {
+ if(mResult.mUseCustomConfig) {
+ log(R.string.import_warning_custom_options);
+ String copt = mResult.mCustomConfigOptions;
+ if(copt.startsWith("#")) {
+ int until = copt.indexOf('\n');
+ copt = copt.substring(until+1);
+ }
+
+ log(copt);
+ }
+
+ if(mResult.mAuthenticationType==VpnProfile.TYPE_KEYSTORE ||
+ mResult.mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
+ findViewById(R.id.importpkcs12).setVisibility(View.VISIBLE);
+ }
+
+ if (isOldCNFormat())
+ findViewById(R.id.correcttls).setVisibility(View.VISIBLE);
+ }
+
+ private boolean isOldCNFormat() {
+ return mResult.mCheckRemoteCN && mResult.mRemoteCN.contains("/") && ! mResult.mRemoteCN.contains("_");
+ }
+
+ private void log(int ressourceId, Object... formatArgs) {
+ log(getString(ressourceId,formatArgs));
+ }
+}
diff --git a/src/se/leap/openvpn/ConfigParser.java b/src/se/leap/openvpn/ConfigParser.java
new file mode 100644
index 00000000..f57bbae9
--- /dev/null
+++ b/src/se/leap/openvpn/ConfigParser.java
@@ -0,0 +1,559 @@
+package se.leap.openvpn;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Vector;
+
+//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
+
+// And rember, this is valid :)
+// --<foo>
+// bar
+// </foo>
+public class ConfigParser {
+
+
+ private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>();
+ public void parseConfig(Reader reader) throws IOException, ConfigParseError {
+
+
+ BufferedReader br =new BufferedReader(reader);
+
+ @SuppressWarnings("unused")
+ int lineno=0;
+
+ while (true){
+ String line = br.readLine();
+ if(line==null)
+ break;
+ lineno++;
+ 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 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",
+ "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<String> remote = getOption("remote",1,3);
+ if(remote != null){
+ 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> 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> tlsremote = getOption("tls-remote",1,1);
+ if(tlsremote!=null){
+ np.mRemoteCN = tlsremote.get(1);
+ np.mCheckRemoteCN=true;
+ }
+
+ 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 canche to embed later.
+ np.mUsername=null;
+ np.mPassword=authuser.get(1);
+ useEmbbedUserAuth(np,authuser.get(1));
+ }
+
+ }
+
+
+ // Check the other options
+
+ checkIgnoreAndInvalidOptions(np);
+ fixup(np);
+
+ return np;
+ }
+
+ 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) {
+ String custom = "# These Options were found in the config file do not map to config settings:\n";
+
+ for(Vector<Vector<String>> option:options.values()) {
+ for(Vector<String> optionsline: option) {
+ for (String arg : optionsline)
+ custom+= VpnProfile.openVpnEscape(arg) + " ";
+ }
+ custom+="\n";
+
+ }
+ np.mCustomConfigOptions = custom;
+ np.mUseCustomConfig=true;
+
+ }
+ }
+
+
+ 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/se/leap/openvpn/FaqFragment.java b/src/se/leap/openvpn/FaqFragment.java
new file mode 100644
index 00000000..2b9f02eb
--- /dev/null
+++ b/src/se/leap/openvpn/FaqFragment.java
@@ -0,0 +1,43 @@
+package se.leap.openvpn;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import se.leap.leapclient.R;
+
+public class FaqFragment extends Fragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v= inflater.inflate(R.layout.faq, container, false);
+
+ insertHtmlEntry(v,R.id.broken_images_faq,R.string.broken_images_faq);
+ insertHtmlEntry(v,R.id.faq_howto,R.string.faq_howto);
+ insertHtmlEntry(v, R.id.baterry_consumption, R.string.baterry_consumption);
+ insertHtmlEntry(v, R.id.faq_tethering, R.string.faq_tethering);
+
+ return v;
+
+
+
+ }
+
+ private void insertHtmlEntry (View v, int viewId, int stringId) {
+ TextView faqitem = (TextView) v.findViewById(viewId);
+ faqitem.setText(Html.fromHtml(getActivity().getString(stringId)));
+ faqitem.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+}
diff --git a/src/se/leap/openvpn/FileProvider.java b/src/se/leap/openvpn/FileProvider.java
new file mode 100644
index 00000000..d2714b2c
--- /dev/null
+++ b/src/se/leap/openvpn/FileProvider.java
@@ -0,0 +1,156 @@
+/*
+ * 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 se.leap.openvpn;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import android.content.ContentProvider;
+import android.content.ContentProvider.PipeDataWriter;
+import android.content.ContentValues;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.provider.OpenableColumns;
+import android.util.Log;
+
+/**
+ * A very simple content provider that can serve arbitrary asset files from
+ * our .apk.
+ */
+public class FileProvider extends ContentProvider
+implements PipeDataWriter<InputStream> {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ try {
+ File dumpfile = getFileFromURI(uri);
+
+
+ MatrixCursor c = new MatrixCursor(projection);
+
+ Object[] row = new Object[projection.length];
+ int i=0;
+ for (String r:projection) {
+ if(r.equals(OpenableColumns.SIZE))
+ row[i] = dumpfile.length();
+ if(r.equals(OpenableColumns.DISPLAY_NAME))
+ row[i] = dumpfile.getName();
+ i++;
+ }
+ c.addRow(row);
+ return c;
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ // Don't support inserts.
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ // Don't support deletes.
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ // Don't support updates.
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ // For this sample, assume all files are .apks.
+ return "application/octet-stream";
+ }
+
+ @Override
+ public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
+ File dumpfile = getFileFromURI(uri);
+
+ try {
+
+ InputStream is = new FileInputStream(dumpfile);
+ // Start a new thread that pipes the stream data back to the caller.
+ return new AssetFileDescriptor(
+ openPipeHelper(uri, null, null, is, this), 0,
+ dumpfile.length());
+ } catch (IOException e) {
+ FileNotFoundException fnf = new FileNotFoundException("Unable to open minidump " + uri);
+ throw fnf;
+ }
+ }
+
+ private File getFileFromURI(Uri uri) throws FileNotFoundException {
+ // Try to open an asset with the given name.
+ String path = uri.getPath();
+ if(path.startsWith("/"))
+ path = path.replaceFirst("/", "");
+
+ // I think this already random enough, no need for magic secure cookies
+ // 1f9563a4-a1f5-2165-255f2219-111823ef.dmp
+ if (!path.matches("^[0-9a-z-.]*(dmp|dmp.log)$"))
+ throw new FileNotFoundException("url not in expect format " + uri);
+ File cachedir = getContext().getCacheDir();
+ File dumpfile = new File(cachedir,path);
+ return dumpfile;
+ }
+
+ @Override
+ public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
+ Bundle opts, InputStream args) {
+ // Transfer data from the asset to the pipe the client is reading.
+ byte[] buffer = new byte[8192];
+ int n;
+ FileOutputStream fout = new FileOutputStream(output.getFileDescriptor());
+ try {
+ while ((n=args.read(buffer)) >= 0) {
+ fout.write(buffer, 0, n);
+ }
+ } catch (IOException e) {
+ Log.i("OpenVPNFileProvider", "Failed transferring", e);
+ } finally {
+ try {
+ args.close();
+ } catch (IOException e) {
+ }
+ try {
+ fout.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+}
diff --git a/src/se/leap/openvpn/FileSelect.java b/src/se/leap/openvpn/FileSelect.java
new file mode 100644
index 00000000..4abf6c31
--- /dev/null
+++ b/src/se/leap/openvpn/FileSelect.java
@@ -0,0 +1,208 @@
+package se.leap.openvpn;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import se.leap.leapclient.R;
+
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Base64;
+
+public class FileSelect extends Activity {
+ public static final String RESULT_DATA = "RESULT_PATH";
+ public static final String START_DATA = "START_DATA";
+ public static final String WINDOW_TITLE = "WINDOW_TILE";
+ public static final String NO_INLINE_SELECTION = "se.leap.openvpn.NO_INLINE_SELECTION";
+ public static final String SHOW_CLEAR_BUTTON = "se.leap.openvpn.SHOW_CLEAR_BUTTON";
+ public static final String DO_BASE64_ENCODE = "se.leap.openvpn.BASE64ENCODE";
+
+ private FileSelectionFragment mFSFragment;
+ private InlineFileTab mInlineFragment;
+ private String mData;
+ private Tab inlineFileTab;
+ private Tab fileExplorerTab;
+ private boolean mNoInline;
+ private boolean mShowClear;
+ private boolean mBase64Encode;
+
+
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.file_dialog);
+
+ mData = getIntent().getStringExtra(START_DATA);
+ if(mData==null)
+ mData=Environment.getExternalStorageDirectory().getPath();
+
+ String title = getIntent().getStringExtra(WINDOW_TITLE);
+ int titleId = getIntent().getIntExtra(WINDOW_TITLE, 0);
+ if(titleId!=0)
+ title =getString(titleId);
+ if(title!=null)
+ setTitle(title);
+
+ mNoInline = getIntent().getBooleanExtra(NO_INLINE_SELECTION, false);
+ mShowClear = getIntent().getBooleanExtra(SHOW_CLEAR_BUTTON, false);
+ mBase64Encode = getIntent().getBooleanExtra(DO_BASE64_ENCODE, false);
+
+ ActionBar bar = getActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ fileExplorerTab = bar.newTab().setText(R.string.file_explorer_tab);
+ inlineFileTab = bar.newTab().setText(R.string.inline_file_tab);
+
+ mFSFragment = new FileSelectionFragment();
+ fileExplorerTab.setTabListener(new MyTabsListener<FileSelectionFragment>(this, mFSFragment));
+ bar.addTab(fileExplorerTab);
+
+ if(!mNoInline) {
+ mInlineFragment = new InlineFileTab();
+ inlineFileTab.setTabListener(new MyTabsListener<InlineFileTab>(this, mInlineFragment));
+ bar.addTab(inlineFileTab);
+ } else {
+ mFSFragment.setNoInLine();
+ }
+
+
+ }
+
+ protected boolean showClear() {
+ if(mData == null || mData.equals(""))
+ return false;
+ else
+ return mShowClear;
+ }
+
+ protected class MyTabsListener<T extends Fragment> implements ActionBar.TabListener
+ {
+ private Fragment mFragment;
+ private boolean mAdded=false;
+
+ public MyTabsListener( Activity activity, Fragment fragment){
+ this.mFragment = fragment;
+ }
+
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ // Check if the fragment is already initialized
+ if (!mAdded) {
+ // If not, instantiate and add it to the activity
+ ft.add(android.R.id.content, mFragment);
+ mAdded =true;
+ } else {
+ // If it exists, simply attach it in order to show it
+ ft.attach(mFragment);
+ }
+ }
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ ft.detach(mFragment);
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+
+ }
+ }
+
+ public void importFile(String path) {
+ File ifile = new File(path);
+ Exception fe = null;
+ try {
+
+ String data =VpnProfile.INLINE_TAG;
+
+ byte[] filedata = readBytesFromFile(ifile) ;
+ if(mBase64Encode)
+ data += Base64.encodeToString(filedata, Base64.DEFAULT);
+ else
+ data += new String(filedata);
+
+ mData =data;
+ mInlineFragment.setData(data);
+ getActionBar().selectTab(inlineFileTab);
+ } catch (FileNotFoundException e) {
+ fe = e;
+ } catch (IOException e) {
+ fe =e;
+ }
+ if(fe!=null) {
+ Builder ab = new AlertDialog.Builder(this);
+ ab.setTitle(R.string.error_importing_file);
+ ab.setMessage(getString(R.string.import_error_message) + "\n" + fe.getLocalizedMessage());
+ ab.setPositiveButton(android.R.string.ok, null);
+ ab.show();
+ }
+ }
+
+ private byte[] readBytesFromFile(File file) throws IOException {
+ InputStream input = new FileInputStream(file);
+
+ long len= file.length();
+
+
+ // Create the byte array to hold the data
+ byte[] bytes = new byte[(int) len];
+
+ // Read in the bytes
+ int offset = 0;
+ int bytesRead = 0;
+ while (offset < bytes.length
+ && (bytesRead=input.read(bytes, offset, bytes.length-offset)) >= 0) {
+ offset += bytesRead;
+ }
+
+ input.close();
+ return bytes;
+ }
+
+
+ public void setFile(String path) {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_DATA, path);
+ setResult(Activity.RESULT_OK,intent);
+ finish();
+ }
+
+ public String getSelectPath() {
+ if(!mData.startsWith(VpnProfile.INLINE_TAG))
+ return mData;
+ else
+ return Environment.getExternalStorageDirectory().getPath();
+ }
+
+ public CharSequence getInlineData() {
+ if(mData.startsWith(VpnProfile.INLINE_TAG))
+ return mData.substring(VpnProfile.INLINE_TAG.length());
+ else
+ return "";
+ }
+
+ public void clearData() {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_DATA, (String)null);
+ setResult(Activity.RESULT_OK,intent);
+ finish();
+
+ }
+
+ public void saveInlineData(String string) {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_DATA, string);
+ setResult(Activity.RESULT_OK,intent);
+ finish();
+
+ }
+}
diff --git a/src/se/leap/openvpn/FileSelectLayout.java b/src/se/leap/openvpn/FileSelectLayout.java
new file mode 100644
index 00000000..2ca4490d
--- /dev/null
+++ b/src/se/leap/openvpn/FileSelectLayout.java
@@ -0,0 +1,92 @@
+package se.leap.openvpn;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import se.leap.leapclient.R;
+
+
+public class FileSelectLayout extends LinearLayout implements OnClickListener {
+
+ private TextView mDataView;
+ private String mData;
+ private Fragment mFragment;
+ private int mTaskId;
+ private Button mSelectButton;
+ private boolean mBase64Encode;
+ private String mTitle;
+ private boolean mShowClear;
+
+ public FileSelectLayout( Context context,AttributeSet attrset) {
+ super(context,attrset);
+ inflate(getContext(), R.layout.file_select, this);
+
+ TypedArray ta = context.obtainStyledAttributes(attrset,R.styleable.FileSelectLayout);
+
+ mTitle = ta.getString(R.styleable.FileSelectLayout_title);
+
+ TextView tview = (TextView) findViewById(R.id.file_title);
+ tview.setText(mTitle);
+
+ mDataView = (TextView) findViewById(R.id.file_selected_item);
+ mSelectButton = (Button) findViewById(R.id.file_select_button);
+ mSelectButton.setOnClickListener(this);
+
+ }
+
+ public void setFragment(Fragment fragment, int i)
+ {
+ mTaskId = i;
+ mFragment = fragment;
+ }
+
+ public void getCertificateFileDialog() {
+ Intent startFC = new Intent(getContext(),FileSelect.class);
+ startFC.putExtra(FileSelect.START_DATA, mData);
+ startFC.putExtra(FileSelect.WINDOW_TITLE,mTitle);
+ if(mBase64Encode)
+ startFC.putExtra(FileSelect.DO_BASE64_ENCODE, true);
+ if(mShowClear)
+ startFC.putExtra(FileSelect.SHOW_CLEAR_BUTTON, true);
+ mFragment.startActivityForResult(startFC,mTaskId);
+ }
+
+
+ public String getData() {
+ return mData;
+ }
+
+ public void setData(String data) {
+ mData = data;
+ if(data==null)
+ mDataView.setText(mFragment.getString(R.string.no_data));
+ else if(mData.startsWith(VpnProfile.INLINE_TAG))
+ mDataView.setText(R.string.inline_file_data);
+ else
+ mDataView.setText(data);
+
+ }
+
+ @Override
+ public void onClick(View v) {
+ if(v == mSelectButton) {
+ getCertificateFileDialog();
+ }
+ }
+
+ public void setBase64Encode() {
+ mBase64Encode =true;
+ }
+
+ public void setShowClear() {
+ mShowClear=true;
+ }
+
+}
diff --git a/src/se/leap/openvpn/FileSelectionFragment.java b/src/se/leap/openvpn/FileSelectionFragment.java
new file mode 100644
index 00000000..5bc981fe
--- /dev/null
+++ b/src/se/leap/openvpn/FileSelectionFragment.java
@@ -0,0 +1,265 @@
+package se.leap.openvpn;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.TreeMap;
+import se.leap.leapclient.R;
+
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+import android.widget.TextView;
+
+/**
+ * Activity para escolha de arquivos/diretorios.
+ *
+ * @author android
+ *
+ */
+public class FileSelectionFragment extends ListFragment {
+
+ private static final String ITEM_KEY = "key";
+ private static final String ITEM_IMAGE = "image";
+ private static final String ROOT = "/";
+
+
+ private List<String> path = null;
+ private TextView myPath;
+ private ArrayList<HashMap<String, Object>> mList;
+
+ private Button selectButton;
+
+
+ private String parentPath;
+ private String currentPath = ROOT;
+
+
+ private String[] formatFilter = null;
+
+ private File selectedFile;
+ private HashMap<String, Integer> lastPositions = new HashMap<String, Integer>();
+ private String mStartPath;
+ private Button mImportFile;
+ private Button mClearButton;
+ private boolean mHideImport=false;
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.file_dialog_main, container,false);
+
+ myPath = (TextView) v.findViewById(R.id.path);
+
+ selectButton = (Button) v.findViewById(R.id.fdButtonSelect);
+ selectButton.setEnabled(false);
+ selectButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (selectedFile != null) {
+ ((FileSelect) getActivity()).setFile(selectedFile.getPath());
+
+ }
+ }
+ });
+
+ mClearButton = (Button) v.findViewById(R.id.fdClear);
+ mClearButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((FileSelect) getActivity()).clearData();
+ }
+ });
+ if(!((FileSelect) getActivity()).showClear()) {
+ mClearButton.setVisibility(View.GONE);
+ mClearButton.setEnabled(false);
+ }
+
+
+ mImportFile = (Button) v.findViewById(R.id.importfile);
+ mImportFile.setEnabled(false);
+ mImportFile.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((FileSelect) getActivity()).importFile(selectedFile.getPath());
+ }
+ });
+
+ if(mHideImport== true) {
+ mImportFile.setVisibility(View.GONE);
+ }
+
+
+
+ return v;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mStartPath = ((FileSelect) getActivity()).getSelectPath();
+ getDir(mStartPath);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ private void getDir(String dirPath) {
+
+ boolean useAutoSelection = dirPath.length() < currentPath.length();
+
+ Integer position = lastPositions.get(parentPath);
+
+ getDirImpl(dirPath);
+
+ if (position != null && useAutoSelection) {
+ getListView().setSelection(position);
+ }
+
+ }
+
+ /**
+ * Monta a estrutura de arquivos e diretorios filhos do diretorio fornecido.
+ *
+ * @param dirPath
+ * Diretorio pai.
+ */
+ private void getDirImpl(final String dirPath) {
+
+ currentPath = dirPath;
+
+ final List<String> item = new ArrayList<String>();
+ path = new ArrayList<String>();
+ mList = new ArrayList<HashMap<String, Object>>();
+
+ File f = new File(currentPath);
+ File[] files = f.listFiles();
+ if (files == null) {
+ currentPath = ROOT;
+ f = new File(currentPath);
+ files = f.listFiles();
+ }
+
+ myPath.setText(getText(R.string.location) + ": " + currentPath);
+
+ if (!currentPath.equals(ROOT)) {
+
+ item.add(ROOT);
+ addItem(ROOT, R.drawable.folder);
+ path.add(ROOT);
+
+ item.add("../");
+ addItem("../", R.drawable.folder);
+ path.add(f.getParent());
+ parentPath = f.getParent();
+
+ }
+
+ TreeMap<String, String> dirsMap = new TreeMap<String, String>();
+ TreeMap<String, String> dirsPathMap = new TreeMap<String, String>();
+ TreeMap<String, String> filesMap = new TreeMap<String, String>();
+ TreeMap<String, String> filesPathMap = new TreeMap<String, String>();
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String dirName = file.getName();
+ dirsMap.put(dirName, dirName);
+ dirsPathMap.put(dirName, file.getPath());
+ } else {
+ final String fileName = file.getName();
+ final String fileNameLwr = fileName.toLowerCase();
+ // se ha um filtro de formatos, utiliza-o
+ if (formatFilter != null) {
+ boolean contains = false;
+ for (int i = 0; i < formatFilter.length; i++) {
+ final String formatLwr = formatFilter[i].toLowerCase();
+ if (fileNameLwr.endsWith(formatLwr)) {
+ contains = true;
+ break;
+ }
+ }
+ if (contains) {
+ filesMap.put(fileName, fileName);
+ filesPathMap.put(fileName, file.getPath());
+ }
+ // senao, adiciona todos os arquivos
+ } else {
+ filesMap.put(fileName, fileName);
+ filesPathMap.put(fileName, file.getPath());
+ }
+ }
+ }
+ item.addAll(dirsMap.tailMap("").values());
+ item.addAll(filesMap.tailMap("").values());
+ path.addAll(dirsPathMap.tailMap("").values());
+ path.addAll(filesPathMap.tailMap("").values());
+
+ SimpleAdapter fileList = new SimpleAdapter(getActivity(), mList, R.layout.file_dialog_row, new String[] {
+ ITEM_KEY, ITEM_IMAGE }, new int[] { R.id.fdrowtext, R.id.fdrowimage });
+
+ for (String dir : dirsMap.tailMap("").values()) {
+ addItem(dir, R.drawable.folder);
+ }
+
+ for (String file : filesMap.tailMap("").values()) {
+ addItem(file, R.drawable.file);
+ }
+
+ fileList.notifyDataSetChanged();
+
+ setListAdapter(fileList);
+
+ }
+
+ private void addItem(String fileName, int imageId) {
+ HashMap<String, Object> item = new HashMap<String, Object>();
+ item.put(ITEM_KEY, fileName);
+ item.put(ITEM_IMAGE, imageId);
+ mList.add(item);
+ }
+
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+
+ File file = new File(path.get(position));
+
+ if (file.isDirectory()) {
+ selectButton.setEnabled(false);
+ mImportFile.setEnabled(false);
+
+ if (file.canRead()) {
+ lastPositions.put(currentPath, position);
+ getDir(path.get(position));
+ } else {
+ new AlertDialog.Builder(getActivity()).setIcon(R.drawable.icon)
+ .setTitle("[" + file.getName() + "] " + getText(R.string.cant_read_folder))
+ .setPositiveButton("OK", null).show();
+ }
+ } else {
+ selectedFile = file;
+ v.setSelected(true);
+ selectButton.setEnabled(true);
+ mImportFile.setEnabled(true);
+ }
+ }
+
+ public void setNoInLine() {
+ mHideImport=true;
+ }
+
+}
diff --git a/src/se/leap/openvpn/GeneralSettings.java b/src/se/leap/openvpn/GeneralSettings.java
new file mode 100644
index 00000000..34657914
--- /dev/null
+++ b/src/se/leap/openvpn/GeneralSettings.java
@@ -0,0 +1,31 @@
+package se.leap.openvpn;
+import java.io.File;
+import se.leap.leapclient.R;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+public class GeneralSettings extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.general_settings);
+ Preference loadtun = findPreference("loadTunModule");
+ if(!isTunModuleAvailable())
+ loadtun.setEnabled(false);
+ }
+
+ private boolean isTunModuleAvailable() {
+ // Check if the tun module exists on the file system
+ if(new File("/system/lib/modules/tun.ko").length() > 10)
+ return true;
+ return false;
+ }
+
+
+ } \ No newline at end of file
diff --git a/src/se/leap/openvpn/InlineFileTab.java b/src/se/leap/openvpn/InlineFileTab.java
new file mode 100644
index 00000000..40995ceb
--- /dev/null
+++ b/src/se/leap/openvpn/InlineFileTab.java
@@ -0,0 +1,66 @@
+package se.leap.openvpn;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import se.leap.leapclient.R;
+
+public class InlineFileTab extends Fragment
+{
+
+ private static final int MENU_SAVE = 0;
+ private EditText mInlineData;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mInlineData.setText(((FileSelect)getActivity()).getInlineData());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState)
+ {
+
+ View v = inflater.inflate(R.layout.file_dialog_inline, container, false);
+ mInlineData =(EditText) v.findViewById(R.id.inlineFileData);
+ return v;
+ }
+
+ public void setData(String data) {
+ if(mInlineData!=null)
+ mInlineData.setText(data);
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.add(0, MENU_SAVE, 0, "Use inline data")
+ .setIcon(android.R.drawable.ic_menu_save)
+ .setAlphabeticShortcut('u')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
+ | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==MENU_SAVE){
+ ((FileSelect)getActivity()).saveInlineData(mInlineData.getText().toString());
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+} \ No newline at end of file
diff --git a/src/se/leap/openvpn/LaunchVPN.java b/src/se/leap/openvpn/LaunchVPN.java
new file mode 100644
index 00000000..3df2aacb
--- /dev/null
+++ b/src/se/leap/openvpn/LaunchVPN.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2008 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 se.leap.openvpn;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Vector;
+import se.leap.leapclient.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * This Activity actually handles two stages of a launcher shortcut's life cycle.
+ *
+ * 1. Your application offers to provide shortcuts to the launcher. When
+ * the user installs a shortcut, an activity within your application
+ * generates the actual shortcut and returns it to the launcher, where it
+ * is shown to the user as an icon.
+ *
+ * 2. Any time the user clicks on an installed shortcut, an intent is sent.
+ * Typically this would then be handled as necessary by an activity within
+ * your application.
+ *
+ * We handle stage 1 (creating a shortcut) by simply sending back the information (in the form
+ * of an {@link android.content.Intent} that the launcher will use to create the shortcut.
+ *
+ * You can also implement this in an interactive way, by having your activity actually present
+ * UI for the user to select the specific nature of the shortcut, such as a contact, picture, URL,
+ * media item, or action.
+ *
+ * We handle stage 2 (responding to a shortcut) in this sample by simply displaying the contents
+ * of the incoming {@link android.content.Intent}.
+ *
+ * In a real application, you would probably use the shortcut intent to display specific content
+ * or start a particular operation.
+ */
+public class LaunchVPN extends ListActivity implements OnItemClickListener {
+
+ public static final String EXTRA_KEY = "se.leap.openvpn.shortcutProfileUUID";
+ public static final String EXTRA_NAME = "se.leap.openvpn.shortcutProfileName";
+ public static final String EXTRA_HIDELOG = "se.leap.openvpn.showNoLogWindow";;
+
+ private static final int START_VPN_PROFILE= 70;
+
+
+ private ProfileManager mPM;
+ private VpnProfile mSelectedProfile;
+ private boolean mhideLog=false;
+
+ private boolean mCmfixed=false;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPM =ProfileManager.getInstance(this);
+
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // Resolve the intent
+
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
+
+ // If the intent is a request to create a shortcut, we'll do that and exit
+
+
+ if(Intent.ACTION_MAIN.equals(action)) {
+ // we got called to be the starting point, most likely a shortcut
+ String shortcutUUID = intent.getStringExtra( EXTRA_KEY);
+ String shortcutName = intent.getStringExtra( EXTRA_NAME);
+ mhideLog = intent.getBooleanExtra(EXTRA_HIDELOG, false);
+
+ VpnProfile profileToConnect = ProfileManager.get(shortcutUUID);
+ if(shortcutName != null && profileToConnect ==null)
+ profileToConnect = ProfileManager.getInstance(this).getProfileByName(shortcutName);
+
+ if(profileToConnect ==null) {
+ OpenVPN.logError(R.string.shortcut_profile_notfound);
+ // show Log window to display error
+ showLogWindow();
+ finish();
+ return;
+ }
+
+ mSelectedProfile = profileToConnect;
+ launchVPN();
+
+ } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ createListView();
+ }
+ }
+
+ private void createListView() {
+ ListView lv = getListView();
+ //lv.setTextFilterEnabled(true);
+
+ Collection<VpnProfile> vpnlist = mPM.getProfiles();
+
+ Vector<String> vpnnames=new Vector<String>();
+ for (VpnProfile vpnProfile : vpnlist) {
+ vpnnames.add(vpnProfile.mName);
+ }
+
+
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,vpnnames);
+ lv.setAdapter(adapter);
+
+ lv.setOnItemClickListener(this);
+ }
+
+ /**
+ * This function creates a shortcut and returns it to the caller. There are actually two
+ * intents that you will send back.
+ *
+ * The first intent serves as a container for the shortcut and is returned to the launcher by
+ * setResult(). This intent must contain three fields:
+ *
+ * <ul>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with
+ * the shortcut.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a
+ * bitmap, <i>or</i> {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as
+ * a drawable resource.</li>
+ * </ul>
+ *
+ * If you use a simple drawable resource, note that you must wrapper it using
+ * {@link android.content.Intent.ShortcutIconResource}, as shown below. This is required so
+ * that the launcher can access resources that are stored in your application's .apk file. If
+ * you return a bitmap, such as a thumbnail, you can simply put the bitmap into the extras
+ * bundle using {@link android.content.Intent#EXTRA_SHORTCUT_ICON}.
+ *
+ * The shortcut intent can be any intent that you wish the launcher to send, when the user
+ * clicks on the shortcut. Typically this will be {@link android.content.Intent#ACTION_VIEW}
+ * with an appropriate Uri for your content, but any Intent will work here as long as it
+ * triggers the desired action within your Activity.
+ * @param profile
+ */
+ private void setupShortcut(VpnProfile profile) {
+ // First, set up the shortcut intent. For this example, we simply create an intent that
+ // will bring us directly back to this activity. A more typical implementation would use a
+ // data Uri in order to display a more specific result, or a custom action in order to
+ // launch a specific operation.
+
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ shortcutIntent.setClass(this, LaunchVPN.class);
+ shortcutIntent.putExtra(EXTRA_KEY,profile.getUUID().toString());
+
+ // Then, set up the container intent (the response to the caller)
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, profile.getName());
+ Parcelable iconResource = Intent.ShortcutIconResource.fromContext(
+ this, R.drawable.icon);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+
+ // Now, return the result to the launcher
+
+ setResult(RESULT_OK, intent);
+ }
+
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ String profilename = ((TextView) view).getText().toString();
+
+ VpnProfile profile = mPM.getProfileByName(profilename);
+
+ // if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ setupShortcut(profile);
+ finish();
+ return;
+ // }
+
+ }
+
+
+
+ private void askForPW(final int type) {
+
+ final EditText entry = new EditText(this);
+ entry.setSingleLine();
+ entry.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ entry.setTransformationMethod(new PasswordTransformationMethod());
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle("Need " + getString(type));
+ dialog.setMessage("Enter the password for profile " + mSelectedProfile.mName);
+ dialog.setView(entry);
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String pw = entry.getText().toString();
+ if(type == R.string.password) {
+ mSelectedProfile.mTransientPW = pw;
+ } else {
+ mSelectedProfile.mTransientPCKS12PW = pw;
+ }
+ onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null);
+
+ }
+
+ });
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+
+ dialog.create().show();
+
+ }
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if(requestCode==START_VPN_PROFILE) {
+ if(resultCode == Activity.RESULT_OK) {
+ int needpw = mSelectedProfile.needUserPWInput();
+ if(needpw !=0) {
+ askForPW(needpw);
+ } else {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean showlogwindow = prefs.getBoolean("showlogwindow", true);
+
+ if(!mhideLog && showlogwindow)
+ showLogWindow();
+ new startOpenVpnThread().start();
+ }
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ // User does not want us to start, so we just vanish
+ finish();
+ }
+ }
+ }
+ void showLogWindow() {
+
+ Intent startLW = new Intent(getBaseContext(),LogWindow.class);
+ startLW.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivity(startLW);
+
+ }
+
+ void showConfigErrorDialog(int vpnok) {
+ AlertDialog.Builder d = new AlertDialog.Builder(this);
+ d.setTitle(R.string.config_error_found);
+ d.setMessage(vpnok);
+ d.setPositiveButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+
+ }
+ });
+ d.show();
+ }
+
+ void launchVPN () {
+ int vpnok = mSelectedProfile.checkProfile(this);
+ if(vpnok!= R.string.no_error_found) {
+ showConfigErrorDialog(vpnok);
+ return;
+ }
+
+ Intent intent = VpnService.prepare(this);
+ // Check if we want to fix /dev/tun
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean usecm9fix = prefs.getBoolean("useCM9Fix", false);
+ boolean loadTunModule = prefs.getBoolean("loadTunModule", false);
+
+ if(loadTunModule)
+ execeuteSUcmd("insmod /system/lib/modules/tun.ko");
+
+ if(usecm9fix && !mCmfixed ) {
+ execeuteSUcmd("chown system /dev/tun");
+ }
+
+
+ if (intent != null) {
+ // Start the query
+ try {
+ startActivityForResult(intent, START_VPN_PROFILE);
+ } catch (ActivityNotFoundException ane) {
+ // Shame on you Sony! At least one user reported that
+ // an official Sony Xperia Arc S image triggers this exception
+ OpenVPN.logError(R.string.no_vpn_support_image);
+ showLogWindow();
+ }
+ } else {
+ onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null);
+ }
+
+ }
+
+ private void execeuteSUcmd(String command) {
+ ProcessBuilder pb = new ProcessBuilder(new String[] {"su","-c",command});
+ try {
+ Process p = pb.start();
+ int ret = p.waitFor();
+ if(ret ==0)
+ mCmfixed=true;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private class startOpenVpnThread extends Thread {
+
+ @Override
+ public void run() {
+ VPNLaunchHelper.startOpenVpn(mSelectedProfile, getBaseContext());
+ finish();
+
+ }
+
+ }
+
+
+}
diff --git a/src/se/leap/openvpn/LogWindow.java b/src/se/leap/openvpn/LogWindow.java
new file mode 100644
index 00000000..abd1acd6
--- /dev/null
+++ b/src/se/leap/openvpn/LogWindow.java
@@ -0,0 +1,351 @@
+package se.leap.openvpn;
+
+import java.util.Vector;
+import se.leap.leapclient.R;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.ListActivity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import se.leap.openvpn.OpenVPN.LogItem;
+import se.leap.openvpn.OpenVPN.LogListener;
+import se.leap.openvpn.OpenVPN.StateListener;
+
+public class LogWindow extends ListActivity implements StateListener {
+ private static final int START_VPN_CONFIG = 0;
+ private String[] mBconfig=null;
+
+
+ class LogWindowListAdapter implements ListAdapter, LogListener, Callback {
+
+ private static final int MESSAGE_NEWLOG = 0;
+
+ private static final int MESSAGE_CLEARLOG = 1;
+
+ private Vector<String> myEntries=new Vector<String>();
+
+ private Handler mHandler;
+
+ private Vector<DataSetObserver> observers=new Vector<DataSetObserver>();
+
+
+ public LogWindowListAdapter() {
+ initLogBuffer();
+
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ OpenVPN.addLogListener(this);
+ }
+
+
+
+ private void initLogBuffer() {
+ myEntries.clear();
+ for (LogItem litem : OpenVPN.getlogbuffer()) {
+ myEntries.add(litem.getString(getContext()));
+ }
+ }
+
+ String getLogStr() {
+ String str = "";
+ for(String entry:myEntries) {
+ str+=entry + '\n';
+ }
+ return str;
+ }
+
+
+ private void shareLog() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr());
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, "Send Logfile"));
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ observers.add(observer);
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ observers.remove(observer);
+ }
+
+ @Override
+ public int getCount() {
+ return myEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return myEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView v;
+ if(convertView==null)
+ v = new TextView(getBaseContext());
+ else
+ v = (TextView) convertView;
+ v.setText(myEntries.get(position));
+ return v;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return myEntries.isEmpty();
+
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public void newLog(LogItem logmessage) {
+ Message msg = Message.obtain();
+ msg.what=MESSAGE_NEWLOG;
+ Bundle mbundle=new Bundle();
+ mbundle.putString("logmessage", logmessage.getString(getBaseContext()));
+ msg.setData(mbundle);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ // We have been called
+ if(msg.what==MESSAGE_NEWLOG) {
+
+ String logmessage = msg.getData().getString("logmessage");
+ myEntries.add(logmessage);
+
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+ } else if (msg.what == MESSAGE_CLEARLOG) {
+ initLogBuffer();
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ }
+
+ return true;
+ }
+
+ void clearLog() {
+ // Actually is probably called from GUI Thread as result of the user
+ // pressing a button. But better safe than sorry
+ OpenVPN.clearLog();
+ OpenVPN.logMessage(0,"","Log cleared.");
+ mHandler.sendEmptyMessage(MESSAGE_CLEARLOG);
+ }
+ }
+
+
+
+ private LogWindowListAdapter ladapter;
+ private TextView mSpeedView;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==R.id.clearlog) {
+ ladapter.clearLog();
+ return true;
+ } else if(item.getItemId()==R.id.cancel){
+ Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.title_cancel);
+ builder.setMessage(R.string.cancel_connection_query);
+ builder.setNegativeButton(android.R.string.no, null);
+ builder.setPositiveButton(android.R.string.yes, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ProfileManager.setConntectedVpnProfileDisconnected(getApplicationContext());
+ OpenVpnManagementThread.stopOpenVPN();
+ }
+ });
+
+ builder.show();
+ return true;
+ } else if(item.getItemId()==R.id.info) {
+ if(mBconfig==null)
+ OpenVPN.triggerLogBuilderConfig();
+
+ } else if(item.getItemId()==R.id.send) {
+ ladapter.shareLog();
+ } else if(item.getItemId()==R.id.edit_vpn) {
+ VpnProfile lastConnectedprofile = ProfileManager.getLastConnectedVpn();
+
+ if(lastConnectedprofile!=null) {
+ Intent vprefintent = new Intent(this,VPNPreferences.class)
+ .putExtra(VpnProfile.EXTRA_PROFILEUUID,lastConnectedprofile.getUUIDString());
+ startActivityForResult(vprefintent,START_VPN_CONFIG);
+ } else {
+ Toast.makeText(this, R.string.log_no_last_vpn, Toast.LENGTH_LONG).show();
+ }
+
+
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ protected Context getContext() {
+ return this;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.logmenu, menu);
+ return true;
+ }
+
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ OpenVPN.addStateListener(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == START_VPN_CONFIG && resultCode==RESULT_OK) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ final VpnProfile profile = ProfileManager.get(configuredVPN);
+ ProfileManager.getInstance(this).saveProfile(this, profile);
+ // Name could be modified, reset List adapter
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle(R.string.configuration_changed);
+ dialog.setMessage(R.string.restart_vpn_after_change);
+
+
+ dialog.setPositiveButton(R.string.restart,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(getBaseContext(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+
+
+ });
+ dialog.setNegativeButton(R.string.ignore, null);
+ dialog.create().show();
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ OpenVPN.removeStateListener(this);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.logwindow);
+ ListView lv = getListView();
+
+ lv.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ ClipboardManager clipboard = (ClipboardManager)
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Log Entry",((TextView) view).getText());
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(getBaseContext(), R.string.copied_entry, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ });
+
+ ladapter = new LogWindowListAdapter();
+ lv.setAdapter(ladapter);
+
+ mSpeedView = (TextView) findViewById(R.id.speed);
+ }
+
+ @Override
+ public void updateState(final String status,final String logmessage, final int resid) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ String prefix=getString(resid) + ":";
+ if (status.equals("BYTECOUNT") || status.equals("NOPROCESS") )
+ prefix="";
+ mSpeedView.setText(prefix + logmessage);
+ }
+ });
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ OpenVPN.removeLogListener(ladapter);
+ }
+
+}
diff --git a/src/se/leap/openvpn/MainActivity.java b/src/se/leap/openvpn/MainActivity.java
new file mode 100644
index 00000000..d33fd832
--- /dev/null
+++ b/src/se/leap/openvpn/MainActivity.java
@@ -0,0 +1,44 @@
+package se.leap.openvpn;
+
+import java.util.List;
+import se.leap.leapclient.R;
+
+import android.content.Intent;
+import android.preference.PreferenceActivity;
+
+public class MainActivity extends PreferenceActivity {
+
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ loadHeadersFromResource(R.xml.main_headers, target);
+
+
+ String translatedby = getString(R.string.translationby);
+ if(!translatedby.equals("") && !translatedby.contains("Arne Schwabe")) {
+ Header translation = new Header();
+ translation.title = getString(R.string.translation);
+ translation.summary = translatedby;
+ target.add(translation);
+ }
+
+ if(SendDumpActivity.getLastestDump(this)!=null) {
+ Header sendDump = new Header();
+ sendDump.titleRes = R.string.send_minidump;
+ sendDump.summaryRes = R.string.send_minidump_summary;
+ sendDump.intent = new Intent(this,SendDumpActivity.class);
+ target.add(sendDump);
+ }
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ System.out.println(data);
+
+
+ }
+
+
+}
diff --git a/src/se/leap/openvpn/NetworkSateReceiver.java b/src/se/leap/openvpn/NetworkSateReceiver.java
new file mode 100644
index 00000000..301803b6
--- /dev/null
+++ b/src/se/leap/openvpn/NetworkSateReceiver.java
@@ -0,0 +1,86 @@
+package se.leap.openvpn;
+
+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 se.leap.leapclient.R;
+
+public class NetworkSateReceiver extends BroadcastReceiver {
+ private int lastNetwork=-1;
+ private OpenVpnManagementThread mManangement;
+
+ private String lastStateMsg=null;
+
+ public NetworkSateReceiver(OpenVpnManagementThread managementThread) {
+ super();
+ mManangement = managementThread;
+ }
+
+ @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)
+ mManangement.reconnect();
+
+ lastNetwork = newnet;
+ } else if (networkInfo==null) {
+ // Not connected, stop openvpn, set last connected network to no network
+ lastNetwork=-1;
+ if(sendusr1)
+ mManangement.signalusr1();
+ }
+
+ 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/se/leap/openvpn/OnBootReceiver.java b/src/se/leap/openvpn/OnBootReceiver.java
new file mode 100644
index 00000000..d97097c3
--- /dev/null
+++ b/src/se/leap/openvpn/OnBootReceiver.java
@@ -0,0 +1,33 @@
+package se.leap.openvpn;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+
+public class OnBootReceiver extends BroadcastReceiver {
+
+ // Debug: am broadcast -a android.intent.action.BOOT_COMPLETED
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ final String action = intent.getAction();
+
+ if(Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ VpnProfile bootProfile = ProfileManager.getOnBootProfile(context);
+ if(bootProfile != null) {
+ lauchVPN(bootProfile, context);
+ }
+ }
+ }
+
+ void lauchVPN(VpnProfile profile,Context context) {
+ Intent startVpnIntent = new Intent(Intent.ACTION_MAIN);
+ startVpnIntent.setClass(context, LaunchVPN.class);
+ startVpnIntent.putExtra(LaunchVPN.EXTRA_KEY,profile.getUUIDString());
+ startVpnIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startVpnIntent.putExtra(LaunchVPN.EXTRA_HIDELOG, true);
+
+ context.startActivity(startVpnIntent);
+ }
+}
diff --git a/src/se/leap/openvpn/OpenVPN.java b/src/se/leap/openvpn/OpenVPN.java
new file mode 100644
index 00000000..152cf2d8
--- /dev/null
+++ b/src/se/leap/openvpn/OpenVPN.java
@@ -0,0 +1,244 @@
+package se.leap.openvpn;
+
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Vector;
+import se.leap.leapclient.R;
+
+
+import android.content.Context;
+import android.os.Build;
+
+public class OpenVPN {
+
+
+ public static LinkedList<LogItem> logbuffer;
+
+ private static Vector<LogListener> logListener;
+ private static Vector<StateListener> stateListener;
+ private static String[] mBconfig;
+
+ private static String mLaststatemsg;
+
+ private static String mLaststate;
+
+ private static int mLastStateresid=R.string.state_noprocess;
+
+ static {
+ logbuffer = new LinkedList<LogItem>();
+ logListener = new Vector<OpenVPN.LogListener>();
+ stateListener = new Vector<OpenVPN.StateListener>();
+ logInformation();
+ }
+
+ public static class LogItem {
+ 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;
+
+ public LogItem(int ressourceId, Object[] args) {
+ mRessourceId = ressourceId;
+ mArgs = args;
+ }
+
+
+ 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(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;
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+
+ synchronized static void logMessage(int level,String prefix, String message)
+ {
+ newlogItem(new LogItem(prefix + message));
+
+ }
+
+ 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 addStateListener(StateListener sl){
+ stateListener.add(sl);
+ if(mLaststate!=null)
+ sl.updateState(mLaststate, mLaststatemsg, mLastStateresid);
+ }
+
+ 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("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;
+
+ }
+
+ 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);
+ updateStateString(state, msg,rid);
+ }
+
+ public synchronized static void updateStateString(String state, String msg, int resid) {
+ if (! "BYTECOUNT".equals(state)) {
+ mLaststate= state;
+ mLaststatemsg = msg;
+ mLastStateresid = resid;
+ }
+
+ for (StateListener sl : stateListener) {
+ sl.updateState(state,msg,resid);
+ }
+ }
+
+ 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 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));
+ }
+
+}
diff --git a/src/se/leap/openvpn/OpenVPNThread.java b/src/se/leap/openvpn/OpenVPNThread.java
new file mode 100644
index 00000000..3f9a0d91
--- /dev/null
+++ b/src/se/leap/openvpn/OpenVPNThread.java
@@ -0,0 +1,129 @@
+package se.leap.openvpn;
+
+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.util.LinkedList;
+import se.leap.leapclient.R;
+
+import android.util.Log;
+import se.leap.openvpn.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);
+ if(mDumpPath!=null) {
+ try {
+ BufferedWriter logout = new BufferedWriter(new FileWriter(mDumpPath + ".log"));
+ for(LogItem li :OpenVPN.getlogbuffer()){
+ logout.write(li.getString(null) + "\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/se/leap/openvpn/OpenVpnManagementThread.java b/src/se/leap/openvpn/OpenVpnManagementThread.java
new file mode 100644
index 00000000..43d4f701
--- /dev/null
+++ b/src/se/leap/openvpn/OpenVpnManagementThread.java
@@ -0,0 +1,577 @@
+package se.leap.openvpn;
+
+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.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.LinkedList;
+import java.util.Vector;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import se.leap.leapclient.R;
+
+import android.content.SharedPreferences;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.preference.PreferenceManager;
+import android.util.Base64;
+import android.util.Log;
+
+public class OpenVpnManagementThread implements Runnable {
+
+ private static final String TAG = "openvpn";
+ private LocalSocket mSocket;
+ private VpnProfile mProfile;
+ private OpenVpnService mOpenVPNService;
+ private LinkedList<FileDescriptor> mFDList=new LinkedList<FileDescriptor>();
+ private int mBytecountinterval=2;
+ private long mLastIn=0;
+ private long mLastOut=0;
+ 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);
+ static private native byte[] rsasign(byte[] input,int pkey) throws InvalidKeyException;
+
+ 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);
+ }
+ }
+ 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("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));
+
+ long diffin = in - mLastIn;
+ long diffout = out - mLastOut;
+
+ mLastIn=in;
+ mLastOut=out;
+
+ String netstat = String.format("In: %8s, %8s/s Out %8s, %8s/s",
+ humanReadableByteCount(in, false),
+ humanReadableByteCount(diffin, false),
+ humanReadableByteCount(out, false),
+ humanReadableByteCount(diffout, false));
+ OpenVPN.updateStateString("BYTECOUNT",netstat);
+
+
+ }
+
+ // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java
+ public static String humanReadableByteCount(long bytes, boolean si) {
+ int unit = si ? 1000 : 1024;
+ if (bytes < unit) return bytes + " B";
+ int exp = (int) (Math.log(bytes) / Math.log(unit));
+ String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
+ return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
+ }
+
+ 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);
+ }
+ private void logStatusMessage(String command) {
+ OpenVPN.logMessage(0,"MGMT:", command);
+ }
+
+
+ public 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) {
+
+ PrivateKey privkey = mProfile.getKeystoreKey();
+ Exception err =null;
+
+ byte[] data = Base64.decode(b64data, Base64.DEFAULT);
+
+ // The Jelly Bean *evil* Hack
+ // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider
+ if(Build.VERSION.SDK_INT==16){
+ processSignJellyBeans(privkey,data);
+ return;
+ }
+
+
+ try{
+
+
+ Cipher rsasinger = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
+
+ rsasinger.init(Cipher.ENCRYPT_MODE, privkey);
+
+ byte[] signed_bytes = rsasinger.doFinal(data);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+ } catch (NoSuchAlgorithmException e){
+ err =e;
+ } catch (InvalidKeyException e) {
+ err =e;
+ } catch (NoSuchPaddingException e) {
+ err =e;
+ } catch (IllegalBlockSizeException e) {
+ err =e;
+ } catch (BadPaddingException e) {
+ err =e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage());
+ }
+
+ }
+
+
+ private void processSignJellyBeans(PrivateKey privkey, byte[] data) {
+ Exception err =null;
+ try {
+ Method[] allm = privkey.getClass().getSuperclass().getDeclaredMethods();
+ System.out.println(allm);
+ Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey");
+ getKey.setAccessible(true);
+
+ // Real object type is OpenSSLKey
+ Object opensslkey = getKey.invoke(privkey);
+
+ getKey.setAccessible(false);
+
+ Method getPkeyContext = opensslkey.getClass().getDeclaredMethod("getPkeyContext");
+
+ // integer pointer to EVP_pkey
+ getPkeyContext.setAccessible(true);
+ int pkey = (Integer) getPkeyContext.invoke(opensslkey);
+ getPkeyContext.setAccessible(false);
+
+ byte[] signed_bytes = rsasign(data, pkey);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+
+ } catch (NoSuchMethodException e) {
+ err=e;
+ } catch (IllegalArgumentException e) {
+ err=e;
+ } catch (IllegalAccessException e) {
+ err=e;
+ } catch (InvocationTargetException e) {
+ err=e;
+ } catch (InvalidKeyException e) {
+ err=e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage());
+ }
+
+ }
+}
diff --git a/src/se/leap/openvpn/OpenVpnPreferencesFragment.java b/src/se/leap/openvpn/OpenVpnPreferencesFragment.java
new file mode 100644
index 00000000..31746332
--- /dev/null
+++ b/src/se/leap/openvpn/OpenVpnPreferencesFragment.java
@@ -0,0 +1,52 @@
+package se.leap.openvpn;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import se.leap.leapclient.R;
+
+public abstract class OpenVpnPreferencesFragment extends PreferenceFragment {
+
+ protected VpnProfile mProfile;
+
+ protected abstract void loadSettings();
+ protected abstract void saveSettings();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile = ProfileManager.get(getActivity(),profileUUID);
+ getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ saveSettings();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if(savedInstanceState!=null) {
+ String profileUUID=savedInstanceState.getString(VpnProfile.EXTRA_PROFILEUUID);
+ mProfile = ProfileManager.get(getActivity(),profileUUID);
+ loadSettings();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ // TODO Auto-generated method stub
+ super.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveSettings();
+ outState.putString(VpnProfile.EXTRA_PROFILEUUID, mProfile.getUUIDString());
+ }
+}
diff --git a/src/se/leap/openvpn/OpenVpnService.java b/src/se/leap/openvpn/OpenVpnService.java
new file mode 100644
index 00000000..c745ee3b
--- /dev/null
+++ b/src/se/leap/openvpn/OpenVpnService.java
@@ -0,0 +1,509 @@
+/*
+ * 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 se.leap.openvpn;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Vector;
+import se.leap.leapclient.R;
+
+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.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.Handler;
+import android.os.Handler.Callback;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import se.leap.openvpn.OpenVPN.StateListener;
+
+public class OpenVpnService extends VpnService implements StateListener, Callback {
+ public static final String START_SERVICE = "se.leap.openvpn.START_SERVICE";
+
+ 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 OpenVpnManagementThread mSocketManager;
+
+ private Thread mSocketManagerThread;
+ 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 final IBinder mBinder = new LocalBinder();
+
+ 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() {
+ OpenVpnManagementThread.stopOpenVPN();
+ endVpnService();
+ }
+
+ // Similar to revoke but do not try to stop process
+ public void processDied() {
+ endVpnService();
+ }
+
+ private void endVpnService() {
+ mProcessThread=null;
+ OpenVPN.logBuilderConfig(null);
+ ProfileManager.setConntectedVpnProfileDisconnected(this);
+ if(!mStarting) {
+ stopSelf();
+ stopForeground(true);
+ }
+ }
+
+ private void showNotification(String msg, String tickerText, boolean lowpriority, long when) {
+ String ns = Context.NOTIFICATION_SERVICE;
+ NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);
+
+
+ int icon = R.drawable.ic_stat_vpn;
+ android.app.Notification.Builder nbuilder = new Notification.Builder(this);
+
+ nbuilder.setContentTitle(getString(R.string.notifcation_title,mProfile.mName));
+ nbuilder.setContentText(msg);
+ nbuilder.setOnlyAlertOnce(true);
+ nbuilder.setOngoing(true);
+ nbuilder.setContentIntent(getLogPendingIntent());
+ nbuilder.setSmallIcon(icon);
+ if(when !=0)
+ nbuilder.setWhen(when);
+
+
+ // Try to set the priority available since API 16 (Jellybean)
+ jbNotificationExtras(lowpriority, nbuilder);
+ if(tickerText!=null)
+ 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 );
+
+ nbuilder.setUsesChronometer(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");
+ 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;
+
+
+ }
+
+ void registerNetworkStateReceiver() {
+ // Registers BroadcastReceiver to track network connection changes.
+ IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ mNetworkStateReceiver = new NetworkSateReceiver(mSocketManager);
+ this.registerReceiver(mNetworkStateReceiver, filter);
+ }
+
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+
+ if(intent != null && intent.getAction() !=null &&intent.getAction().equals(START_SERVICE))
+ return START_NOT_STICKY;
+
+
+ // 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);
+
+
+ OpenVPN.addStateListener(this);
+
+ // Set a flag that we are starting a new VPN
+ mStarting=true;
+ // Stop the previous session by interrupting the thread.
+ if(OpenVpnManagementThread.stopOpenVPN()){
+ // an old was asked to exit, wait 2s
+ 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
+ LocalServerSocket mgmtsocket = openManagmentInterface(8);
+
+ if(mgmtsocket!=null) {
+ // start a Thread that handles incoming messages of the managment socket
+ mSocketManager = new OpenVpnManagementThread(mProfile,mgmtsocket,this);
+ mSocketManagerThread = new Thread(mSocketManager,"OpenVPNMgmtThread");
+ mSocketManagerThread.start();
+ OpenVPN.logInfo("started Socket Thread");
+ registerNetworkStateReceiver();
+ }
+
+
+ // Start a new session by creating a new thread.
+ OpenVPNThread processThread = new OpenVPNThread(this, argv,nativelibdir);
+
+ mProcessThread = new Thread(processThread, "OpenVPNProcessThread");
+ mProcessThread.start();
+
+ ProfileManager.setConnectedVpnProfile(this, mProfile);
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mProcessThread != null) {
+ mSocketManager.managmentCommand("signal SIGINT\n");
+
+ mProcessThread.interrupt();
+ }
+ if (mNetworkStateReceiver!= null) {
+ this.unregisterReceiver(mNetworkStateReceiver);
+ }
+
+ }
+
+
+
+ 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(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 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(mode.equals("net30"))
+ 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) {
+ // If the process is not running, ignore any state,
+ // Notification should be invisible in this state
+ if(mProcessThread==null)
+ return;
+
+ // Display byte count only after being connected
+
+ if("BYTECOUNT".equals(state)) {
+ if(mDisplayBytecount) {
+ showNotification(logmessage,null,true,mConnecttime);
+ }
+ } else {
+ if("CONNECTED".equals(state)) {
+ 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);
+
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ Runnable r = msg.getCallback();
+ if(r!=null){
+ r.run();
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/se/leap/openvpn/ProfileManager.java b/src/se/leap/openvpn/ProfileManager.java
new file mode 100644
index 00000000..b9eb3ab6
--- /dev/null
+++ b/src/se/leap/openvpn/ProfileManager.java
@@ -0,0 +1,220 @@
+package se.leap.openvpn;
+
+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 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/se/leap/openvpn/ProxyDetection.java b/src/se/leap/openvpn/ProxyDetection.java
new file mode 100644
index 00000000..7114857d
--- /dev/null
+++ b/src/se/leap/openvpn/ProxyDetection.java
@@ -0,0 +1,53 @@
+package se.leap.openvpn;
+
+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 se.leap.leapclient.R;
+
+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/se/leap/openvpn/SendDumpActivity.java b/src/se/leap/openvpn/SendDumpActivity.java
new file mode 100644
index 00000000..20194db0
--- /dev/null
+++ b/src/se/leap/openvpn/SendDumpActivity.java
@@ -0,0 +1,59 @@
+package se.leap.openvpn;
+
+import java.io.File;
+import java.util.ArrayList;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+public class SendDumpActivity extends Activity {
+
+ protected void onStart() {
+ super.onStart();
+ emailMiniDumps();
+ finish();
+ };
+
+ public void emailMiniDumps()
+ {
+ //need to "send multiple" to get more than one attachment
+ final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);
+ emailIntent.setType("*/*");
+ emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
+ new String[]{"Arne Schwabe <arne@rfc2549.org>"});
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, "OpenVPN Minidump");
+
+ emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe the issue you have experienced");
+
+ ArrayList<Uri> uris = new ArrayList<Uri>();
+
+ File ldump = getLastestDump(this);
+ if(ldump==null) {
+ OpenVPN.logError("No Minidump found!");
+ }
+
+ uris.add(Uri.parse("content://se.leap.openvpn.FileProvider/" + ldump.getName()));
+ uris.add(Uri.parse("content://se.leap.openvpn.FileProvider/" + ldump.getName() + ".log"));
+
+ emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ startActivity(emailIntent);
+ }
+
+ static public File getLastestDump(Context c) {
+ long newestDumpTime=0;
+ File newestDumpFile=null;
+
+ for(File f:c.getCacheDir().listFiles()) {
+ if(!f.getName().endsWith(".dmp"))
+ continue;
+
+ if (newestDumpTime < f.lastModified()) {
+ newestDumpTime = f.lastModified();
+ newestDumpFile=f;
+ }
+ }
+ return newestDumpFile;
+ }
+}
diff --git a/src/se/leap/openvpn/Settings_Authentication.java b/src/se/leap/openvpn/Settings_Authentication.java
new file mode 100644
index 00000000..00f4c08a
--- /dev/null
+++ b/src/se/leap/openvpn/Settings_Authentication.java
@@ -0,0 +1,136 @@
+package se.leap.openvpn;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.SwitchPreference;
+import se.leap.leapclient.R;
+
+
+public class Settings_Authentication extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener, OnPreferenceClickListener {
+ private static final int SELECT_TLS_FILE = 23223232;
+ private CheckBoxPreference mExpectTLSCert;
+ private CheckBoxPreference mCheckRemoteCN;
+ private EditTextPreference mRemoteCN;
+ private ListPreference mTLSAuthDirection;
+ private Preference mTLSAuthFile;
+ private SwitchPreference mUseTLSAuth;
+ private EditTextPreference mCipher;
+ private String mTlsAuthFileData;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_authentification);
+
+ mExpectTLSCert = (CheckBoxPreference) findPreference("remoteServerTLS");
+ mCheckRemoteCN = (CheckBoxPreference) findPreference("checkRemoteCN");
+ mRemoteCN = (EditTextPreference) findPreference("remotecn");
+ mRemoteCN.setOnPreferenceChangeListener(this);
+
+ mUseTLSAuth = (SwitchPreference) findPreference("useTLSAuth" );
+ mTLSAuthFile = findPreference("tlsAuthFile");
+ mTLSAuthDirection = (ListPreference) findPreference("tls_direction");
+
+
+ mTLSAuthFile.setOnPreferenceClickListener(this);
+
+ mCipher =(EditTextPreference) findPreference("cipher");
+ mCipher.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mExpectTLSCert.setChecked(mProfile.mExpectTLSCert);
+ mCheckRemoteCN.setChecked(mProfile.mCheckRemoteCN);
+ mRemoteCN.setText(mProfile.mRemoteCN);
+ onPreferenceChange(mRemoteCN, mProfile.mRemoteCN);
+
+ mUseTLSAuth.setChecked(mProfile.mUseTLSAuth);
+ mTlsAuthFileData= mProfile.mTLSAuthFilename;
+ setTlsAuthSummary(mTlsAuthFileData);
+ mTLSAuthDirection.setValue(mProfile.mTLSAuthDirection);
+ mCipher.setText(mProfile.mCipher);
+ onPreferenceChange(mCipher, mProfile.mCipher);
+ }
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mExpectTLSCert=mExpectTLSCert.isChecked();
+ mProfile.mCheckRemoteCN=mCheckRemoteCN.isChecked();
+ mProfile.mRemoteCN=mRemoteCN.getText();
+
+ mProfile.mUseTLSAuth = mUseTLSAuth.isChecked();
+ mProfile.mTLSAuthFilename = mTlsAuthFileData;
+
+ if(mTLSAuthDirection.getValue()==null)
+ mProfile.mTLSAuthDirection=null;
+ else
+ mProfile.mTLSAuthDirection = mTLSAuthDirection.getValue().toString();
+
+ if(mCipher.getText()==null)
+ mProfile.mCipher=null;
+ else
+ mProfile.mCipher = mCipher.getText();
+
+ }
+
+
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(preference==mRemoteCN) {
+ if ("".equals(newValue))
+ preference.setSummary(mProfile.mServerName);
+ else
+ preference.setSummary((String)newValue);
+ } else if (preference == mCipher) {
+ preference.setSummary((CharSequence) newValue);
+ }
+ return true;
+ }
+ void startFileDialog() {
+ Intent startFC = new Intent(getActivity(),FileSelect.class);
+ startFC.putExtra(FileSelect.START_DATA, Environment.getExternalStorageDirectory().getPath());
+
+ startActivityForResult(startFC,SELECT_TLS_FILE);
+ }
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startFileDialog();
+ return true;
+
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if(requestCode==SELECT_TLS_FILE && resultCode == Activity.RESULT_OK){
+ String result = data.getStringExtra(FileSelect.RESULT_DATA);
+ mTlsAuthFileData=result;
+ setTlsAuthSummary(result);
+
+ }
+ }
+
+ private void setTlsAuthSummary(String result) {
+ if(result==null) result = getString(R.string.no_certificate);
+ if(result.startsWith(VpnProfile.INLINE_TAG))
+ mTLSAuthFile.setSummary(R.string.inline_file_data);
+ else
+ mTLSAuthFile.setSummary(result);
+ }
+} \ No newline at end of file
diff --git a/src/se/leap/openvpn/Settings_Basic.java b/src/se/leap/openvpn/Settings_Basic.java
new file mode 100644
index 00000000..f3510da9
--- /dev/null
+++ b/src/se/leap/openvpn/Settings_Basic.java
@@ -0,0 +1,347 @@
+/*
+ * 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 se.leap.openvpn;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Fragment;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+import se.leap.leapclient.R.id;
+import se.leap.leapclient.R;
+
+public class Settings_Basic extends Fragment implements View.OnClickListener, OnItemSelectedListener, Callback {
+ private static final int CHOOSE_FILE_OFFSET = 1000;
+ private static final int UPDATE_ALIAS = 20;
+
+
+
+ private TextView mServerAddress;
+ private TextView mServerPort;
+ private FileSelectLayout mClientCert;
+ private FileSelectLayout mCaCert;
+ private FileSelectLayout mClientKey;
+ private TextView mAliasName;
+ private CheckBox mUseLzo;
+ private ToggleButton mTcpUdp;
+ private Spinner mType;
+ private FileSelectLayout mpkcs12;
+ private TextView mPKCS12Password;
+
+ private Handler mHandler;
+
+
+
+
+
+ private SparseArray<FileSelectLayout> fileselects = new SparseArray<FileSelectLayout>();
+
+
+ private EditText mUserName;
+
+
+ private EditText mPassword;
+
+
+ private View mView;
+
+
+ private VpnProfile mProfile;
+ private EditText mProfileName;
+ private EditText mKeyPassword;
+
+
+
+ private void addFileSelectLayout (FileSelectLayout fsl) {
+ int i = fileselects.size() + CHOOSE_FILE_OFFSET;
+ fileselects.put(i, fsl);
+ fsl.setFragment(this,i);
+ }
+
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String profileuuid =getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile=ProfileManager.get(profileuuid);
+ getActivity().setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+
+ mView = inflater.inflate(R.layout.basic_settings,container,false);
+
+ mProfileName = (EditText) mView.findViewById(R.id.profilename);
+ mServerAddress = (TextView) mView.findViewById(R.id.address);
+ mServerPort = (TextView) mView.findViewById(R.id.port);
+ mClientCert = (FileSelectLayout) mView.findViewById(R.id.certselect);
+ mClientKey = (FileSelectLayout) mView.findViewById(R.id.keyselect);
+ mCaCert = (FileSelectLayout) mView.findViewById(R.id.caselect);
+ mpkcs12 = (FileSelectLayout) mView.findViewById(R.id.pkcs12select);
+ mUseLzo = (CheckBox) mView.findViewById(R.id.lzo);
+ mTcpUdp = (ToggleButton) mView.findViewById(id.tcpudp);
+ mType = (Spinner) mView.findViewById(R.id.type);
+ mPKCS12Password = (TextView) mView.findViewById(R.id.pkcs12password);
+ mAliasName = (TextView) mView.findViewById(R.id.aliasname);
+
+ mUserName = (EditText) mView.findViewById(R.id.auth_username);
+ mPassword = (EditText) mView.findViewById(R.id.auth_password);
+ mKeyPassword = (EditText) mView.findViewById(R.id.key_password);
+
+
+
+ addFileSelectLayout(mCaCert);
+ addFileSelectLayout(mClientCert);
+ addFileSelectLayout(mClientKey);
+ addFileSelectLayout(mpkcs12);
+ mpkcs12.setBase64Encode();
+ mCaCert.setShowClear();
+
+
+ mType.setOnItemSelectedListener(this);
+
+ mView.findViewById(R.id.select_keystore_button).setOnClickListener(this);
+
+
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ return mView;
+ }
+
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ String profileuuid =getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ mProfile=ProfileManager.get(profileuuid);
+ loadPreferences();
+
+ }
+
+ @Override
+ public void onActivityResult(int request, int result, Intent data) {
+ if (result == Activity.RESULT_OK && request >= CHOOSE_FILE_OFFSET) {
+ String filedata = data.getStringExtra(FileSelect.RESULT_DATA);
+ FileSelectLayout fsl = fileselects.get(request);
+ fsl.setData(filedata);
+
+ savePreferences();
+
+ // Private key files may result in showing/hiding the private key password dialog
+ if(fsl==mClientKey) {
+ changeType(mType.getSelectedItemPosition());
+ }
+ }
+
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (parent == mType) {
+ changeType(position);
+ }
+ }
+ @Override
+ public void onPause() {
+ super.onPause();
+ savePreferences();
+ }
+
+
+
+ private void changeType(int type){
+ // hide everything
+ mView.findViewById(R.id.pkcs12).setVisibility(View.GONE);
+ mView.findViewById(R.id.certs).setVisibility(View.GONE);
+ mView.findViewById(R.id.statickeys).setVisibility(View.GONE);
+ mView.findViewById(R.id.keystore).setVisibility(View.GONE);
+ mView.findViewById(R.id.cacert).setVisibility(View.GONE);
+ mView.findViewById(R.id.userpassword).setVisibility(View.GONE);
+ mView.findViewById(R.id.key_password_layout).setVisibility(View.GONE);
+
+ // Fall through are by design
+ switch(type) {
+ case VpnProfile.TYPE_USERPASS_CERTIFICATES:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_CERTIFICATES:
+ mView.findViewById(R.id.certs).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ if(mProfile.requireTLSKeyPassword())
+ mView.findViewById(R.id.key_password_layout).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_PKCS12:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_PKCS12:
+ mView.findViewById(R.id.pkcs12).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_STATICKEYS:
+ mView.findViewById(R.id.statickeys).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_KEYSTORE:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ case VpnProfile.TYPE_KEYSTORE:
+ mView.findViewById(R.id.keystore).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ break;
+
+ case VpnProfile.TYPE_USERPASS:
+ mView.findViewById(R.id.userpassword).setVisibility(View.VISIBLE);
+ mView.findViewById(R.id.cacert).setVisibility(View.VISIBLE);
+ break;
+ }
+
+
+ }
+
+ private void loadPreferences() {
+ mProfileName.setText(mProfile.mName);
+ mClientCert.setData(mProfile.mClientCertFilename);
+ mClientKey.setData(mProfile.mClientKeyFilename);
+ mCaCert.setData(mProfile.mCaFilename);
+
+ mUseLzo.setChecked(mProfile.mUseLzo);
+ mServerPort.setText(mProfile.mServerPort);
+ mServerAddress.setText(mProfile.mServerName);
+ mTcpUdp.setChecked(mProfile.mUseUdp);
+ mType.setSelection(mProfile.mAuthenticationType);
+ mpkcs12.setData(mProfile.mPKCS12Filename);
+ mPKCS12Password.setText(mProfile.mPKCS12Password);
+ mUserName.setText(mProfile.mUsername);
+ mPassword.setText(mProfile.mPassword);
+ mKeyPassword.setText(mProfile.mKeyPassword);
+
+ setAlias();
+
+ }
+
+ void savePreferences() {
+
+ mProfile.mName = mProfileName.getText().toString();
+ mProfile.mCaFilename = mCaCert.getData();
+ mProfile.mClientCertFilename = mClientCert.getData();
+ mProfile.mClientKeyFilename = mClientKey.getData();
+
+ mProfile.mUseLzo = mUseLzo.isChecked();
+ mProfile.mServerPort =mServerPort.getText().toString();
+ mProfile.mServerName = mServerAddress.getText().toString();
+ mProfile.mUseUdp = mTcpUdp.isChecked();
+
+ mProfile.mAuthenticationType = mType.getSelectedItemPosition();
+ mProfile.mPKCS12Filename = mpkcs12.getData();
+ mProfile.mPKCS12Password = mPKCS12Password.getText().toString();
+
+ mProfile.mPassword = mPassword.getText().toString();
+ mProfile.mUsername = mUserName.getText().toString();
+ mProfile.mKeyPassword = mKeyPassword.getText().toString();
+
+ }
+
+
+ private void setAlias() {
+ if(mProfile.mAlias == null) {
+ mAliasName.setText(R.string.client_no_certificate);
+ } else {
+ mAliasName.setText(mProfile.mAlias);
+ }
+ }
+
+ public void showCertDialog () {
+ try {
+ KeyChain.choosePrivateKeyAlias(getActivity(),
+ new KeyChainAliasCallback() {
+
+ public void alias(String alias) {
+ // Credential alias selected. Remember the alias selection for future use.
+ mProfile.mAlias=alias;
+ mHandler.sendEmptyMessage(UPDATE_ALIAS);
+ }
+
+
+ },
+ new String[] {"RSA"}, // List of acceptable key types. null for any
+ null, // issuer, null for any
+ mProfile.mServerName, // host name of server requesting the cert, null if unavailable
+ -1, // port of server requesting the cert, -1 if unavailable
+ mProfile.mAlias); // alias to preselect, null if unavailable
+ } catch (ActivityNotFoundException anf) {
+ Builder ab = new AlertDialog.Builder(getActivity());
+ ab.setTitle(R.string.broken_image_cert_title);
+ ab.setMessage(R.string.broken_image_cert);
+ ab.setPositiveButton(android.R.string.ok, null);
+ ab.show();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mView.findViewById(R.id.select_keystore_button)) {
+ showCertDialog();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ savePreferences();
+ if(mProfile!=null) {
+ outState.putString(getActivity().getPackageName() + "profileUUID", mProfile.getUUID().toString());
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ setAlias();
+ return true;
+ }
+
+
+}
diff --git a/src/se/leap/openvpn/Settings_IP.java b/src/se/leap/openvpn/Settings_IP.java
new file mode 100644
index 00000000..df2efb40
--- /dev/null
+++ b/src/se/leap/openvpn/Settings_IP.java
@@ -0,0 +1,129 @@
+package se.leap.openvpn;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceManager;
+import android.preference.SwitchPreference;
+import se.leap.leapclient.R;
+
+public class Settings_IP extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private EditTextPreference mIPv4;
+ private EditTextPreference mIPv6;
+ private SwitchPreference mUsePull;
+ private CheckBoxPreference mOverrideDNS;
+ private EditTextPreference mSearchdomain;
+ private EditTextPreference mDNS1;
+ private EditTextPreference mDNS2;
+ private CheckBoxPreference mNobind;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+
+ // Make sure default values are applied. In a real app, you would
+ // want this in a shared function that is used to retrieve the
+ // SharedPreferences wherever they are needed.
+ PreferenceManager.setDefaultValues(getActivity(),
+ R.xml.vpn_ipsettings, false);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_ipsettings);
+ mIPv4 = (EditTextPreference) findPreference("ipv4_address");
+ mIPv6 = (EditTextPreference) findPreference("ipv6_address");
+ mUsePull = (SwitchPreference) findPreference("usePull");
+ mOverrideDNS = (CheckBoxPreference) findPreference("overrideDNS");
+ mSearchdomain =(EditTextPreference) findPreference("searchdomain");
+ mDNS1 = (EditTextPreference) findPreference("dns1");
+ mDNS2 = (EditTextPreference) findPreference("dns2");
+ mNobind = (CheckBoxPreference) findPreference("nobind");
+
+ mIPv4.setOnPreferenceChangeListener(this);
+ mIPv6.setOnPreferenceChangeListener(this);
+ mDNS1.setOnPreferenceChangeListener(this);
+ mDNS2.setOnPreferenceChangeListener(this);
+ mUsePull.setOnPreferenceChangeListener(this);
+ mOverrideDNS.setOnPreferenceChangeListener(this);
+ mSearchdomain.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mUsePull.setChecked(mProfile.mUsePull);
+ mIPv4.setText(mProfile.mIPv4Address);
+ mIPv6.setText(mProfile.mIPv6Address);
+ mDNS1.setText(mProfile.mDNS1);
+ mDNS2.setText(mProfile.mDNS2);
+ mOverrideDNS.setChecked(mProfile.mOverrideDNS);
+ mSearchdomain.setText(mProfile.mSearchDomain);
+ mNobind.setChecked(mProfile.mNobind);
+
+ // Sets Summary
+ onPreferenceChange(mIPv4, mIPv4.getText());
+ onPreferenceChange(mIPv6, mIPv6.getText());
+ onPreferenceChange(mDNS1, mDNS1.getText());
+ onPreferenceChange(mDNS2, mDNS2.getText());
+ onPreferenceChange(mSearchdomain, mSearchdomain.getText());
+
+ setDNSState();
+ }
+
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mUsePull = mUsePull.isChecked();
+ mProfile.mIPv4Address = mIPv4.getText();
+ mProfile.mIPv6Address = mIPv6.getText();
+ mProfile.mDNS1 = mDNS1.getText();
+ mProfile.mDNS2 = mDNS2.getText();
+ mProfile.mOverrideDNS = mOverrideDNS.isChecked();
+ mProfile.mSearchDomain = mSearchdomain.getText();
+ mProfile.mNobind = mNobind.isChecked();
+
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference,
+ Object newValue) {
+ if(preference==mIPv4 || preference == mIPv6
+ || preference==mDNS1 || preference == mDNS2
+ || preference == mSearchdomain
+ )
+
+ preference.setSummary((String)newValue);
+
+ if(preference== mUsePull || preference == mOverrideDNS)
+ if(preference==mOverrideDNS) {
+ // Set so the function gets the right value
+ mOverrideDNS.setChecked((Boolean) newValue);
+ }
+ setDNSState();
+
+ saveSettings();
+ return true;
+ }
+
+ private void setDNSState() {
+ boolean enabled;
+ mOverrideDNS.setEnabled(mUsePull.isChecked());
+ if(!mUsePull.isChecked())
+ enabled =true;
+ else if (mOverrideDNS.isChecked())
+ enabled = true;
+ else
+ enabled = false;
+
+ mDNS1.setEnabled(enabled);
+ mDNS2.setEnabled(enabled);
+ mSearchdomain.setEnabled(enabled);
+
+
+ }
+
+
+ } \ No newline at end of file
diff --git a/src/se/leap/openvpn/Settings_Obscure.java b/src/se/leap/openvpn/Settings_Obscure.java
new file mode 100644
index 00000000..ffbce971
--- /dev/null
+++ b/src/se/leap/openvpn/Settings_Obscure.java
@@ -0,0 +1,116 @@
+package se.leap.openvpn;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import se.leap.leapclient.R;
+
+public class Settings_Obscure extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private CheckBoxPreference mUseRandomHostName;
+ private CheckBoxPreference mUseFloat;
+ private CheckBoxPreference mUseCustomConfig;
+ private EditTextPreference mCustomConfig;
+ private ListPreference mLogverbosity;
+ private CheckBoxPreference mPersistent;
+ private ListPreference mConnectretrymax;
+ private EditTextPreference mConnectretry;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_obscure);
+
+
+ mUseRandomHostName = (CheckBoxPreference) findPreference("useRandomHostname");
+ mUseFloat = (CheckBoxPreference) findPreference("useFloat");
+ mUseCustomConfig = (CheckBoxPreference) findPreference("enableCustomOptions");
+ mCustomConfig = (EditTextPreference) findPreference("customOptions");
+ mLogverbosity = (ListPreference) findPreference("verblevel");
+ mPersistent = (CheckBoxPreference) findPreference("usePersistTun");
+ mConnectretrymax = (ListPreference) findPreference("connectretrymax");
+ mConnectretry = (EditTextPreference) findPreference("connectretry");
+
+ mLogverbosity.setOnPreferenceChangeListener(this);
+ mLogverbosity.setSummary("%s");
+
+ mConnectretrymax.setOnPreferenceChangeListener(this);
+ mConnectretrymax.setSummary("%s");
+
+ mConnectretry.setOnPreferenceChangeListener(this);
+
+
+ loadSettings();
+
+ }
+
+ protected void loadSettings() {
+ mUseRandomHostName.setChecked(mProfile.mUseRandomHostname);
+ mUseFloat.setChecked(mProfile.mUseFloat);
+ mUseCustomConfig.setChecked(mProfile.mUseCustomConfig);
+ mCustomConfig.setText(mProfile.mCustomConfigOptions);
+ mPersistent.setChecked(mProfile.mPersistTun);
+
+ mLogverbosity.setValue(mProfile.mVerb);
+ onPreferenceChange(mLogverbosity, mProfile.mVerb);
+
+ mConnectretrymax.setValue(mProfile.mConnectRetryMax);
+ onPreferenceChange(mConnectretrymax, mProfile.mConnectRetryMax);
+
+ mConnectretry.setText(mProfile.mConnectRetry);
+ onPreferenceChange(mConnectretry, mProfile.mConnectRetry);
+ }
+
+
+ protected void saveSettings() {
+ mProfile.mUseRandomHostname = mUseRandomHostName.isChecked();
+ mProfile.mUseFloat = mUseFloat.isChecked();
+ mProfile.mUseCustomConfig = mUseCustomConfig.isChecked();
+ mProfile.mCustomConfigOptions = mCustomConfig.getText();
+ mProfile.mVerb = mLogverbosity.getValue();
+ mProfile.mConnectRetryMax = mConnectretrymax.getValue();
+ mProfile.mPersistTun = mPersistent.isChecked();
+ mProfile.mConnectRetry = mConnectretry.getText();
+ }
+
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(preference==mLogverbosity) {
+ // Catch old version problem
+ if(newValue==null){
+ newValue="1";
+ }
+ mLogverbosity.setDefaultValue(newValue);
+ //This is idiotic.
+ int i =Integer.parseInt((String) newValue);
+
+ // verb >= 5 is not supported by the chooser
+ if(i < mLogverbosity.getEntries().length )
+ mLogverbosity.setSummary(mLogverbosity.getEntries()[i]);
+ else
+ mLogverbosity.setSummary(String.format("debug verbosity: %d",i));
+ } else if (preference == mConnectretrymax) {
+ if(newValue==null) {
+ newValue="5";
+ }
+ mConnectretrymax.setDefaultValue(newValue);
+
+ for(int i=0;i<mConnectretrymax.getEntryValues().length;i++){
+ if(mConnectretrymax.getEntryValues().equals(newValue))
+ mConnectretrymax.setSummary(mConnectretrymax.getEntries()[i]);
+ }
+
+ } else if (preference == mConnectretry) {
+ if(newValue==null || newValue=="")
+ newValue="5";
+ mConnectretry.setSummary(String.format("%s s" , newValue));
+ }
+
+ return true;
+ }
+
+} \ No newline at end of file
diff --git a/src/se/leap/openvpn/Settings_Routing.java b/src/se/leap/openvpn/Settings_Routing.java
new file mode 100644
index 00000000..48c09ff0
--- /dev/null
+++ b/src/se/leap/openvpn/Settings_Routing.java
@@ -0,0 +1,73 @@
+package se.leap.openvpn;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import se.leap.leapclient.R;
+
+
+public class Settings_Routing extends OpenVpnPreferencesFragment implements OnPreferenceChangeListener {
+ private EditTextPreference mCustomRoutes;
+ private CheckBoxPreference mUseDefaultRoute;
+ private EditTextPreference mCustomRoutesv6;
+ private CheckBoxPreference mUseDefaultRoutev6;
+ private CheckBoxPreference mRouteNoPull;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.vpn_routing);
+ mCustomRoutes = (EditTextPreference) findPreference("customRoutes");
+ mUseDefaultRoute = (CheckBoxPreference) findPreference("useDefaultRoute");
+ mCustomRoutesv6 = (EditTextPreference) findPreference("customRoutesv6");
+ mUseDefaultRoutev6 = (CheckBoxPreference) findPreference("useDefaultRoutev6");
+ mRouteNoPull = (CheckBoxPreference) findPreference("routenopull");
+
+ mCustomRoutes.setOnPreferenceChangeListener(this);
+ mCustomRoutesv6.setOnPreferenceChangeListener(this);
+
+ loadSettings();
+ }
+
+ @Override
+ protected void loadSettings() {
+
+ mUseDefaultRoute.setChecked(mProfile.mUseDefaultRoute);
+ mUseDefaultRoutev6.setChecked(mProfile.mUseDefaultRoutev6);
+
+ mCustomRoutes.setText(mProfile.mCustomRoutes);
+ mCustomRoutesv6.setText(mProfile.mCustomRoutesv6);
+
+ mRouteNoPull.setChecked(mProfile.mRoutenopull);
+
+ // Sets Summary
+ onPreferenceChange(mCustomRoutes, mCustomRoutes.getText());
+ onPreferenceChange(mCustomRoutesv6, mCustomRoutesv6.getText());
+ mRouteNoPull.setEnabled(mProfile.mUsePull);
+ }
+
+
+ @Override
+ protected void saveSettings() {
+ mProfile.mUseDefaultRoute = mUseDefaultRoute.isChecked();
+ mProfile.mUseDefaultRoutev6 = mUseDefaultRoutev6.isChecked();
+ mProfile.mCustomRoutes = mCustomRoutes.getText();
+ mProfile.mCustomRoutesv6 = mCustomRoutesv6.getText();
+ mProfile.mRoutenopull = mRouteNoPull.isChecked();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference,
+ Object newValue) {
+ if( preference == mCustomRoutes || preference == mCustomRoutesv6 )
+ preference.setSummary((String)newValue);
+
+ saveSettings();
+ return true;
+ }
+
+
+} \ No newline at end of file
diff --git a/src/se/leap/openvpn/ShowConfigFragment.java b/src/se/leap/openvpn/ShowConfigFragment.java
new file mode 100644
index 00000000..9bce67a6
--- /dev/null
+++ b/src/se/leap/openvpn/ShowConfigFragment.java
@@ -0,0 +1,67 @@
+package se.leap.openvpn;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import se.leap.leapclient.R;
+
+
+public class ShowConfigFragment extends Fragment {
+ private String configtext;
+ public android.view.View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ {
+ String profileUUID = getArguments().getString(getActivity().getPackageName() + ".profileUUID");
+ VpnProfile vp = ProfileManager.get(profileUUID);
+ View v=inflater.inflate(R.layout.viewconfig, container,false);
+ TextView cv = (TextView) v.findViewById(R.id.configview);
+
+ int check=vp.checkProfile(getActivity());
+ if(check!=R.string.no_error_found) {
+ cv.setText(check);
+ configtext = getString(check);
+ }
+ else {
+ String cfg=vp.getConfigFile(getActivity());
+ configtext= cfg;
+ cv.setText(cfg);
+ }
+ return v;
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.configmenu, menu);
+ }
+
+ private void shareConfig() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, configtext);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.export_config_title));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, "Export Configfile"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.sendConfig) {
+ shareConfig();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/src/se/leap/openvpn/VPNLaunchHelper.java b/src/se/leap/openvpn/VPNLaunchHelper.java
new file mode 100644
index 00000000..59303748
--- /dev/null
+++ b/src/se/leap/openvpn/VPNLaunchHelper.java
@@ -0,0 +1,75 @@
+package se.leap.openvpn;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import se.leap.leapclient.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+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);
+
+ }
+}
diff --git a/src/se/leap/openvpn/VPNPreferences.java b/src/se/leap/openvpn/VPNPreferences.java
new file mode 100644
index 00000000..81d854ce
--- /dev/null
+++ b/src/se/leap/openvpn/VPNPreferences.java
@@ -0,0 +1,138 @@
+package se.leap.openvpn;
+
+import java.util.List;
+import se.leap.leapclient.R;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+
+public class VPNPreferences extends PreferenceActivity {
+
+ private String mProfileUUID;
+ private VpnProfile mProfile;
+
+ public VPNPreferences() {
+ super();
+
+ }
+
+
+
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ };
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putString(getIntent().getStringExtra(getPackageName() + ".profileUUID"),mProfileUUID);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Intent intent = getIntent();
+
+
+ if(intent!=null) {
+ String profileUUID = intent.getStringExtra(getPackageName() + ".profileUUID");
+ if(profileUUID==null) {
+ Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ profileUUID = initialArguments.getString(getPackageName() + ".profileUUID");
+ }
+ if(profileUUID!=null){
+
+ mProfileUUID = profileUUID;
+ mProfile = ProfileManager.get(this,mProfileUUID);
+
+ }
+ }
+ // When a profile is deleted from a category fragment in hadset mod we need to finish
+ // this activity as well when returning
+ if (mProfile==null || mProfile.profileDleted) {
+ setResult(VPNProfileList.RESULT_VPN_DELETED);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ mProfileUUID = getIntent().getStringExtra(getPackageName() + ".profileUUID");
+ if(savedInstanceState!=null){
+ String savedUUID = savedInstanceState.getString(getPackageName() + ".profileUUID");
+ if(savedUUID!=null)
+ mProfileUUID=savedUUID;
+ }
+
+ mProfile = ProfileManager.get(this,mProfileUUID);
+ if(mProfile!=null) {
+ setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ loadHeadersFromResource(R.xml.vpn_headers, target);
+ for (Header header : target) {
+ if(header.fragmentArguments==null)
+ header.fragmentArguments = new Bundle();
+ header.fragmentArguments.putString(getPackageName() + ".profileUUID",mProfileUUID);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ setResult(RESULT_OK, getIntent());
+ super.onBackPressed();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId() == R.id.remove_vpn)
+ askProfileRemoval();
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ getMenuInflater().inflate(R.menu.vpnpreferences_menu, menu);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ private void askProfileRemoval() {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle("Confirm deletion");
+ dialog.setMessage(getString(R.string.remove_vpn_query, mProfile.mName));
+
+ dialog.setPositiveButton(android.R.string.yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ removeProfile(mProfile);
+ }
+
+ });
+ dialog.setNegativeButton(android.R.string.no,null);
+ dialog.create().show();
+ }
+
+ protected void removeProfile(VpnProfile profile) {
+ ProfileManager.getInstance(this).removeProfile(this,profile);
+ setResult(VPNProfileList.RESULT_VPN_DELETED);
+ finish();
+
+ }
+}
+
diff --git a/src/se/leap/openvpn/VPNProfileList.java b/src/se/leap/openvpn/VPNProfileList.java
new file mode 100644
index 00000000..a7132c9e
--- /dev/null
+++ b/src/se/leap/openvpn/VPNProfileList.java
@@ -0,0 +1,317 @@
+package se.leap.openvpn;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+import se.leap.leapclient.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.Html.ImageGetter;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class VPNProfileList extends ListFragment {
+
+ final static int RESULT_VPN_DELETED = Activity.RESULT_FIRST_USER;
+
+ private static final int MENU_ADD_PROFILE = Menu.FIRST;
+
+ private static final int START_VPN_CONFIG = 92;
+ private static final int SELECT_PROFILE = 43;
+ private static final int IMPORT_PROFILE = 231;
+
+ private static final int MENU_IMPORT_PROFILE = Menu.FIRST +1;
+
+ class VPNArrayAdapter extends ArrayAdapter<VpnProfile> {
+
+ public VPNArrayAdapter(Context context, int resource,
+ int textViewResourceId) {
+ super(context, resource, textViewResourceId);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v = super.getView(position, convertView, parent);
+
+ View titleview = v.findViewById(R.id.vpn_list_item_left);
+ titleview.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ VpnProfile profile =(VpnProfile) getListAdapter().getItem(position);
+ startVPN(profile);
+ }
+ });
+
+ View settingsview = v.findViewById(R.id.quickedit_settings);
+ settingsview.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ mEditProfile =(VpnProfile) getListAdapter().getItem(position);
+ editVPN(mEditProfile);
+
+ }
+ });
+
+ return v;
+ }
+ }
+
+
+
+
+
+
+
+
+ private ArrayAdapter<VpnProfile> mArrayadapter;
+
+ protected VpnProfile mEditProfile=null;
+
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
+ // Debug load JNI
+ //OpenVPN.foo();
+ }
+
+
+ class MiniImageGetter implements ImageGetter {
+
+
+ @Override
+ public Drawable getDrawable(String source) {
+ Drawable d=null;
+ if ("ic_menu_add".equals(source))
+ d = getActivity().getResources().getDrawable(android.R.drawable.ic_menu_add);
+ else if("ic_menu_archive".equals(source))
+ d = getActivity().getResources().getDrawable(R.drawable.ic_menu_archive);
+
+
+
+ if(d!=null) {
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ return d;
+ }else{
+ return null;
+ }
+ }
+ }
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.vpn_profile_list, container,false);
+
+ TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint);
+ TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint);
+
+
+
+ newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint),new MiniImageGetter(),null));
+ importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint),new MiniImageGetter(),null));
+
+
+
+ return v;
+
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ setListAdapter();
+ }
+
+ class VpnProfileNameComperator implements Comparator<VpnProfile> {
+
+ @Override
+ public int compare(VpnProfile lhs, VpnProfile rhs) {
+ return lhs.mName.compareTo(rhs.mName);
+ }
+
+ }
+
+ private void setListAdapter() {
+ mArrayadapter = new VPNArrayAdapter(getActivity(),R.layout.vpn_list_item,R.id.vpn_item_title);
+ Collection<VpnProfile> allvpn = getPM().getProfiles();
+
+ TreeSet<VpnProfile> sortedset = new TreeSet<VpnProfile>(new VpnProfileNameComperator());
+ sortedset.addAll(allvpn);
+ mArrayadapter.addAll(sortedset);
+
+ setListAdapter(mArrayadapter);
+ }
+
+
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile)
+ .setIcon(android.R.drawable.ic_menu_add)
+ .setAlphabeticShortcut('a')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
+ | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import)
+ .setIcon(R.drawable.ic_menu_archive)
+ .setAlphabeticShortcut('i')
+ .setTitleCondensed(getActivity().getString(R.string.menu_import_short))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
+ | MenuItem.SHOW_AS_ACTION_WITH_TEXT );
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == MENU_ADD_PROFILE) {
+ onAddProfileClicked();
+ return true;
+ } else if (itemId == MENU_IMPORT_PROFILE) {
+ startImportConfig();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void startImportConfig() {
+ Intent intent = new Intent(getActivity(),FileSelect.class);
+ intent.putExtra(FileSelect.NO_INLINE_SELECTION, true);
+ intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file);
+ startActivityForResult(intent, SELECT_PROFILE);
+ }
+
+
+
+
+
+ private void onAddProfileClicked() {
+ Context context = getActivity();
+ if (context != null) {
+ final EditText entry = new EditText(context);
+ entry.setSingleLine();
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(context);
+ dialog.setTitle(R.string.menu_add_profile);
+ dialog.setMessage(R.string.add_profile_name_prompt);
+ dialog.setView(entry);
+
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String name = entry.getText().toString();
+ if (getPM().getProfileByName(name)==null) {
+ VpnProfile profile = new VpnProfile(name);
+ addProfile(profile);
+ } else {
+ Toast.makeText(getActivity(), R.string.duplicate_profile_name, Toast.LENGTH_LONG).show();
+ }
+ }
+
+
+ });
+ dialog.setNegativeButton(android.R.string.cancel, null);
+ dialog.create().show();
+ }
+
+ }
+
+
+ private void addProfile(VpnProfile profile) {
+ getPM().addProfile(profile);
+ getPM().saveProfileList(getActivity());
+ getPM().saveProfile(getActivity(),profile);
+ mArrayadapter.add(profile);
+ }
+
+
+
+
+
+ private ProfileManager getPM() {
+ return ProfileManager.getInstance(getActivity());
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if(resultCode == RESULT_VPN_DELETED){
+ if(mArrayadapter != null && mEditProfile !=null)
+ mArrayadapter.remove(mEditProfile);
+ }
+
+ if(resultCode != Activity.RESULT_OK)
+ return;
+
+ if (requestCode == START_VPN_CONFIG) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ VpnProfile profile = ProfileManager.get(configuredVPN);
+ getPM().saveProfile(getActivity(), profile);
+ // Name could be modified, reset List adapter
+ setListAdapter();
+
+ } else if(requestCode== SELECT_PROFILE) {
+ String filedata = data.getStringExtra(FileSelect.RESULT_DATA);
+ Intent startImport = new Intent(getActivity(),ConfigConverter.class);
+ startImport.setAction(ConfigConverter.IMPORT_PROFILE);
+ Uri uri = new Uri.Builder().path(filedata).scheme("file").build();
+ startImport.setData(uri);
+ startActivityForResult(startImport, IMPORT_PROFILE);
+ } else if(requestCode == IMPORT_PROFILE) {
+ String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+ mArrayadapter.add(ProfileManager.get(profileUUID));
+ }
+
+ }
+
+
+ private void editVPN(VpnProfile profile) {
+
+ Intent vprefintent = new Intent(getActivity(),VPNPreferences.class)
+ .putExtra(getActivity().getPackageName() + ".profileUUID", profile.getUUID().toString());
+
+ startActivityForResult(vprefintent,START_VPN_CONFIG);
+ }
+
+ private void startVPN(VpnProfile profile) {
+
+ getPM().saveProfile(getActivity(), profile);
+
+ Intent intent = new Intent(getActivity(),LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+
+ getActivity().finish();
+ }
+}
diff --git a/src/se/leap/openvpn/VpnProfile.java b/src/se/leap/openvpn/VpnProfile.java
new file mode 100644
index 00000000..293a3d06
--- /dev/null
+++ b/src/se/leap/openvpn/VpnProfile.java
@@ -0,0 +1,746 @@
+package se.leap.openvpn;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.UUID;
+import java.util.Vector;
+
+import org.spongycastle.util.io.pem.PemObject;
+import org.spongycastle.util.io.pem.PemWriter;
+import se.leap.leapclient.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.preference.PreferenceManager;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+
+public class VpnProfile implements Serializable{
+ // Parcable
+ /**
+ *
+ */
+ private static final long serialVersionUID = 7085688938959334563L;
+ static final int TYPE_CERTIFICATES=0;
+ static final int TYPE_PKCS12=1;
+ static final int TYPE_KEYSTORE=2;
+ public static final int TYPE_USERPASS = 3;
+ public static final int TYPE_STATICKEYS = 4;
+ public static final int TYPE_USERPASS_CERTIFICATES = 5;
+ public static final int TYPE_USERPASS_PKCS12 = 6;
+ public static final int TYPE_USERPASS_KEYSTORE = 7;
+
+ // Don't change this, not all parts of the program use this constant
+ public static final String EXTRA_PROFILEUUID = "se.leap.openvpn.profileUUID";
+ public static final String INLINE_TAG = "[[INLINE]]";
+ private static final String OVPNCONFIGFILE = "android.conf";
+
+ protected transient String mTransientPW=null;
+ protected transient String mTransientPCKS12PW=null;
+ private transient PrivateKey mPrivateKey;
+ protected boolean profileDleted=false;
+
+
+ public static String DEFAULT_DNS1="131.234.137.23";
+ public static String DEFAULT_DNS2="131.234.137.24";
+
+ // Public attributes, since I got mad with getter/setter
+ // set members to default values
+ private UUID mUuid;
+ public int mAuthenticationType = TYPE_KEYSTORE ;
+ public String mName;
+ public String mAlias;
+ public String mClientCertFilename;
+ public String mTLSAuthDirection="";
+ public String mTLSAuthFilename;
+ public String mClientKeyFilename;
+ public String mCaFilename;
+ public boolean mUseLzo=true;
+ public String mServerPort= "1194" ;
+ public boolean mUseUdp = true;
+ public String mPKCS12Filename;
+ public String mPKCS12Password;
+ public boolean mUseTLSAuth = false;
+ public String mServerName = "openvpn.blinkt.de" ;
+ public String mDNS1=DEFAULT_DNS1;
+ public String mDNS2=DEFAULT_DNS2;
+ public String mIPv4Address;
+ public String mIPv6Address;
+ public boolean mOverrideDNS=false;
+ public String mSearchDomain="blinkt.de";
+ public boolean mUseDefaultRoute=true;
+ public boolean mUsePull=true;
+ public String mCustomRoutes;
+ public boolean mCheckRemoteCN=false;
+ public boolean mExpectTLSCert=true;
+ public String mRemoteCN="";
+ public String mPassword="";
+ public String mUsername="";
+ public boolean mRoutenopull=false;
+ public boolean mUseRandomHostname=false;
+ public boolean mUseFloat=false;
+ public boolean mUseCustomConfig=false;
+ public String mCustomConfigOptions="";
+ public String mVerb="1";
+ public String mCipher="";
+ public boolean mNobind=false;
+ public boolean mUseDefaultRoutev6=true;
+ public String mCustomRoutesv6="";
+ public String mKeyPassword="";
+ public boolean mPersistTun = false;
+ public String mConnectRetryMax="5";
+ public String mConnectRetry="5";
+ public boolean mUserEditable=true;
+
+ static final String MINIVPN = "miniopenvpn";
+
+
+
+
+
+
+ public void clearDefaults() {
+ mServerName="unkown";
+ mUsePull=false;
+ mUseLzo=false;
+ mUseDefaultRoute=false;
+ mUseDefaultRoutev6=false;
+ mExpectTLSCert=false;
+ mPersistTun = false;
+ }
+
+
+ public static String openVpnEscape(String unescaped) {
+ if(unescaped==null)
+ return null;
+ String escapedString = unescaped.replace("\\", "\\\\");
+ escapedString = escapedString.replace("\"","\\\"");
+ escapedString = escapedString.replace("\n","\\n");
+
+ if (escapedString.equals(unescaped) && !escapedString.contains(" ") && !escapedString.contains("#"))
+ return unescaped;
+ else
+ return '"' + escapedString + '"';
+ }
+
+
+ static final String OVPNCONFIGCA = "android-ca.pem";
+ static final String OVPNCONFIGUSERCERT = "android-user.pem";
+
+
+ public VpnProfile(String name) {
+ mUuid = UUID.randomUUID();
+ mName = name;
+ }
+
+ public UUID getUUID() {
+ return mUuid;
+
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+
+ public String getConfigFile(Context context)
+ {
+
+ File cacheDir= context.getCacheDir();
+ String cfg="";
+
+ // Enable managment interface
+ cfg += "# Enables connection to GUI\n";
+ cfg += "management ";
+
+ cfg +=cacheDir.getAbsolutePath() + "/" + "mgmtsocket";
+ cfg += " unix\n";
+ cfg += "management-client\n";
+ // Not needed, see updated man page in 2.3
+ //cfg += "management-signal\n";
+ cfg += "management-query-passwords\n";
+ cfg += "management-hold\n\n";
+
+ /* tmp-dir patched out :)
+ cfg+="# /tmp does not exist on Android\n";
+ cfg+="tmp-dir ";
+ cfg+=cacheDir.getAbsolutePath();
+ cfg+="\n\n"; */
+
+ cfg+="# Log window is better readable this way\n";
+ cfg+="suppress-timestamps\n";
+
+
+
+ boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS);
+
+ if(useTLSClient && mUsePull)
+ cfg+="client\n";
+ else if (mUsePull)
+ cfg+="pull\n";
+ else if(useTLSClient)
+ cfg+="tls-client\n";
+
+
+ cfg+="verb " + mVerb + "\n";
+
+ if(mConnectRetryMax ==null) {
+ mConnectRetryMax="5";
+ }
+
+ if(!mConnectRetryMax.equals("-1"))
+ cfg+="connect-retry-max " + mConnectRetryMax+ "\n";
+
+ if(mConnectRetry==null)
+ mConnectRetry="5";
+
+
+ cfg+="connect-retry " + mConnectRetry + "\n";
+
+ cfg+="resolv-retry 60\n";
+
+
+
+ // We cannot use anything else than tun
+ cfg+="dev tun\n";
+
+ // Server Address
+ cfg+="remote ";
+ cfg+=mServerName;
+ cfg+=" ";
+ cfg+=mServerPort;
+ if(mUseUdp)
+ cfg+=" udp\n";
+ else
+ cfg+=" tcp-client\n";
+
+
+
+
+ switch(mAuthenticationType) {
+ case VpnProfile.TYPE_USERPASS_CERTIFICATES:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_CERTIFICATES:
+ // Ca
+ cfg+=insertFileData("ca",mCaFilename);
+
+ // Client Cert + Key
+ cfg+=insertFileData("key",mClientKeyFilename);
+ cfg+=insertFileData("cert",mClientCertFilename);
+
+ break;
+ case VpnProfile.TYPE_USERPASS_PKCS12:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_PKCS12:
+ cfg+=insertFileData("pkcs12",mPKCS12Filename);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_KEYSTORE:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_KEYSTORE:
+ cfg+="ca " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGCA + "\n";
+ cfg+="cert " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGUSERCERT + "\n";
+ cfg+="management-external-key\n";
+
+ break;
+ case VpnProfile.TYPE_USERPASS:
+ cfg+="auth-user-pass\n";
+ cfg+=insertFileData("ca",mCaFilename);
+ }
+
+ if(mUseLzo) {
+ cfg+="comp-lzo\n";
+ }
+
+ if(mUseTLSAuth) {
+ if(mAuthenticationType==TYPE_STATICKEYS)
+ cfg+=insertFileData("secret",mTLSAuthFilename);
+ else
+ cfg+=insertFileData("tls-auth",mTLSAuthFilename);
+
+ if(nonNull(mTLSAuthDirection)) {
+ cfg+= "key-direction ";
+ cfg+= mTLSAuthDirection;
+ cfg+="\n";
+ }
+
+ }
+
+ if(!mUsePull ) {
+ if(nonNull(mIPv4Address))
+ cfg +="ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n";
+
+ if(nonNull(mIPv6Address))
+ cfg +="ifconfig-ipv6 " + mIPv6Address + "\n";
+ }
+
+ if(mUsePull && mRoutenopull)
+ cfg += "route-nopull\n";
+
+ String routes = "";
+ int numroutes=0;
+ if(mUseDefaultRoute)
+ routes += "route 0.0.0.0 0.0.0.0\n";
+ else
+ for(String route:getCustomRoutes()) {
+ routes += "route " + route + "\n";
+ numroutes++;
+ }
+
+
+ if(mUseDefaultRoutev6)
+ cfg += "route-ipv6 ::/0\n";
+ else
+ for(String route:getCustomRoutesv6()) {
+ routes += "route-ipv6 " + route + "\n";
+ numroutes++;
+ }
+
+ // Round number to next 100
+ if(numroutes> 90) {
+ numroutes = ((numroutes / 100)+1) * 100;
+ cfg+="# Alot of routes are set, increase max-routes\n";
+ cfg+="max-routes " + numroutes + "\n";
+ }
+ cfg+=routes;
+
+ if(mOverrideDNS || !mUsePull) {
+ if(nonNull(mDNS1))
+ cfg+="dhcp-option DNS " + mDNS1 + "\n";
+ if(nonNull(mDNS2))
+ cfg+="dhcp-option DNS " + mDNS2 + "\n";
+ if(nonNull(mSearchDomain))
+ cfg+="dhcp-option DOMAIN " + mSearchDomain + "\n";
+
+ }
+
+ if(mNobind)
+ cfg+="nobind\n";
+
+
+
+ // Authentication
+ if(mCheckRemoteCN) {
+ if(mRemoteCN == null || mRemoteCN.equals("") )
+ cfg+="tls-remote " + mServerName + "\n";
+ else
+ cfg += "tls-remote " + openVpnEscape(mRemoteCN) + "\n";
+ }
+ if(mExpectTLSCert)
+ cfg += "remote-cert-tls server\n";
+
+
+ if(nonNull(mCipher)){
+ cfg += "cipher " + mCipher + "\n";
+ }
+
+
+ // Obscure Settings dialog
+ if(mUseRandomHostname)
+ cfg += "#my favorite options :)\nremote-random-hostname\n";
+
+ if(mUseFloat)
+ cfg+= "float\n";
+
+ if(mPersistTun) {
+ cfg+= "persist-tun\n";
+ cfg+= "# persist-tun also sets persist-remote-ip to avoid DNS resolve problem\n";
+ cfg+= "persist-remote-ip\n";
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true);
+ if(usesystemproxy) {
+ cfg+= "# Use system proxy setting\n";
+ cfg+= "management-query-proxy\n";
+ }
+
+
+ if(mUseCustomConfig) {
+ cfg += "# Custom configuration options\n";
+ cfg += "# You are on your on own here :)\n";
+ cfg += mCustomConfigOptions;
+ cfg += "\n";
+
+ }
+
+
+
+ return cfg;
+ }
+
+ //! Put inline data inline and other data as normal escaped filename
+ private String insertFileData(String cfgentry, String filedata) {
+ if(filedata==null) {
+ return String.format("%s %s\n",cfgentry,"missing");
+ }else if(filedata.startsWith(VpnProfile.INLINE_TAG)){
+ String datawoheader = filedata.substring(VpnProfile.INLINE_TAG.length());
+ return String.format("<%s>\n%s\n</%s>\n",cfgentry,datawoheader,cfgentry);
+ } else {
+ return String.format("%s %s\n",cfgentry,openVpnEscape(filedata));
+ }
+ }
+
+ private boolean nonNull(String val) {
+ if(val == null || val.equals(""))
+ return false;
+ else
+ return true;
+ }
+
+ private Collection<String> getCustomRoutes() {
+ Vector<String> cidrRoutes=new Vector<String>();
+ if(mCustomRoutes==null) {
+ // No routes set, return empty vector
+ return cidrRoutes;
+ }
+ for(String route:mCustomRoutes.split("[\n \t]")) {
+ if(!route.equals("")) {
+ String cidrroute = cidrToIPAndNetmask(route);
+ if(cidrRoutes == null)
+ return null;
+
+ cidrRoutes.add(cidrroute);
+ }
+ }
+
+ return cidrRoutes;
+ }
+
+ private Collection<String> getCustomRoutesv6() {
+ Vector<String> cidrRoutes=new Vector<String>();
+ if(mCustomRoutesv6==null) {
+ // No routes set, return empty vector
+ return cidrRoutes;
+ }
+ for(String route:mCustomRoutesv6.split("[\n \t]")) {
+ if(!route.equals("")) {
+ cidrRoutes.add(route);
+ }
+ }
+
+ return cidrRoutes;
+ }
+
+
+
+ private String cidrToIPAndNetmask(String route) {
+ String[] parts = route.split("/");
+
+ // No /xx, assume /32 as netmask
+ if (parts.length ==1)
+ parts = (route + "/32").split("/");
+
+ if (parts.length!=2)
+ return null;
+ int len;
+ try {
+ len = Integer.parseInt(parts[1]);
+ } catch(NumberFormatException ne) {
+ return null;
+ }
+ if (len <0 || len >32)
+ return null;
+
+
+ long nm = 0xffffffffl;
+ nm = (nm << (32-len)) & 0xffffffffl;
+
+ String netmask =String.format("%d.%d.%d.%d", (nm & 0xff000000) >> 24,(nm & 0xff0000) >> 16, (nm & 0xff00) >> 8 ,nm & 0xff );
+ return parts[0] + " " + netmask;
+ }
+
+
+
+ private String[] buildOpenvpnArgv(File cacheDir)
+ {
+ Vector<String> args = new Vector<String>();
+
+ // Add fixed paramenters
+ //args.add("/data/data/se.leap.openvpn/lib/openvpn");
+ args.add(cacheDir.getAbsolutePath() +"/" + VpnProfile.MINIVPN);
+
+ args.add("--config");
+ args.add(cacheDir.getAbsolutePath() + "/" + OVPNCONFIGFILE);
+ // Silences script security warning
+
+ args.add("script-security");
+ args.add("0");
+
+
+ return (String[]) args.toArray(new String[args.size()]);
+ }
+
+ public Intent prepareIntent(Context context) {
+ String prefix = context.getPackageName();
+
+ Intent intent = new Intent(context,OpenVpnService.class);
+
+ if(mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
+ if(!saveCertificates(context))
+ return null;
+ }
+
+ intent.putExtra(prefix + ".ARGV" , buildOpenvpnArgv(context.getCacheDir()));
+ intent.putExtra(prefix + ".profileUUID", mUuid.toString());
+
+ ApplicationInfo info = context.getApplicationInfo();
+ intent.putExtra(prefix +".nativelib",info.nativeLibraryDir);
+
+ try {
+ FileWriter cfg = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE);
+ cfg.write(getConfigFile(context));
+ cfg.flush();
+ cfg.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return intent;
+ }
+
+ private boolean saveCertificates(Context context) {
+ PrivateKey privateKey = null;
+ X509Certificate[] cachain=null;
+ try {
+ privateKey = KeyChain.getPrivateKey(context,mAlias);
+ mPrivateKey = privateKey;
+
+ cachain = KeyChain.getCertificateChain(context, mAlias);
+ if(cachain.length <= 1 && !nonNull(mCaFilename))
+ OpenVPN.logMessage(0, "", context.getString(R.string.keychain_nocacert));
+
+ for(X509Certificate cert:cachain) {
+ OpenVPN.logInfo(R.string.cert_from_keystore,cert.getSubjectDN());
+ }
+
+
+
+
+ if(nonNull(mCaFilename)) {
+ try {
+ Certificate cacert = getCacertFromFile();
+ X509Certificate[] newcachain = new X509Certificate[cachain.length+1];
+ for(int i=0;i<cachain.length;i++)
+ newcachain[i]=cachain[i];
+
+ newcachain[cachain.length-1]=(X509Certificate) cacert;
+
+ } catch (Exception e) {
+ OpenVPN.logError("Could not read CA certificate" + e.getLocalizedMessage());
+ }
+ }
+
+
+ FileWriter fout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGCA);
+ PemWriter pw = new PemWriter(fout);
+ for(X509Certificate cert:cachain) {
+ pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
+ }
+
+ pw.close();
+
+
+ if(cachain.length>= 1){
+ X509Certificate usercert = cachain[0];
+
+ FileWriter userout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGUSERCERT);
+
+ PemWriter upw = new PemWriter(userout);
+ upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded()));
+ upw.close();
+
+ }
+
+ return true;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (KeyChainException e) {
+ OpenVPN.logMessage(0,"",context.getString(R.string.keychain_access));
+ }
+ return false;
+ }
+ private Certificate getCacertFromFile() throws FileNotFoundException, CertificateException {
+ CertificateFactory certFact = CertificateFactory.getInstance("X.509");
+
+ InputStream inStream;
+
+ if(mCaFilename.startsWith(INLINE_TAG))
+ inStream = new ByteArrayInputStream(mCaFilename.replace(INLINE_TAG,"").getBytes());
+ else
+ inStream = new FileInputStream(mCaFilename);
+
+ return certFact.generateCertificate(inStream);
+ }
+
+
+ //! Return an error if somethign is wrong
+ public int checkProfile(Context context) {
+ if(mAuthenticationType==TYPE_KEYSTORE || mAuthenticationType==TYPE_USERPASS_KEYSTORE) {
+ if(mAlias==null)
+ return R.string.no_keystore_cert_selected;
+ }
+
+ if(!mUsePull) {
+ if(mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null)
+ return R.string.ipv4_format_error;
+ }
+ if(isUserPWAuth() && !nonNull(mUsername)) {
+ return R.string.error_empty_username;
+ }
+ if(!mUseDefaultRoute && getCustomRoutes()==null)
+ return R.string.custom_route_format_error;
+
+ // Everything okay
+ return R.string.no_error_found;
+
+ }
+
+ //! Openvpn asks for a "Private Key", this should be pkcs12 key
+ //
+ public String getPasswordPrivateKey() {
+ if(mTransientPCKS12PW!=null) {
+ String pwcopy = mTransientPCKS12PW;
+ mTransientPCKS12PW=null;
+ return pwcopy;
+ }
+ switch (mAuthenticationType) {
+ case TYPE_PKCS12:
+ case TYPE_USERPASS_PKCS12:
+ return mPKCS12Password;
+
+ case TYPE_CERTIFICATES:
+ case TYPE_USERPASS_CERTIFICATES:
+ return mKeyPassword;
+
+ case TYPE_USERPASS:
+ case TYPE_STATICKEYS:
+ default:
+ return null;
+ }
+ }
+ private boolean isUserPWAuth() {
+ switch(mAuthenticationType) {
+ case TYPE_USERPASS:
+ case TYPE_USERPASS_CERTIFICATES:
+ case TYPE_USERPASS_KEYSTORE:
+ case TYPE_USERPASS_PKCS12:
+ return true;
+ default:
+ return false;
+
+ }
+ }
+
+
+ public boolean requireTLSKeyPassword() {
+ if(!nonNull(mClientKeyFilename))
+ return false;
+
+ String data = "";
+ if(mClientKeyFilename.startsWith(INLINE_TAG))
+ data = mClientKeyFilename;
+ else {
+ char[] buf = new char[2048];
+ FileReader fr;
+ try {
+ fr = new FileReader(mClientKeyFilename);
+ int len = fr.read(buf);
+ while(len > 0 ) {
+ data += new String(buf,0,len);
+ len = fr.read(buf);
+ }
+ fr.close();
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+
+ }
+
+ if(data.contains("Proc-Type: 4,ENCRYPTED"))
+ return true;
+ else if(data.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----"))
+ return true;
+ else
+ return false;
+ }
+
+ public int needUserPWInput() {
+ if((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12)&&
+ (mPKCS12Password == null || mPKCS12Password.equals(""))) {
+ if(mTransientPCKS12PW==null)
+ return R.string.pkcs12_file_encryption_key;
+ }
+
+ if(mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) {
+ if(requireTLSKeyPassword() && !nonNull(mKeyPassword))
+ if(mTransientPCKS12PW==null) {
+ return R.string.private_key_password;
+ }
+ }
+
+ if(isUserPWAuth() && (mPassword.equals("") || mPassword == null)) {
+ if(mTransientPW==null)
+ return R.string.password;
+
+ }
+ return 0;
+ }
+
+ public String getPasswordAuth() {
+ if(mTransientPW!=null) {
+ String pwcopy = mTransientPW;
+ mTransientPW=null;
+ return pwcopy;
+ } else {
+ return mPassword;
+ }
+ }
+
+
+ // Used by the Array Adapter
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+
+ public String getUUIDString() {
+ return mUuid.toString();
+ }
+
+
+ public PrivateKey getKeystoreKey() {
+ return mPrivateKey;
+ }
+
+
+
+}
+
+
+
+