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 com.android.settings.R;
     20 
     21 import android.content.Context;
     22 import android.content.DialogInterface;
     23 import android.net.IConnectivityManager;
     24 import android.net.LinkProperties;
     25 import android.net.RouteInfo;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.os.Message;
     29 import android.os.ServiceManager;
     30 import android.preference.Preference;
     31 import android.preference.PreferenceGroup;
     32 import android.security.Credentials;
     33 import android.security.KeyStore;
     34 import android.util.Log;
     35 import android.view.ContextMenu;
     36 import android.view.ContextMenu.ContextMenuInfo;
     37 import android.view.Menu;
     38 import android.view.MenuItem;
     39 import android.view.View;
     40 import android.widget.AdapterView.AdapterContextMenuInfo;
     41 
     42 import com.android.internal.net.LegacyVpnInfo;
     43 import com.android.internal.net.VpnConfig;
     44 import com.android.settings.SettingsPreferenceFragment;
     45 
     46 import java.net.Inet4Address;
     47 import java.nio.charset.Charsets;
     48 import java.util.Arrays;
     49 import java.util.HashMap;
     50 
     51 public class VpnSettings extends SettingsPreferenceFragment implements
     52         Handler.Callback, Preference.OnPreferenceClickListener,
     53         DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
     54 
     55     private static final String TAG = "VpnSettings";
     56 
     57     private final IConnectivityManager mService = IConnectivityManager.Stub
     58             .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
     59     private final KeyStore mKeyStore = KeyStore.getInstance();
     60     private boolean mUnlocking = false;
     61 
     62     private HashMap<String, VpnPreference> mPreferences;
     63     private VpnDialog mDialog;
     64 
     65     private Handler mUpdater;
     66     private LegacyVpnInfo mInfo;
     67 
     68     // The key of the profile for the current ContextMenu.
     69     private String mSelectedKey;
     70 
     71     @Override
     72     public void onCreate(Bundle savedState) {
     73         super.onCreate(savedState);
     74         addPreferencesFromResource(R.xml.vpn_settings2);
     75         getPreferenceScreen().setOrderingAsAdded(false);
     76 
     77         if (savedState != null) {
     78             VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"),
     79                     savedState.getByteArray("VpnProfile"));
     80             if (profile != null) {
     81                 mDialog = new VpnDialog(getActivity(), this, profile,
     82                         savedState.getBoolean("VpnEditing"));
     83             }
     84         }
     85     }
     86 
     87     @Override
     88     public void onSaveInstanceState(Bundle savedState) {
     89         // We do not save view hierarchy, as they are just profiles.
     90         if (mDialog != null) {
     91             VpnProfile profile = mDialog.getProfile();
     92             savedState.putString("VpnKey", profile.key);
     93             savedState.putByteArray("VpnProfile", profile.encode());
     94             savedState.putBoolean("VpnEditing", mDialog.isEditing());
     95         }
     96         // else?
     97     }
     98 
     99     @Override
    100     public void onResume() {
    101         super.onResume();
    102 
    103         // Check KeyStore here, so others do not need to deal with it.
    104         if (mKeyStore.state() != KeyStore.State.UNLOCKED) {
    105             if (!mUnlocking) {
    106                 // Let us unlock KeyStore. See you later!
    107                 Credentials.getInstance().unlock(getActivity());
    108             } else {
    109                 // We already tried, but it is still not working!
    110                 finishFragment();
    111             }
    112             mUnlocking = !mUnlocking;
    113             return;
    114         }
    115 
    116         // Now KeyStore is always unlocked. Reset the flag.
    117         mUnlocking = false;
    118 
    119         // Currently we are the only user of profiles in KeyStore.
    120         // Assuming KeyStore and KeyGuard do the right thing, we can
    121         // safely cache profiles in the memory.
    122         if (mPreferences == null) {
    123             mPreferences = new HashMap<String, VpnPreference>();
    124             PreferenceGroup group = getPreferenceScreen();
    125 
    126             String[] keys = mKeyStore.saw(Credentials.VPN);
    127             if (keys != null && keys.length > 0) {
    128                 Context context = getActivity();
    129 
    130                 for (String key : keys) {
    131                     VpnProfile profile = VpnProfile.decode(key,
    132                             mKeyStore.get(Credentials.VPN + key));
    133                     if (profile == null) {
    134                         Log.w(TAG, "bad profile: key = " + key);
    135                         mKeyStore.delete(Credentials.VPN + key);
    136                     } else {
    137                         VpnPreference preference = new VpnPreference(context, profile);
    138                         mPreferences.put(key, preference);
    139                         group.addPreference(preference);
    140                     }
    141                 }
    142             }
    143             group.findPreference("add_network").setOnPreferenceClickListener(this);
    144         }
    145 
    146         // Show the dialog if there is one.
    147         if (mDialog != null) {
    148             mDialog.setOnDismissListener(this);
    149             mDialog.show();
    150         }
    151 
    152         // Start monitoring.
    153         if (mUpdater == null) {
    154             mUpdater = new Handler(this);
    155         }
    156         mUpdater.sendEmptyMessage(0);
    157 
    158         // Register for context menu. Hmmm, getListView() is hidden?
    159         registerForContextMenu(getListView());
    160     }
    161 
    162     @Override
    163     public void onPause() {
    164         super.onPause();
    165 
    166         // Hide the dialog if there is one.
    167         if (mDialog != null) {
    168             mDialog.setOnDismissListener(null);
    169             mDialog.dismiss();
    170         }
    171 
    172         // Unregister for context menu.
    173         if (getView() != null) {
    174             unregisterForContextMenu(getListView());
    175         }
    176     }
    177 
    178     @Override
    179     public void onDismiss(DialogInterface dialog) {
    180         // Here is the exit of a dialog.
    181         mDialog = null;
    182     }
    183 
    184     @Override
    185     public void onClick(DialogInterface dialog, int button) {
    186         if (button == DialogInterface.BUTTON_POSITIVE) {
    187             // Always save the profile.
    188             VpnProfile profile = mDialog.getProfile();
    189             mKeyStore.put(Credentials.VPN + profile.key, profile.encode());
    190 
    191             // Update the preference.
    192             VpnPreference preference = mPreferences.get(profile.key);
    193             if (preference != null) {
    194                 disconnect(profile.key);
    195                 preference.update(profile);
    196             } else {
    197                 preference = new VpnPreference(getActivity(), profile);
    198                 mPreferences.put(profile.key, preference);
    199                 getPreferenceScreen().addPreference(preference);
    200             }
    201 
    202             // If we are not editing, connect!
    203             if (!mDialog.isEditing()) {
    204                 try {
    205                     connect(profile);
    206                 } catch (Exception e) {
    207                     Log.e(TAG, "connect", e);
    208                 }
    209             }
    210         }
    211     }
    212 
    213     @Override
    214     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
    215         if (mDialog != null) {
    216             Log.v(TAG, "onCreateContextMenu() is called when mDialog != null");
    217             return;
    218         }
    219 
    220         if (info instanceof AdapterContextMenuInfo) {
    221             Preference preference = (Preference) getListView().getItemAtPosition(
    222                     ((AdapterContextMenuInfo) info).position);
    223             if (preference instanceof VpnPreference) {
    224                 VpnProfile profile = ((VpnPreference) preference).getProfile();
    225                 mSelectedKey = profile.key;
    226                 menu.setHeaderTitle(profile.name);
    227                 menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit);
    228                 menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete);
    229             }
    230         }
    231     }
    232 
    233     @Override
    234     public boolean onContextItemSelected(MenuItem item) {
    235         if (mDialog != null) {
    236             Log.v(TAG, "onContextItemSelected() is called when mDialog != null");
    237             return false;
    238         }
    239 
    240         VpnPreference preference = mPreferences.get(mSelectedKey);
    241         if (preference == null) {
    242             Log.v(TAG, "onContextItemSelected() is called but no preference is found");
    243             return false;
    244         }
    245 
    246         switch (item.getItemId()) {
    247             case R.string.vpn_menu_edit:
    248                 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true);
    249                 mDialog.setOnDismissListener(this);
    250                 mDialog.show();
    251                 return true;
    252             case R.string.vpn_menu_delete:
    253                 disconnect(mSelectedKey);
    254                 getPreferenceScreen().removePreference(preference);
    255                 mPreferences.remove(mSelectedKey);
    256                 mKeyStore.delete(Credentials.VPN + mSelectedKey);
    257                 return true;
    258         }
    259         return false;
    260     }
    261 
    262     @Override
    263     public boolean onPreferenceClick(Preference preference) {
    264         if (mDialog != null) {
    265             Log.v(TAG, "onPreferenceClick() is called when mDialog != null");
    266             return true;
    267         }
    268 
    269         if (preference instanceof VpnPreference) {
    270             VpnProfile profile = ((VpnPreference) preference).getProfile();
    271             if (mInfo != null && profile.key.equals(mInfo.key) &&
    272                     mInfo.state == LegacyVpnInfo.STATE_CONNECTED) {
    273                 try {
    274                     mInfo.intent.send();
    275                     return true;
    276                 } catch (Exception e) {
    277                     // ignore
    278                 }
    279             }
    280             mDialog = new VpnDialog(getActivity(), this, profile, false);
    281         } else {
    282             // Generate a new key. Here we just use the current time.
    283             long millis = System.currentTimeMillis();
    284             while (mPreferences.containsKey(Long.toHexString(millis))) {
    285                 ++millis;
    286             }
    287             mDialog = new VpnDialog(getActivity(), this,
    288                     new VpnProfile(Long.toHexString(millis)), true);
    289         }
    290         mDialog.setOnDismissListener(this);
    291         mDialog.show();
    292         return true;
    293     }
    294 
    295     @Override
    296     public boolean handleMessage(Message message) {
    297         mUpdater.removeMessages(0);
    298 
    299         if (isResumed()) {
    300             try {
    301                 LegacyVpnInfo info = mService.getLegacyVpnInfo();
    302                 if (mInfo != null) {
    303                     VpnPreference preference = mPreferences.get(mInfo.key);
    304                     if (preference != null) {
    305                         preference.update(-1);
    306                     }
    307                     mInfo = null;
    308                 }
    309                 if (info != null) {
    310                     VpnPreference preference = mPreferences.get(info.key);
    311                     if (preference != null) {
    312                         preference.update(info.state);
    313                         mInfo = info;
    314                     }
    315                 }
    316             } catch (Exception e) {
    317                 // ignore
    318             }
    319             mUpdater.sendEmptyMessageDelayed(0, 1000);
    320         }
    321         return true;
    322     }
    323 
    324     private String[] getDefaultNetwork() throws Exception {
    325         LinkProperties network = mService.getActiveLinkProperties();
    326         if (network == null) {
    327             throw new IllegalStateException("Network is not available");
    328         }
    329         String interfaze = network.getInterfaceName();
    330         if (interfaze == null) {
    331             throw new IllegalStateException("Cannot get the default interface");
    332         }
    333         String gateway = null;
    334         for (RouteInfo route : network.getRoutes()) {
    335             // Currently legacy VPN only works on IPv4.
    336             if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) {
    337                 gateway = route.getGateway().getHostAddress();
    338                 break;
    339             }
    340         }
    341         if (gateway == null) {
    342             throw new IllegalStateException("Cannot get the default gateway");
    343         }
    344         return new String[] {interfaze, gateway};
    345     }
    346 
    347     private void connect(VpnProfile profile) throws Exception {
    348         // Get the default interface and the default gateway.
    349         String[] network = getDefaultNetwork();
    350         String interfaze = network[0];
    351         String gateway = network[1];
    352 
    353         // Load certificates.
    354         String privateKey = "";
    355         String userCert = "";
    356         String caCert = "";
    357         String serverCert = "";
    358         if (!profile.ipsecUserCert.isEmpty()) {
    359             byte[] value = mKeyStore.get(Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert);
    360             privateKey = (value == null) ? null : new String(value, Charsets.UTF_8);
    361             value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert);
    362             userCert = (value == null) ? null : new String(value, Charsets.UTF_8);
    363         }
    364         if (!profile.ipsecCaCert.isEmpty()) {
    365             byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert);
    366             caCert = (value == null) ? null : new String(value, Charsets.UTF_8);
    367         }
    368         if (!profile.ipsecServerCert.isEmpty()) {
    369             byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert);
    370             serverCert = (value == null) ? null : new String(value, Charsets.UTF_8);
    371         }
    372         if (privateKey == null || userCert == null || caCert == null || serverCert == null) {
    373             // TODO: find out a proper way to handle this. Delete these keys?
    374             throw new IllegalStateException("Cannot load credentials");
    375         }
    376 
    377         // Prepare arguments for racoon.
    378         String[] racoon = null;
    379         switch (profile.type) {
    380             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
    381                 racoon = new String[] {
    382                     interfaze, profile.server, "udppsk", profile.ipsecIdentifier,
    383                     profile.ipsecSecret, "1701",
    384                 };
    385                 break;
    386             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
    387                 racoon = new String[] {
    388                     interfaze, profile.server, "udprsa", privateKey, userCert,
    389                     caCert, serverCert, "1701",
    390                 };
    391                 break;
    392             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
    393                 racoon = new String[] {
    394                     interfaze, profile.server, "xauthpsk", profile.ipsecIdentifier,
    395                     profile.ipsecSecret, profile.username, profile.password, "", gateway,
    396                 };
    397                 break;
    398             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
    399                 racoon = new String[] {
    400                     interfaze, profile.server, "xauthrsa", privateKey, userCert,
    401                     caCert, serverCert, profile.username, profile.password, "", gateway,
    402                 };
    403                 break;
    404             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
    405                 racoon = new String[] {
    406                     interfaze, profile.server, "hybridrsa",
    407                     caCert, serverCert, profile.username, profile.password, "", gateway,
    408                 };
    409                 break;
    410         }
    411 
    412         // Prepare arguments for mtpd.
    413         String[] mtpd = null;
    414         switch (profile.type) {
    415             case VpnProfile.TYPE_PPTP:
    416                 mtpd = new String[] {
    417                     interfaze, "pptp", profile.server, "1723",
    418                     "name", profile.username, "password", profile.password,
    419                     "linkname", "vpn", "refuse-eap", "nodefaultroute",
    420                     "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400",
    421                     (profile.mppe ? "+mppe" : "nomppe"),
    422                 };
    423                 break;
    424             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
    425             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
    426                 mtpd = new String[] {
    427                     interfaze, "l2tp", profile.server, "1701", profile.l2tpSecret,
    428                     "name", profile.username, "password", profile.password,
    429                     "linkname", "vpn", "refuse-eap", "nodefaultroute",
    430                     "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400",
    431                 };
    432                 break;
    433         }
    434 
    435         VpnConfig config = new VpnConfig();
    436         config.user = profile.key;
    437         config.interfaze = interfaze;
    438         config.session = profile.name;
    439         config.routes = profile.routes;
    440         if (!profile.dnsServers.isEmpty()) {
    441             config.dnsServers = Arrays.asList(profile.dnsServers.split(" +"));
    442         }
    443         if (!profile.searchDomains.isEmpty()) {
    444             config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
    445         }
    446 
    447         mService.startLegacyVpn(config, racoon, mtpd);
    448     }
    449 
    450     private void disconnect(String key) {
    451         if (mInfo != null && key.equals(mInfo.key)) {
    452             try {
    453                 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
    454             } catch (Exception e) {
    455                 // ignore
    456             }
    457         }
    458     }
    459 
    460     private class VpnPreference extends Preference {
    461         private VpnProfile mProfile;
    462         private int mState = -1;
    463 
    464         VpnPreference(Context context, VpnProfile profile) {
    465             super(context);
    466             setPersistent(false);
    467             setOrder(0);
    468             setOnPreferenceClickListener(VpnSettings.this);
    469 
    470             mProfile = profile;
    471             update();
    472         }
    473 
    474         VpnProfile getProfile() {
    475             return mProfile;
    476         }
    477 
    478         void update(VpnProfile profile) {
    479             mProfile = profile;
    480             update();
    481         }
    482 
    483         void update(int state) {
    484             mState = state;
    485             update();
    486         }
    487 
    488         void update() {
    489             if (mState < 0) {
    490                 String[] types = getContext().getResources()
    491                         .getStringArray(R.array.vpn_types_long);
    492                 setSummary(types[mProfile.type]);
    493             } else {
    494                 String[] states = getContext().getResources()
    495                         .getStringArray(R.array.vpn_states);
    496                 setSummary(states[mState]);
    497             }
    498             setTitle(mProfile.name);
    499             notifyHierarchyChanged();
    500         }
    501 
    502         @Override
    503         public int compareTo(Preference preference) {
    504             int result = -1;
    505             if (preference instanceof VpnPreference) {
    506                 VpnPreference another = (VpnPreference) preference;
    507                 if ((result = another.mState - mState) == 0 &&
    508                         (result = mProfile.name.compareTo(another.mProfile.name)) == 0 &&
    509                         (result = mProfile.type - another.mProfile.type) == 0) {
    510                     result = mProfile.key.compareTo(another.mProfile.key);
    511                 }
    512             }
    513             return result;
    514         }
    515     }
    516 }
    517