diff options
author | Tobias Brunner <tobias@strongswan.org> | 2017-01-20 11:55:48 +0100 |
---|---|---|
committer | Tobias Brunner <tobias@strongswan.org> | 2017-01-20 11:55:48 +0100 |
commit | 343a5e9f26264623032ef65c2149a1678b0c4f87 (patch) | |
tree | 6fb6890750a7c0f44739dd1d3c9a994e755a5c26 /src | |
parent | a4c7778086c969cdd134730cbbb7ed307814d92b (diff) | |
parent | 7b73cf4aa9057170752134b8267dec70eb36a3ff (diff) | |
download | strongswan-343a5e9f26264623032ef65c2149a1678b0c4f87.tar.bz2 strongswan-343a5e9f26264623032ef65c2149a1678b0c4f87.tar.xz |
Merge branch 'android-import'
Adds a VPN profile import feature.
Diffstat (limited to 'src')
18 files changed, 1298 insertions, 51 deletions
diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle index 450a12fbd..af1737299 100644 --- a/src/frontends/android/app/build.gradle +++ b/src/frontends/android/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.strongswan.android" minSdkVersion 15 targetSdkVersion 22 - versionCode 36 - versionName "1.7.2" + versionCode 40 + versionName "1.8.0" } sourceSets.main { diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index da465ba74..bc2de9af7 100644 --- a/src/frontends/android/app/src/main/AndroidManifest.xml +++ b/src/frontends/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:name=".logic.StrongSwanApplication" @@ -64,6 +65,36 @@ </intent-filter> </activity> <activity + android:name=".ui.VpnProfileImportActivity" + android:label="@string/profile_import" + android:taskAffinity="" + android:excludeFromRecents="true" > + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:scheme="file" /> + <data android:scheme="content" /> + <data android:mimeType="application/vnd.strongswan.profile" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:scheme="file" /> + <data android:scheme="content" /> + <data android:host="*" /> + <data android:pathPattern=".*\\..*\\..*\\..*\\.sswan" /> + <data android:pathPattern=".*\\..*\\..*\\.sswan" /> + <data android:pathPattern=".*\\..*\\.sswan" /> + <data android:pathPattern=".*\\.sswan" /> + </intent-filter> + </activity> + <activity android:name=".ui.TrustedCertificateImportActivity" android:label="@string/import_certificate" android:theme="@style/AlertDialogTheme" > diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java index 8a9d319b5..54bdfcbdd 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java @@ -18,6 +18,8 @@ package org.strongswan.android.data; +import java.util.UUID; + public class VpnProfile implements Cloneable { /* While storing this as EnumSet would be nicer this simplifies storing it in a database */ @@ -28,8 +30,14 @@ public class VpnProfile implements Cloneable private String mRemoteId, mLocalId; private Integer mMTU, mPort, mSplitTunneling; private VpnType mVpnType; + private UUID mUUID; private long mId = -1; + public VpnProfile() + { + this.mUUID = UUID.randomUUID(); + } + public long getId() { return mId; @@ -40,6 +48,16 @@ public class VpnProfile implements Cloneable this.mId = id; } + public void setUUID(UUID uuid) + { + this.mUUID = uuid; + } + + public UUID getUUID() + { + return mUUID; + } + public String getName() { return mName; @@ -171,7 +189,12 @@ public class VpnProfile implements Cloneable { if (o != null && o instanceof VpnProfile) { - return this.mId == ((VpnProfile)o).getId(); + VpnProfile other = (VpnProfile)o; + if (this.mUUID != null && other.getUUID() != null) + { + return this.mUUID.equals(other.getUUID()); + } + return this.mId == other.getId(); } return false; } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java index 17026536b..1c509a302 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java @@ -19,6 +19,7 @@ package org.strongswan.android.data; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import android.content.ContentValues; import android.content.Context; @@ -33,6 +34,7 @@ public class VpnProfileDataSource { private static final String TAG = VpnProfileDataSource.class.getSimpleName(); public static final String KEY_ID = "_id"; + public static final String KEY_UUID = "_uuid"; public static final String KEY_NAME = "name"; public static final String KEY_GATEWAY = "gateway"; public static final String KEY_VPN_TYPE = "vpn_type"; @@ -53,11 +55,12 @@ public class VpnProfileDataSource private static final String DATABASE_NAME = "strongswan.db"; private static final String TABLE_VPNPROFILE = "vpnprofile"; - private static final int DATABASE_VERSION = 8; + private static final int DATABASE_VERSION = 9; public static final String DATABASE_CREATE = "CREATE TABLE " + TABLE_VPNPROFILE + " (" + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + KEY_UUID + " TEXT UNIQUE," + KEY_NAME + " TEXT NOT NULL," + KEY_GATEWAY + " TEXT NOT NULL," + KEY_VPN_TYPE + " TEXT NOT NULL," + @@ -73,6 +76,7 @@ public class VpnProfileDataSource ");"; private static final String[] ALL_COLUMNS = new String[] { KEY_ID, + KEY_UUID, KEY_NAME, KEY_GATEWAY, KEY_VPN_TYPE, @@ -141,6 +145,12 @@ public class VpnProfileDataSource db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_REMOTE_ID + " TEXT;"); } + if (oldVersion < 9) + { + db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_UUID + + " TEXT;"); + updateColumns(db); + } } private void updateColumns(SQLiteDatabase db) @@ -262,6 +272,24 @@ public class VpnProfileDataSource } /** + * Get a single VPN profile from the database by its UUID. + * @param uuid the UUID of the VPN profile + * @return the profile or null, if not found + */ + public VpnProfile getVpnProfile(UUID uuid) + { + VpnProfile profile = null; + Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, + KEY_UUID + "='" + uuid.toString() + "'", null, null, null, null); + if (cursor.moveToFirst()) + { + profile = VpnProfileFromCursor(cursor); + } + cursor.close(); + return profile; + } + + /** * Get a list of all VPN profiles stored in the database. * @return list of VPN profiles */ @@ -285,6 +313,7 @@ public class VpnProfileDataSource { VpnProfile profile = new VpnProfile(); profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID))); + profile.setUUID(getUUID(cursor, cursor.getColumnIndex(KEY_UUID))); profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME))); profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY))); profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndex(KEY_VPN_TYPE)))); @@ -303,6 +332,7 @@ public class VpnProfileDataSource private ContentValues ContentValuesFromVpnProfile(VpnProfile profile) { ContentValues values = new ContentValues(); + values.put(KEY_UUID, profile.getUUID() != null ? profile.getUUID().toString() : null); values.put(KEY_NAME, profile.getName()); values.put(KEY_GATEWAY, profile.getGateway()); values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier()); @@ -322,4 +352,16 @@ public class VpnProfileDataSource { return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex); } + + private UUID getUUID(Cursor cursor, int columnIndex) + { + try + { + return cursor.isNull(columnIndex) ? null : UUID.fromString(cursor.getString(columnIndex)); + } + catch (Exception e) + { + return null; + } + } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java index 30fb101be..bf64370cf 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.security.KeyChain; import android.security.KeyChainAliasCallback; import android.security.KeyChainException; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDialogFragment; @@ -63,14 +64,14 @@ import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.security.TrustedCertificateEntry; import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter; import org.strongswan.android.ui.widget.TextInputLayoutHelper; +import org.strongswan.android.utils.Constants; import java.security.cert.X509Certificate; +import java.util.UUID; public class VpnProfileDetailActivity extends AppCompatActivity { private static final int SELECT_TRUSTED_CERTIFICATE = 0; - private static final int MTU_MIN = 1280; - private static final int MTU_MAX = 1500; private VpnProfileDataSource mDataSource; private Long mId; @@ -453,6 +454,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity if (mProfile != null) { updateProfileData(); + if (mProfile.getUUID() == null) + { + mProfile.setUUID(UUID.randomUUID()); + } mDataSource.updateVpnProfile(mProfile); } else @@ -461,6 +466,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity updateProfileData(); mDataSource.insertProfile(mProfile); } + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId()); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId())); finish(); } @@ -496,9 +505,9 @@ public class VpnProfileDetailActivity extends AppCompatActivity showCertificateAlert(); valid = false; } - if (!validateInteger(mMTU, MTU_MIN, MTU_MAX)) + if (!validateInteger(mMTU, Constants.MTU_MIN, Constants.MTU_MAX)) { - mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX)); + mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), Constants.MTU_MIN, Constants.MTU_MAX)); valid = false; } if (!validateInteger(mPort, 1, 65535)) @@ -567,8 +576,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity mRemoteId.setText(mProfile.getRemoteId()); mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null); mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null); - mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0 : false); - mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0 : false); + mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0); + mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0); useralias = mProfile.getUserCertificateAlias(); local_id = mProfile.getLocalId(); alias = mProfile.getCertificateAlias(); @@ -686,11 +695,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); } - catch (KeyChainException e) - { - e.printStackTrace(); - } - catch (InterruptedException e) + catch (KeyChainException | InterruptedException e) { e.printStackTrace(); } @@ -722,11 +727,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity { chain = KeyChain.getCertificateChain(mContext, mAlias); } - catch (KeyChainException e) - { - e.printStackTrace(); - } - catch (InterruptedException e) + catch (KeyChainException | InterruptedException e) { e.printStackTrace(); } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java new file mode 100644 index 000000000..1b6b6e896 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java @@ -0,0 +1,811 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.ui; + +import android.app.Activity; +import android.app.LoaderManager; +import android.app.ProgressDialog; +import android.content.AsyncTaskLoader; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.Loader; +import android.net.Uri; +import android.os.Bundle; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.security.KeyChainException; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.util.Base64; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.json.JSONException; +import org.json.JSONObject; +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnType; +import org.strongswan.android.data.VpnType.VpnTypeFeature; +import org.strongswan.android.logic.TrustedCertificateManager; +import org.strongswan.android.security.TrustedCertificateEntry; +import org.strongswan.android.ui.widget.TextInputLayoutHelper; +import org.strongswan.android.utils.Constants; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.UUID; + +import javax.net.ssl.SSLHandshakeException; + +public class VpnProfileImportActivity extends AppCompatActivity +{ + private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED"; + private static final int INSTALL_PKCS12 = 0; + private static final int PROFILE_LOADER = 0; + private static final int USER_CERT_LOADER = 1; + + private VpnProfileDataSource mDataSource; + private ParsedVpnProfile mProfile; + private VpnProfile mExisting; + private TrustedCertificateEntry mCertEntry; + private TrustedCertificateEntry mUserCertEntry; + private String mUserCertLoading; + private boolean mHideImport; + private ProgressDialog mProgress; + private TextView mExistsWarning; + private ViewGroup mBasicDataGroup; + private TextView mName; + private TextView mGateway; + private TextView mSelectVpnType; + private ViewGroup mUsernamePassword; + private EditText mUsername; + private TextInputLayoutHelper mUsernameWrap; + private EditText mPassword; + private ViewGroup mUserCertificate; + private RelativeLayout mSelectUserCert; + private Button mImportUserCert; + private ViewGroup mRemoteCertificate; + private RelativeLayout mRemoteCert; + + private LoaderManager.LoaderCallbacks<ProfileLoadResult> mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks<ProfileLoadResult>() + { + @Override + public Loader<ProfileLoadResult> onCreateLoader(int id, Bundle args) + { + return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData()); + } + + @Override + public void onLoadFinished(Loader<ProfileLoadResult> loader, ProfileLoadResult data) + { + handleProfile(data); + } + + @Override + public void onLoaderReset(Loader<ProfileLoadResult> loader) + { + + } + }; + + private LoaderManager.LoaderCallbacks<TrustedCertificateEntry> mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks<TrustedCertificateEntry>() + { + @Override + public Loader<TrustedCertificateEntry> onCreateLoader(int id, Bundle args) + { + return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading); + } + + @Override + public void onLoadFinished(Loader<TrustedCertificateEntry> loader, TrustedCertificateEntry data) + { + handleUserCertificate(data); + } + + @Override + public void onLoaderReset(Loader<TrustedCertificateEntry> loader) + { + + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + mDataSource = new VpnProfileDataSource(this); + mDataSource.open(); + + setContentView(R.layout.profile_import_view); + + mExistsWarning = (TextView)findViewById(R.id.exists_warning); + mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group); + mName = (TextView)findViewById(R.id.name); + mGateway = (TextView)findViewById(R.id.gateway); + mSelectVpnType = (TextView)findViewById(R.id.vpn_type); + + mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group); + mUsername = (EditText)findViewById(R.id.username); + mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap); + mPassword = (EditText)findViewById(R.id.password); + + mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group); + mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate); + mImportUserCert = (Button)findViewById(R.id.import_user_certificate); + + mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group); + mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate); + + mExistsWarning.setVisibility(View.GONE); + mBasicDataGroup.setVisibility(View.GONE); + mUsernamePassword.setVisibility(View.GONE); + mUserCertificate.setVisibility(View.GONE); + mRemoteCertificate.setVisibility(View.GONE); + + mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener()); + mImportUserCert.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) + { + Intent intent = KeyChain.createInstallIntent(); + intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName())); + intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12); + startActivityForResult(intent, INSTALL_PKCS12); + } + }); + + Intent intent = getIntent(); + String action = intent.getAction(); + if (Intent.ACTION_VIEW.equals(action)) + { + mProgress = ProgressDialog.show(this, null, getString(R.string.loading), + true, true, new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) + { + finish(); + } + }); + + getLoaderManager().initLoader(PROFILE_LOADER, null, mProfileLoaderCallbacks); + } + + if (savedInstanceState != null) + { + mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE); + if (mUserCertLoading != null) + { + getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks); + } + mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED)); + } + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + mDataSource.close(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + if (mUserCertEntry != null) + { + outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias()); + } + outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.profile_import, menu); + if (mHideImport) + { + MenuItem item = menu.findItem(R.id.menu_accept); + item.setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case android.R.id.home: + finish(); + return true; + case R.id.menu_accept: + saveProfile(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) + { + case INSTALL_PKCS12: + if (resultCode == Activity.RESULT_OK) + { /* no need to import twice */ + mImportUserCert.setEnabled(false); + mSelectUserCert.performClick(); + } + } + } + + public void handleProfile(ProfileLoadResult data) + { + mProgress.dismiss(); + + mProfile = null; + if (data != null && data.ThrownException == null) + { + try + { + JSONObject obj = new JSONObject(data.Profile); + mProfile = parseProfile(obj); + } + catch (JSONException e) + { + mExistsWarning.setVisibility(View.VISIBLE); + mExistsWarning.setText(e.getLocalizedMessage()); + mHideImport = true; + invalidateOptionsMenu(); + return; + } + } + if (mProfile == null) + { + String error = null; + if (data.ThrownException != null) + { + try + { + throw data.ThrownException; + } + catch (FileNotFoundException e) + { + error = getString(R.string.profile_import_failed_not_found); + } + catch (UnknownHostException e) + { + error = getString(R.string.profile_import_failed_host); + } + catch (SSLHandshakeException e) + { + error = getString(R.string.profile_import_failed_tls); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + if (error != null) + { + Toast.makeText(this, getString(R.string.profile_import_failed_detail, error), Toast.LENGTH_LONG).show(); + } + else + { + Toast.makeText(this, R.string.profile_import_failed, Toast.LENGTH_LONG).show(); + } + finish(); + return; + } + mExisting = mDataSource.getVpnProfile(mProfile.getUUID()); + mExistsWarning.setVisibility(mExisting != null ? View.VISIBLE : View.GONE); + + mBasicDataGroup.setVisibility(View.VISIBLE); + mName.setText(mProfile.getName()); + mGateway.setText(mProfile.getGateway()); + mSelectVpnType.setText(getResources().getStringArray(R.array.vpn_types)[mProfile.getVpnType().ordinal()]); + + mUsernamePassword.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE); + if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS)) + { + mUsername.setText(mProfile.getUsername()); + if (mProfile.getUsername() != null && !mProfile.getUsername().isEmpty()) + { + mUsername.setEnabled(false); + } + } + + mUserCertificate.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE); + mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE); + mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE); + + updateUserCertView(); + + if (mProfile.Certificate != null) + { + try + { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(mProfile.Certificate)); + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + String alias = store.getCertificateAlias(certificate); + mCertEntry = new TrustedCertificateEntry(alias, certificate); + ((TextView)mRemoteCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary()); + ((TextView)mRemoteCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary()); + } + catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) + { + e.printStackTrace(); + mRemoteCertificate.setVisibility(View.GONE); + } + } + } + + private void handleUserCertificate(TrustedCertificateEntry data) + { + mUserCertEntry = data; + mUserCertLoading = null; + updateUserCertView(); + } + + private void updateUserCertView() + { + if (mUserCertLoading != null) + { + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading); + ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading); + } + else if (mUserCertEntry != null) + { /* clear any errors and set the new data */ + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null); + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias()); + ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString()); + } + else + { + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label); + ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate); + } + } + + private ParsedVpnProfile parseProfile(JSONObject obj) throws JSONException + { + UUID uuid; + try + { + uuid = UUID.fromString(obj.getString("uuid")); + } + catch (IllegalArgumentException e) + { + e.printStackTrace(); + return null; + } + ParsedVpnProfile profile = new ParsedVpnProfile(); + + profile.setUUID(uuid); + profile.setName(obj.getString("name")); + VpnType type = VpnType.fromIdentifier(obj.getString("type")); + profile.setVpnType(type); + + JSONObject remote = obj.getJSONObject("remote"); + profile.setGateway(remote.getString("addr")); + profile.setPort(getInteger(remote, "port", 1, 65535)); + profile.setRemoteId(remote.optString("id", null)); + profile.Certificate = decodeBase64(remote.optString("cert", null)); + + JSONObject local = obj.optJSONObject("local"); + if (local != null) + { + if (type.has(VpnTypeFeature.USER_PASS)) + { + profile.setUsername(local.optString("eap_id", null)); + } + + if (type.has(VpnTypeFeature.CERTIFICATE)) + { + profile.setLocalId(local.optString("id", null)); + profile.PKCS12 = decodeBase64(local.optString("p12", null)); + } + } + + profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX)); + JSONObject split = obj.optJSONObject("split-tunneling"); + if (split != null) + { + int st = 0; + st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0; + st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0; + profile.setSplitTunneling(st == 0 ? null : st); + } + return profile; + } + + private Integer getInteger(JSONObject obj, String key, int min, int max) + { + Integer res = obj.optInt(key); + return res < min || res > max ? null : res; + } + + /** + * Save or update the profile depending on whether we actually have a + * profile object or not (this was created in updateProfileData) + */ + private void saveProfile() + { + if (verifyInput()) + { + updateProfileData(); + if (mExisting != null) + { + mProfile.setId(mExisting.getId()); + mDataSource.updateVpnProfile(mProfile); + } + else + { + mDataSource.insertProfile(mProfile); + } + if (mCertEntry != null) + { + try + { /* store the CA/server certificate */ + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + store.setCertificateEntry(null, mCertEntry.getCertificate()); + TrustedCertificateManager.getInstance().reset(); + } + catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) + { + e.printStackTrace(); + } + } + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId()); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + + intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId())); + finish(); + } + } + + /** + * Verify the user input and display error messages. + * @return true if the input is valid + */ + private boolean verifyInput() + { + boolean valid = true; + if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS)) + { + if (mUsername.getText().toString().trim().isEmpty()) + { + mUsernameWrap.setError(getString(R.string.alert_text_no_input_username)); + valid = false; + } + } + if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null) + { /* let's show an error icon */ + ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(""); + valid = false; + } + return valid; + } + + /** + * Update the profile object with the data entered by the user + */ + private void updateProfileData() + { + if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS)) + { + mProfile.setUsername(mUsername.getText().toString().trim()); + String password = mPassword.getText().toString().trim(); + password = password.isEmpty() ? null : password; + mProfile.setPassword(password); + } + if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE)) + { + mProfile.setUserCertificateAlias(mUserCertEntry.getAlias()); + } + if (mCertEntry != null) + { + mProfile.setCertificateAlias(mCertEntry.getAlias()); + } + } + + /** + * Load the JSON-encoded VPN profile from the given URI + */ + private static class ProfileLoader extends AsyncTaskLoader<ProfileLoadResult> + { + private final Uri mUri; + private ProfileLoadResult mData; + + public ProfileLoader(Context context, Uri uri) + { + super(context); + mUri = uri; + } + + @Override + public ProfileLoadResult loadInBackground() + { + ProfileLoadResult result = new ProfileLoadResult(); + InputStream in = null; + + if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) || + ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) + { + try + { + in = getContext().getContentResolver().openInputStream(mUri); + } + catch (FileNotFoundException e) + { + result.ThrownException = e; + } + } + else + { + try + { + URL url = new URL(mUri.toString()); + in = url.openStream(); + } + catch (IOException e) + { + result.ThrownException = e; + } + } + if (in != null) + { + result.Profile = streamToString(in); + } + return result; + } + + @Override + protected void onStartLoading() + { + if (mData != null) + { /* if we have data ready, deliver it directly */ + deliverResult(mData); + } + if (takeContentChanged() || mData == null) + { + forceLoad(); + } + } + + @Override + public void deliverResult(ProfileLoadResult data) + { + if (isReset()) + { + return; + } + mData = data; + if (isStarted()) + { /* if it is started we deliver the data directly, + * otherwise this is handled in onStartLoading */ + super.deliverResult(data); + } + } + + @Override + protected void onReset() + { + mData = null; + super.onReset(); + } + + private String streamToString(InputStream in) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int len; + + try + { + while ((len = in.read(buf)) != -1) + { + out.write(buf, 0, len); + } + return out.toString("UTF-8"); + } + catch (IOException e) + { + e.printStackTrace(); + } + return null; + } + } + + private static class ProfileLoadResult + { + public String Profile; + public Exception ThrownException; + } + + /** + * Ask the user to select an available certificate. + */ + private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback + { + @Override + public void onClick(View v) + { + String alias = null; + if (mUserCertEntry != null) + { + alias = mUserCertEntry.getAlias(); + mUserCertEntry = null; + } + else if (mProfile != null) + { + alias = getString(R.string.profile_cert_alias, mProfile.getName()); + } + KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias); + } + + @Override + public void alias(final String alias) + { + /* alias() is not called from our main thread */ + runOnUiThread(new Runnable() { + @Override + public void run() + { + mUserCertLoading = alias; + updateUserCertView(); + if (alias != null) + { /* otherwise the dialog was canceled, the request denied */ + getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks); + } + } + }); + } + } + + /** + * Load the selected user certificate asynchronously. This cannot be done + * from the main thread as getCertificateChain() calls back to our main + * thread to bind to the KeyChain service resulting in a deadlock. + */ + private static class UserCertificateLoader extends AsyncTaskLoader<TrustedCertificateEntry> + { + private final String mAlias; + private TrustedCertificateEntry mData; + + public UserCertificateLoader(Context context, String alias) + { + super(context); + mAlias = alias; + } + + @Override + public TrustedCertificateEntry loadInBackground() + { + X509Certificate[] chain = null; + try + { + chain = KeyChain.getCertificateChain(getContext(), mAlias); + } + catch (KeyChainException | InterruptedException e) + { + e.printStackTrace(); + } + if (chain != null && chain.length > 0) + { + return new TrustedCertificateEntry(mAlias, chain[0]); + } + return null; + } + + @Override + protected void onStartLoading() + { + if (mData != null) + { /* if we have data ready, deliver it directly */ + deliverResult(mData); + } + if (takeContentChanged() || mData == null) + { + forceLoad(); + } + } + + @Override + public void deliverResult(TrustedCertificateEntry data) + { + if (isReset()) + { + return; + } + mData = data; + if (isStarted()) + { /* if it is started we deliver the data directly, + * otherwise this is handled in onStartLoading */ + super.deliverResult(data); + } + } + + @Override + protected void onReset() + { + mData = null; + super.onReset(); + } + } + + private byte[] decodeBase64(String encoded) + { + if (encoded == null || encoded.isEmpty()) + { + return null; + } + byte[] data = null; + try + { + data = Base64.decode(encoded, Base64.DEFAULT); + } + catch (IllegalArgumentException e) + { + e.printStackTrace(); + } + return data; + } + + private class ParsedVpnProfile extends VpnProfile + { + public byte[] Certificate; + public byte[] PKCS12; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java index d8d99ff00..8210d597c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java @@ -17,12 +17,14 @@ package org.strongswan.android.ui; -import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; import android.util.AttributeSet; import android.view.ActionMode; import android.view.LayoutInflater; @@ -41,9 +43,11 @@ import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; import org.strongswan.android.ui.adapter.VpnProfileAdapter; +import org.strongswan.android.utils.Constants; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; public class VpnProfileListFragment extends Fragment @@ -58,11 +62,47 @@ public class VpnProfileListFragment extends Fragment private OnVpnProfileSelectedListener mListener; private boolean mReadOnly; + private BroadcastReceiver mProfilesChanged = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + long id, ids[]; + if ((id = intent.getLongExtra(Constants.VPN_PROFILES_SINGLE, 0)) > 0) + { + VpnProfile profile = mDataSource.getVpnProfile(id); + if (profile != null) + { /* in case this was an edit, we remove it first */ + mVpnProfiles.remove(profile); + mVpnProfiles.add(profile); + mListAdapter.notifyDataSetChanged(); + } + } + else if ((ids = intent.getLongArrayExtra(Constants.VPN_PROFILES_MULTIPLE)) != null) + { + for (long i : ids) + { + Iterator<VpnProfile> profiles = mVpnProfiles.iterator(); + while (profiles.hasNext()) + { + VpnProfile profile = profiles.next(); + if (profile.getId() == i) + { + profiles.remove(); + break; + } + } + } + mListAdapter.notifyDataSetChanged(); + } + } + }; + /** * The activity containing this fragment should implement this interface */ public interface OnVpnProfileSelectedListener { - public void onVpnProfileSelected(VpnProfile profile); + void onVpnProfileSelected(VpnProfile profile); } @Override @@ -116,6 +156,9 @@ public class VpnProfileListFragment extends Fragment mVpnProfiles = mDataSource.getAllVpnProfiles(); mListAdapter = new VpnProfileAdapter(getActivity(), R.layout.profile_list_item, mVpnProfiles); + + IntentFilter profileChangesFilter = new IntentFilter(Constants.VPN_PROFILES_CHANGED); + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mProfilesChanged, profileChangesFilter); } @Override @@ -123,6 +166,7 @@ public class VpnProfileListFragment extends Fragment { super.onDestroy(); mDataSource.close(); + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mProfilesChanged); } @Override @@ -157,30 +201,6 @@ public class VpnProfileListFragment extends Fragment } } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) - { - switch (requestCode) - { - case ADD_REQUEST: - case EDIT_REQUEST: - if (resultCode != Activity.RESULT_OK) - { - return; - } - long id = data.getLongExtra(VpnProfileDataSource.KEY_ID, 0); - VpnProfile profile = mDataSource.getVpnProfile(id); - if (profile != null) - { /* in case this was an edit, we remove it first */ - mVpnProfiles.remove(profile); - mVpnProfiles.add(profile); - mListAdapter.notifyDataSetChanged(); - } - return; - } - super.onActivityResult(requestCode, resultCode, data); - } - private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> a, View v, int position, long id) @@ -213,7 +233,7 @@ public class VpnProfileListFragment extends Fragment MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.profile_list_context, menu); mEditProfile = menu.findItem(R.id.edit_profile); - mSelected = new HashSet<Integer>(); + mSelected = new HashSet<>(); mode.setTitle(R.string.select_profiles); return true; } @@ -234,17 +254,21 @@ public class VpnProfileListFragment extends Fragment } case R.id.delete_profile: { - ArrayList<VpnProfile> profiles = new ArrayList<VpnProfile>(); + ArrayList<VpnProfile> profiles = new ArrayList<>(); for (int position : mSelected) { profiles.add((VpnProfile)mListView.getItemAtPosition(position)); } - for (VpnProfile profile : profiles) + long ids[] = new long[profiles.size()]; + for (int i = 0; i < profiles.size(); i++) { + VpnProfile profile = profiles.get(i); + ids[i] = profile.getId(); mDataSource.deleteVpnProfile(profile); - mVpnProfiles.remove(profile); } - mListAdapter.notifyDataSetChanged(); + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_MULTIPLE, ids); + LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); Toast.makeText(VpnProfileListFragment.this.getActivity(), R.string.profiles_deleted, Toast.LENGTH_SHORT).show(); break; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java new file mode 100644 index 000000000..413ecae97 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.utils; + +public final class Constants +{ + /** + * Intent action used to notify about changes to the VPN profiles + */ + public static final String VPN_PROFILES_CHANGED = "org.strongswan.android.VPN_PROFILES_CHANGED"; + + /** + * Used in the intent above to notify about edits or inserts of a VPN profile (long) + */ + public static final String VPN_PROFILES_SINGLE = "org.strongswan.android.VPN_PROFILES_SINGLE"; + + /** + * Used in the intent above to notify about the deletion of multiple VPN profiles (array of longs) + */ + public static final String VPN_PROFILES_MULTIPLE = "org.strongswan.android.VPN_PROFILES_MULTIPLE"; + + /** + * Limits for MTU + */ + public static final int MTU_MAX = 1500; + public static final int MTU_MIN = 1280; +} diff --git a/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png Binary files differnew file mode 100644 index 000000000..ceb1a1eeb --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png diff --git a/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png Binary files differnew file mode 100644 index 000000000..af7f8288d --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png diff --git a/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png Binary files differnew file mode 100644 index 000000000..b7c7ffd0e --- /dev/null +++ b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png diff --git a/src/frontends/android/app/src/main/res/layout/profile_import_view.xml b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml new file mode 100644 index 000000000..fc06aa5d4 --- /dev/null +++ b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml @@ -0,0 +1,196 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 Tobias Brunner + HSR Hochschule fuer Technik Rapperswil + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. +--> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="10dp" + android:animateLayoutChanges="true" > + + <TextView + android:id="@+id/exists_warning" + android:background="@drawable/state_background" + android:padding="8dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:drawableLeft="@android:drawable/ic_dialog_alert" + android:drawableStart="@android:drawable/ic_dialog_alert" + android:drawablePadding="8dp" + android:textStyle="bold" + android:text="@string/profile_import_exists" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorPrimary" /> + + <LinearLayout + android:id="@+id/basic_data_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:textSize="12sp" + android:text="@string/profile_name_label_simple" /> + + <TextView + android:id="@+id/name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:textSize="12sp" + android:text="@string/profile_gateway_label" /> + + <TextView + android:id="@+id/gateway" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:textSize="12sp" + android:text="@string/profile_vpn_type_label" /> + + <TextView + android:id="@+id/vpn_type" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/username_password_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginTop="6dp"> + + <org.strongswan.android.ui.widget.TextInputLayoutHelper + android:id="@+id/username_wrap" + android:layout_width="match_parent" + android:layout_height="wrap_content" > + + <android.support.design.widget.TextInputEditText + android:id="@+id/username" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:inputType="textNoSuggestions" + android:hint="@string/profile_username_label" /> + + </org.strongswan.android.ui.widget.TextInputLayoutHelper> + + <org.strongswan.android.ui.widget.TextInputLayoutHelper + android:id="@+id/password_wrap" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + app:helper_text="@string/profile_password_hint" > + + <android.support.design.widget.TextInputEditText + android:id="@+id/password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:inputType="textPassword|textNoSuggestions" + android:hint="@string/profile_password_label" /> + + </org.strongswan.android.ui.widget.TextInputLayoutHelper> + + </LinearLayout> + + <LinearLayout + android:id="@+id/user_certificate_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:orientation="vertical" > + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginLeft="4dp" + android:textSize="12sp" + android:text="@string/profile_user_certificate_label" /> + + <include + android:id="@+id/select_user_certificate" + layout="@layout/two_line_button" /> + + <Button + android:id="@+id/import_user_certificate" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" + android:text="@string/profile_cert_import" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/remote_certificate_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:orientation="vertical"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="12sp" + android:text="@string/profile_ca_label" /> + + <include + android:id="@+id/remote_certificate" + layout="@layout/two_line_button" /> + </LinearLayout> + + </LinearLayout> + +</ScrollView> diff --git a/src/frontends/android/app/src/main/res/menu/profile_import.xml b/src/frontends/android/app/src/main/res/menu/profile_import.xml new file mode 100644 index 000000000..99893f7b0 --- /dev/null +++ b/src/frontends/android/app/src/main/res/menu/profile_import.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 Tobias Brunner + Hochschule fuer Technik Rapperswil + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/menu_accept" + android:title="@string/profile_edit_import" + app:showAsAction="always|withText" /> + +</menu> diff --git a/src/frontends/android/app/src/main/res/values-de/strings.xml b/src/frontends/android/app/src/main/res/values-de/strings.xml index 857a18aaa..0bb73cb1e 100644 --- a/src/frontends/android/app/src/main/res/values-de/strings.xml +++ b/src/frontends/android/app/src/main/res/values-de/strings.xml @@ -48,8 +48,10 @@ <!-- VPN profile details --> <string name="profile_edit_save">Speichern</string> + <string name="profile_edit_import">Importieren</string> <string name="profile_edit_cancel">Abbrechen</string> <string name="profile_name_label">Profilname (optional)</string> + <string name="profile_name_label_simple">Profilname</string> <string name="profile_name_hint">Standardwert ist der konfigurierte Server</string> <string name="profile_name_hint_gateway">Standardwert ist \"%1$s\"</string> <string name="profile_gateway_label">Server</string> @@ -80,6 +82,15 @@ <string name="profile_split_tunneling_label">Split-Tunneling</string> <string name="profile_split_tunnelingv4_title">Blockiere IPv4 Verkehr der nicht für das VPN bestimmt ist</string> <string name="profile_split_tunnelingv6_title">Blockiere IPv6 Verkehr der nicht für das VPN bestimmt ist</string> + <string name="profile_import">VPN Profile importieren</string> + <string name="profile_import_failed">VPN Profil-Import fehlgeschlagen</string> + <string name="profile_import_failed_detail">VPN Profil-Import fehlgeschlagen: %1$s</string> + <string name="profile_import_failed_not_found">Datei nicht gefunden</string> + <string name="profile_import_failed_host">Host unbekannt</string> + <string name="profile_import_failed_tls">TLS-Handshake fehlgeschlagen</string> + <string name="profile_import_exists">Dieses VPN Profil existiert bereits, die bestehenden Einstellungen werden ersetzt.</string> + <string name="profile_cert_import">Zertifikat aus VPN Profil importieren</string> + <string name="profile_cert_alias">Zertifikat für \"%1$s\"</string> <!-- Warnings/Notifications in the details view --> <string name="alert_text_no_input_gateway">Ein Wert wird benötigt, um die Verbindung aufbauen zu können</string> <string name="alert_text_no_input_username">Bitte geben Sie Ihren Benutzernamen ein</string> diff --git a/src/frontends/android/app/src/main/res/values-pl/strings.xml b/src/frontends/android/app/src/main/res/values-pl/strings.xml index df4412210..bf08b8ee1 100644 --- a/src/frontends/android/app/src/main/res/values-pl/strings.xml +++ b/src/frontends/android/app/src/main/res/values-pl/strings.xml @@ -48,8 +48,10 @@ <!-- VPN profile details --> <string name="profile_edit_save">Zapisz</string> + <string name="profile_edit_import">Import</string> <string name="profile_edit_cancel">Anuluj</string> <string name="profile_name_label">Nazwa profilu (opcjonalny)</string> + <string name="profile_name_label_simple">Nazwa profilu</string> <string name="profile_name_hint">Defaults to the configured server</string> <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string> <string name="profile_gateway_label">Serwer</string> @@ -80,6 +82,15 @@ <string name="profile_split_tunneling_label">Split tunneling</string> <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string> <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string> + <string name="profile_import">Import VPN profile</string> + <string name="profile_import_failed">Failed to import VPN profile</string> + <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string> + <string name="profile_import_failed_not_found">File not found</string> + <string name="profile_import_failed_host">Host unknown</string> + <string name="profile_import_failed_tls">TLS handshake failed</string> + <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string> + <string name="profile_cert_import">Import certificate from VPN profile</string> + <string name="profile_cert_alias">Certificate for \"%1$s\"</string> <!-- Warnings/Notifications in the details view --> <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string> <string name="alert_text_no_input_username">Wprowadź swoją nazwę użytkownika</string> diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml index 933b2fb2d..03ee02670 100644 --- a/src/frontends/android/app/src/main/res/values-ru/strings.xml +++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml @@ -45,8 +45,10 @@ <!-- VPN profile details --> <string name="profile_edit_save">Сохранить</string> + <string name="profile_edit_import">Import</string> <string name="profile_edit_cancel">Отмена</string> <string name="profile_name_label">Название профиля (необязательный)</string> + <string name="profile_name_label_simple">Название профиля</string> <string name="profile_name_hint">Defaults to the configured server</string> <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string> <string name="profile_gateway_label">Сервер</string> @@ -77,6 +79,15 @@ <string name="profile_split_tunneling_label">Split tunneling</string> <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string> <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string> + <string name="profile_import">Import VPN profile</string> + <string name="profile_import_failed">Failed to import VPN profile</string> + <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string> + <string name="profile_import_failed_not_found">File not found</string> + <string name="profile_import_failed_host">Host unknown</string> + <string name="profile_import_failed_tls">TLS handshake failed</string> + <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string> + <string name="profile_cert_import">Import certificate from VPN profile</string> + <string name="profile_cert_alias">Certificate for \"%1$s\"</string> <!-- Warnings/Notifications in the details view --> <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string> <string name="alert_text_no_input_username">Пожалуйста введите имя пользователя</string> diff --git a/src/frontends/android/app/src/main/res/values-ua/strings.xml b/src/frontends/android/app/src/main/res/values-ua/strings.xml index e48a9215e..07c726a96 100644 --- a/src/frontends/android/app/src/main/res/values-ua/strings.xml +++ b/src/frontends/android/app/src/main/res/values-ua/strings.xml @@ -46,8 +46,10 @@ <!-- VPN profile details --> <string name="profile_edit_save">Зберегти</string> + <string name="profile_edit_import">Import</string> <string name="profile_edit_cancel">Відміна</string> <string name="profile_name_label">Назва профілю (необов\'язковий)</string> + <string name="profile_name_label_simple">Назва профілю</string> <string name="profile_name_hint">Defaults to the configured server</string> <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string> <string name="profile_gateway_label">Сервер</string> @@ -78,6 +80,15 @@ <string name="profile_split_tunneling_label">Split tunneling</string> <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string> <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string> + <string name="profile_import">Import VPN profile</string> + <string name="profile_import_failed">Failed to import VPN profile</string> + <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string> + <string name="profile_import_failed_not_found">File not found</string> + <string name="profile_import_failed_host">Host unknown</string> + <string name="profile_import_failed_tls">TLS handshake failed</string> + <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string> + <string name="profile_cert_import">Import certificate from VPN profile</string> + <string name="profile_cert_alias">Certificate for \"%1$s\"</string> <!-- Warnings/Notifications in the details view --> <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string> <string name="alert_text_no_input_username">Введіть ім\'я користувача </string> diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml index 0ceace143..8d74e4544 100644 --- a/src/frontends/android/app/src/main/res/values/strings.xml +++ b/src/frontends/android/app/src/main/res/values/strings.xml @@ -48,8 +48,10 @@ <!-- VPN profile details --> <string name="profile_edit_save">Save</string> + <string name="profile_edit_import">Import</string> <string name="profile_edit_cancel">Cancel</string> <string name="profile_name_label">Profile name (optional)</string> + <string name="profile_name_label_simple">Profile name</string> <string name="profile_name_hint">Defaults to the configured server</string> <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string> <string name="profile_gateway_label">Server</string> @@ -80,6 +82,15 @@ <string name="profile_split_tunneling_label">Split tunneling</string> <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string> <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string> + <string name="profile_import">Import VPN profile</string> + <string name="profile_import_failed">Failed to import VPN profile</string> + <string name="profile_import_failed_detail">Failed to import VPN profile: %1$s</string> + <string name="profile_import_failed_not_found">File not found</string> + <string name="profile_import_failed_host">Host unknown</string> + <string name="profile_import_failed_tls">TLS handshake failed</string> + <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string> + <string name="profile_cert_import">Import certificate from VPN profile</string> + <string name="profile_cert_alias">Certificate for \"%1$s\"</string> <!-- Warnings/Notifications in the details view --> <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string> <string name="alert_text_no_input_username">Please enter your username </string> |