Home | History | Annotate | Download | only in vpn2
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.vpn2;
     18 
     19 import android.app.AlertDialog;
     20 import android.content.Context;
     21 import android.content.DialogInterface;
     22 import android.os.Bundle;
     23 import android.os.SystemProperties;
     24 import android.security.Credentials;
     25 import android.security.KeyStore;
     26 import android.text.Editable;
     27 import android.text.TextWatcher;
     28 import android.view.View;
     29 import android.view.WindowManager;
     30 import android.widget.AdapterView;
     31 import android.widget.ArrayAdapter;
     32 import android.widget.CheckBox;
     33 import android.widget.CompoundButton;
     34 import android.widget.Spinner;
     35 import android.widget.TextView;
     36 
     37 import com.android.internal.net.VpnProfile;
     38 import com.android.settings.R;
     39 
     40 import java.net.InetAddress;
     41 
     42 /**
     43  * Dialog showing information about a VPN configuration. The dialog
     44  * can be launched to either edit or prompt for credentials to connect
     45  * to a user-added VPN.
     46  *
     47  * {@see AppDialog}
     48  */
     49 class ConfigDialog extends AlertDialog implements TextWatcher,
     50         View.OnClickListener, AdapterView.OnItemSelectedListener,
     51         CompoundButton.OnCheckedChangeListener {
     52     private final KeyStore mKeyStore = KeyStore.getInstance();
     53     private final DialogInterface.OnClickListener mListener;
     54     private final VpnProfile mProfile;
     55 
     56     private boolean mEditing;
     57     private boolean mExists;
     58 
     59     private View mView;
     60 
     61     private TextView mName;
     62     private Spinner mType;
     63     private TextView mServer;
     64     private TextView mUsername;
     65     private TextView mPassword;
     66     private TextView mSearchDomains;
     67     private TextView mDnsServers;
     68     private TextView mRoutes;
     69     private CheckBox mMppe;
     70     private TextView mL2tpSecret;
     71     private TextView mIpsecIdentifier;
     72     private TextView mIpsecSecret;
     73     private Spinner mIpsecUserCert;
     74     private Spinner mIpsecCaCert;
     75     private Spinner mIpsecServerCert;
     76     private CheckBox mSaveLogin;
     77     private CheckBox mShowOptions;
     78     private CheckBox mAlwaysOnVpn;
     79 
     80     ConfigDialog(Context context, DialogInterface.OnClickListener listener,
     81             VpnProfile profile, boolean editing, boolean exists) {
     82         super(context);
     83 
     84         mListener = listener;
     85         mProfile = profile;
     86         mEditing = editing;
     87         mExists = exists;
     88     }
     89 
     90     @Override
     91     protected void onCreate(Bundle savedState) {
     92         mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
     93         setView(mView);
     94 
     95         Context context = getContext();
     96 
     97         // First, find out all the fields.
     98         mName = (TextView) mView.findViewById(R.id.name);
     99         mType = (Spinner) mView.findViewById(R.id.type);
    100         mServer = (TextView) mView.findViewById(R.id.server);
    101         mUsername = (TextView) mView.findViewById(R.id.username);
    102         mPassword = (TextView) mView.findViewById(R.id.password);
    103         mSearchDomains = (TextView) mView.findViewById(R.id.search_domains);
    104         mDnsServers = (TextView) mView.findViewById(R.id.dns_servers);
    105         mRoutes = (TextView) mView.findViewById(R.id.routes);
    106         mMppe = (CheckBox) mView.findViewById(R.id.mppe);
    107         mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret);
    108         mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
    109         mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
    110         mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
    111         mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
    112         mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
    113         mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
    114         mShowOptions = (CheckBox) mView.findViewById(R.id.show_options);
    115         mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn);
    116 
    117         // Second, copy values from the profile.
    118         mName.setText(mProfile.name);
    119         mType.setSelection(mProfile.type);
    120         mServer.setText(mProfile.server);
    121         if (mProfile.saveLogin) {
    122             mUsername.setText(mProfile.username);
    123             mPassword.setText(mProfile.password);
    124         }
    125         mSearchDomains.setText(mProfile.searchDomains);
    126         mDnsServers.setText(mProfile.dnsServers);
    127         mRoutes.setText(mProfile.routes);
    128         mMppe.setChecked(mProfile.mppe);
    129         mL2tpSecret.setText(mProfile.l2tpSecret);
    130         mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
    131         mIpsecSecret.setText(mProfile.ipsecSecret);
    132         loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY, 0, mProfile.ipsecUserCert);
    133         loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE,
    134                 R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
    135         loadCertificates(mIpsecServerCert, Credentials.USER_CERTIFICATE,
    136                 R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
    137         mSaveLogin.setChecked(mProfile.saveLogin);
    138         mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
    139         mAlwaysOnVpn.setOnCheckedChangeListener(this);
    140         // Update SaveLogin checkbox after Always-on checkbox is updated
    141         updateSaveLoginStatus();
    142 
    143         // Hide lockdown VPN on devices that require IMS authentication
    144         if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
    145             mAlwaysOnVpn.setVisibility(View.GONE);
    146         }
    147 
    148         // Third, add listeners to required fields.
    149         mName.addTextChangedListener(this);
    150         mType.setOnItemSelectedListener(this);
    151         mServer.addTextChangedListener(this);
    152         mUsername.addTextChangedListener(this);
    153         mPassword.addTextChangedListener(this);
    154         mDnsServers.addTextChangedListener(this);
    155         mRoutes.addTextChangedListener(this);
    156         mIpsecSecret.addTextChangedListener(this);
    157         mIpsecUserCert.setOnItemSelectedListener(this);
    158         mShowOptions.setOnClickListener(this);
    159 
    160         // Fourth, determine whether to do editing or connecting.
    161         boolean valid = validate(true);
    162         mEditing = mEditing || !valid;
    163 
    164         if (mEditing) {
    165             setTitle(R.string.vpn_edit);
    166 
    167             // Show common fields.
    168             mView.findViewById(R.id.editor).setVisibility(View.VISIBLE);
    169 
    170             // Show type-specific fields.
    171             changeType(mProfile.type);
    172 
    173             // Hide 'save login' when we are editing.
    174             mSaveLogin.setVisibility(View.GONE);
    175 
    176             // Switch to advanced view immediately if any advanced options are on
    177             if (!mProfile.searchDomains.isEmpty() || !mProfile.dnsServers.isEmpty() ||
    178                     !mProfile.routes.isEmpty()) {
    179                 showAdvancedOptions();
    180             }
    181 
    182             // Create a button to forget the profile if it has already been saved..
    183             if (mExists) {
    184                 setButton(DialogInterface.BUTTON_NEUTRAL,
    185                         context.getString(R.string.vpn_forget), mListener);
    186             }
    187 
    188             // Create a button to save the profile.
    189             setButton(DialogInterface.BUTTON_POSITIVE,
    190                     context.getString(R.string.vpn_save), mListener);
    191         } else {
    192             setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
    193 
    194             // Create a button to connect the network.
    195             setButton(DialogInterface.BUTTON_POSITIVE,
    196                     context.getString(R.string.vpn_connect), mListener);
    197         }
    198 
    199         // Always provide a cancel button.
    200         setButton(DialogInterface.BUTTON_NEGATIVE,
    201                 context.getString(R.string.vpn_cancel), mListener);
    202 
    203         // Let AlertDialog create everything.
    204         super.onCreate(savedState);
    205 
    206         // Disable the action button if necessary.
    207         getButton(DialogInterface.BUTTON_POSITIVE)
    208                 .setEnabled(mEditing ? valid : validate(false));
    209 
    210         // Workaround to resize the dialog for the input method.
    211         getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
    212                 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
    213     }
    214 
    215     @Override
    216     public void onRestoreInstanceState(Bundle savedState) {
    217         super.onRestoreInstanceState(savedState);
    218 
    219         // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced
    220         // options here if they were already revealed or set.
    221         if (mShowOptions.isChecked()) {
    222             showAdvancedOptions();
    223         }
    224     }
    225 
    226     @Override
    227     public void afterTextChanged(Editable field) {
    228         getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
    229     }
    230 
    231     @Override
    232     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    233     }
    234 
    235     @Override
    236     public void onTextChanged(CharSequence s, int start, int before, int count) {
    237     }
    238 
    239     @Override
    240     public void onClick(View view) {
    241         if (view == mShowOptions) {
    242             showAdvancedOptions();
    243         }
    244     }
    245 
    246     @Override
    247     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
    248         if (parent == mType) {
    249             changeType(position);
    250         }
    251         getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
    252     }
    253 
    254     @Override
    255     public void onNothingSelected(AdapterView<?> parent) {
    256     }
    257 
    258     @Override
    259     public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
    260         if (compoundButton == mAlwaysOnVpn) {
    261             updateSaveLoginStatus();
    262             getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
    263         }
    264     }
    265 
    266     public boolean isVpnAlwaysOn() {
    267         return mAlwaysOnVpn.isChecked();
    268     }
    269 
    270     private void updateSaveLoginStatus() {
    271         if (mAlwaysOnVpn.isChecked()) {
    272             mSaveLogin.setChecked(true);
    273             mSaveLogin.setEnabled(false);
    274         } else {
    275             mSaveLogin.setChecked(mProfile.saveLogin);
    276             mSaveLogin.setEnabled(true);
    277         }
    278     }
    279 
    280     private void showAdvancedOptions() {
    281         mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
    282         mShowOptions.setVisibility(View.GONE);
    283     }
    284 
    285     private void changeType(int type) {
    286         // First, hide everything.
    287         mMppe.setVisibility(View.GONE);
    288         mView.findViewById(R.id.l2tp).setVisibility(View.GONE);
    289         mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
    290         mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
    291         mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
    292 
    293         // Then, unhide type-specific fields.
    294         switch (type) {
    295             case VpnProfile.TYPE_PPTP:
    296                 mMppe.setVisibility(View.VISIBLE);
    297                 break;
    298 
    299             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
    300                 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
    301                 // fall through
    302             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
    303                 mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
    304                 break;
    305 
    306             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
    307                 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
    308                 // fall through
    309             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
    310                 mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
    311                 // fall through
    312             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
    313                 mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
    314                 break;
    315         }
    316     }
    317 
    318     private boolean validate(boolean editing) {
    319         if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) {
    320             return false;
    321         }
    322         if (!editing) {
    323             return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
    324         }
    325         if (mName.getText().length() == 0 || mServer.getText().length() == 0 ||
    326                 !validateAddresses(mDnsServers.getText().toString(), false) ||
    327                 !validateAddresses(mRoutes.getText().toString(), true)) {
    328             return false;
    329         }
    330         switch (mType.getSelectedItemPosition()) {
    331             case VpnProfile.TYPE_PPTP:
    332             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
    333                 return true;
    334 
    335             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
    336             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
    337                 return mIpsecSecret.getText().length() != 0;
    338 
    339             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
    340             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
    341                 return mIpsecUserCert.getSelectedItemPosition() != 0;
    342         }
    343         return false;
    344     }
    345 
    346     private boolean validateAddresses(String addresses, boolean cidr) {
    347         try {
    348             for (String address : addresses.split(" ")) {
    349                 if (address.isEmpty()) {
    350                     continue;
    351                 }
    352                 // Legacy VPN currently only supports IPv4.
    353                 int prefixLength = 32;
    354                 if (cidr) {
    355                     String[] parts = address.split("/", 2);
    356                     address = parts[0];
    357                     prefixLength = Integer.parseInt(parts[1]);
    358                 }
    359                 byte[] bytes = InetAddress.parseNumericAddress(address).getAddress();
    360                 int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 |
    361                         (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24;
    362                 if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 ||
    363                         (prefixLength < 32 && (integer << prefixLength) != 0)) {
    364                     return false;
    365                 }
    366             }
    367         } catch (Exception e) {
    368             return false;
    369         }
    370         return true;
    371     }
    372 
    373     private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) {
    374         Context context = getContext();
    375         String first = (firstId == 0) ? "" : context.getString(firstId);
    376         String[] certificates = mKeyStore.list(prefix);
    377 
    378         if (certificates == null || certificates.length == 0) {
    379             certificates = new String[] {first};
    380         } else {
    381             String[] array = new String[certificates.length + 1];
    382             array[0] = first;
    383             System.arraycopy(certificates, 0, array, 1, certificates.length);
    384             certificates = array;
    385         }
    386 
    387         ArrayAdapter<String> adapter = new ArrayAdapter<String>(
    388                 context, android.R.layout.simple_spinner_item, certificates);
    389         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    390         spinner.setAdapter(adapter);
    391 
    392         for (int i = 1; i < certificates.length; ++i) {
    393             if (certificates[i].equals(selected)) {
    394                 spinner.setSelection(i);
    395                 break;
    396             }
    397         }
    398     }
    399 
    400     boolean isEditing() {
    401         return mEditing;
    402     }
    403 
    404     VpnProfile getProfile() {
    405         // First, save common fields.
    406         VpnProfile profile = new VpnProfile(mProfile.key);
    407         profile.name = mName.getText().toString();
    408         profile.type = mType.getSelectedItemPosition();
    409         profile.server = mServer.getText().toString().trim();
    410         profile.username = mUsername.getText().toString();
    411         profile.password = mPassword.getText().toString();
    412         profile.searchDomains = mSearchDomains.getText().toString().trim();
    413         profile.dnsServers = mDnsServers.getText().toString().trim();
    414         profile.routes = mRoutes.getText().toString().trim();
    415 
    416         // Then, save type-specific fields.
    417         switch (profile.type) {
    418             case VpnProfile.TYPE_PPTP:
    419                 profile.mppe = mMppe.isChecked();
    420                 break;
    421 
    422             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
    423                 profile.l2tpSecret = mL2tpSecret.getText().toString();
    424                 // fall through
    425             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
    426                 profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
    427                 profile.ipsecSecret = mIpsecSecret.getText().toString();
    428                 break;
    429 
    430             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
    431                 profile.l2tpSecret = mL2tpSecret.getText().toString();
    432                 // fall through
    433             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
    434                 if (mIpsecUserCert.getSelectedItemPosition() != 0) {
    435                     profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
    436                 }
    437                 // fall through
    438             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
    439                 if (mIpsecCaCert.getSelectedItemPosition() != 0) {
    440                     profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
    441                 }
    442                 if (mIpsecServerCert.getSelectedItemPosition() != 0) {
    443                     profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
    444                 }
    445                 break;
    446         }
    447 
    448         final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
    449         profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin);
    450         return profile;
    451     }
    452 }
    453