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