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.internal.net.VpnProfile;
20import com.android.settings.R;
21
22import android.app.AlertDialog;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.os.Bundle;
26import android.security.Credentials;
27import android.security.KeyStore;
28import android.text.Editable;
29import android.text.TextWatcher;
30import android.view.View;
31import android.view.WindowManager;
32import android.widget.AdapterView;
33import android.widget.ArrayAdapter;
34import android.widget.Button;
35import android.widget.CheckBox;
36import android.widget.Spinner;
37import android.widget.TextView;
38
39import java.net.InetAddress;
40
41class VpnDialog extends AlertDialog implements TextWatcher,
42        View.OnClickListener, AdapterView.OnItemSelectedListener {
43    private final KeyStore mKeyStore = KeyStore.getInstance();
44    private final DialogInterface.OnClickListener mListener;
45    private final VpnProfile mProfile;
46
47    private boolean mEditing;
48
49    private View mView;
50
51    private TextView mName;
52    private Spinner mType;
53    private TextView mServer;
54    private TextView mUsername;
55    private TextView mPassword;
56    private TextView mSearchDomains;
57    private TextView mDnsServers;
58    private TextView mRoutes;
59    private CheckBox mMppe;
60    private TextView mL2tpSecret;
61    private TextView mIpsecIdentifier;
62    private TextView mIpsecSecret;
63    private Spinner mIpsecUserCert;
64    private Spinner mIpsecCaCert;
65    private Spinner mIpsecServerCert;
66    private CheckBox mSaveLogin;
67
68    VpnDialog(Context context, DialogInterface.OnClickListener listener,
69            VpnProfile profile, boolean editing) {
70        super(context);
71        mListener = listener;
72        mProfile = profile;
73        mEditing = editing;
74    }
75
76    @Override
77    protected void onCreate(Bundle savedState) {
78        mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
79        setView(mView);
80        setInverseBackgroundForced(true);
81
82        Context context = getContext();
83
84        // First, find out all the fields.
85        mName = (TextView) mView.findViewById(R.id.name);
86        mType = (Spinner) mView.findViewById(R.id.type);
87        mServer = (TextView) mView.findViewById(R.id.server);
88        mUsername = (TextView) mView.findViewById(R.id.username);
89        mPassword = (TextView) mView.findViewById(R.id.password);
90        mSearchDomains = (TextView) mView.findViewById(R.id.search_domains);
91        mDnsServers = (TextView) mView.findViewById(R.id.dns_servers);
92        mRoutes = (TextView) mView.findViewById(R.id.routes);
93        mMppe = (CheckBox) mView.findViewById(R.id.mppe);
94        mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret);
95        mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
96        mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
97        mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
98        mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
99        mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
100        mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
101
102        // Second, copy values from the profile.
103        mName.setText(mProfile.name);
104        mType.setSelection(mProfile.type);
105        mServer.setText(mProfile.server);
106        if (mProfile.saveLogin) {
107            mUsername.setText(mProfile.username);
108            mPassword.setText(mProfile.password);
109        }
110        mSearchDomains.setText(mProfile.searchDomains);
111        mDnsServers.setText(mProfile.dnsServers);
112        mRoutes.setText(mProfile.routes);
113        mMppe.setChecked(mProfile.mppe);
114        mL2tpSecret.setText(mProfile.l2tpSecret);
115        mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
116        mIpsecSecret.setText(mProfile.ipsecSecret);
117        loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY,
118                0, mProfile.ipsecUserCert);
119        loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE,
120                R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
121        loadCertificates(mIpsecServerCert, Credentials.USER_CERTIFICATE,
122                R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
123        mSaveLogin.setChecked(mProfile.saveLogin);
124
125        // Third, add listeners to required fields.
126        mName.addTextChangedListener(this);
127        mType.setOnItemSelectedListener(this);
128        mServer.addTextChangedListener(this);
129        mUsername.addTextChangedListener(this);
130        mPassword.addTextChangedListener(this);
131        mDnsServers.addTextChangedListener(this);
132        mRoutes.addTextChangedListener(this);
133        mIpsecSecret.addTextChangedListener(this);
134        mIpsecUserCert.setOnItemSelectedListener(this);
135
136        // Forth, determine to do editing or connecting.
137        boolean valid = validate(true);
138        mEditing = mEditing || !valid;
139
140        if (mEditing) {
141            setTitle(R.string.vpn_edit);
142
143            // Show common fields.
144            mView.findViewById(R.id.editor).setVisibility(View.VISIBLE);
145
146            // Show type-specific fields.
147            changeType(mProfile.type);
148
149            // Show advanced options directly if any of them is set.
150            View showOptions = mView.findViewById(R.id.show_options);
151            if (mProfile.searchDomains.isEmpty() && mProfile.dnsServers.isEmpty() &&
152                    mProfile.routes.isEmpty()) {
153                showOptions.setOnClickListener(this);
154            } else {
155                onClick(showOptions);
156            }
157
158            // Create a button to save the profile.
159            setButton(DialogInterface.BUTTON_POSITIVE,
160                    context.getString(R.string.vpn_save), mListener);
161        } else {
162            setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
163
164            // Not editing, just show username and password.
165            mView.findViewById(R.id.login).setVisibility(View.VISIBLE);
166
167            // Create a button to connect the network.
168            setButton(DialogInterface.BUTTON_POSITIVE,
169                    context.getString(R.string.vpn_connect), mListener);
170        }
171
172        // Always provide a cancel button.
173        setButton(DialogInterface.BUTTON_NEGATIVE,
174                context.getString(R.string.vpn_cancel), mListener);
175
176        // Let AlertDialog create everything.
177        super.onCreate(null);
178
179        // Disable the action button if necessary.
180        getButton(DialogInterface.BUTTON_POSITIVE)
181                .setEnabled(mEditing ? valid : validate(false));
182
183        // Workaround to resize the dialog for the input method.
184        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
185                WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
186    }
187
188    @Override
189    public void afterTextChanged(Editable field) {
190        getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
191    }
192
193    @Override
194    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
195    }
196
197    @Override
198    public void onTextChanged(CharSequence s, int start, int before, int count) {
199    }
200
201    @Override
202    public void onClick(View showOptions) {
203        showOptions.setVisibility(View.GONE);
204        mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
205    }
206
207    @Override
208    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
209        if (parent == mType) {
210            changeType(position);
211        }
212        getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
213    }
214
215    @Override
216    public void onNothingSelected(AdapterView<?> parent) {
217    }
218
219    private void changeType(int type) {
220        // First, hide everything.
221        mMppe.setVisibility(View.GONE);
222        mView.findViewById(R.id.l2tp).setVisibility(View.GONE);
223        mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
224        mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
225        mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
226
227        // Then, unhide type-specific fields.
228        switch (type) {
229            case VpnProfile.TYPE_PPTP:
230                mMppe.setVisibility(View.VISIBLE);
231                break;
232
233            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
234                mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
235                // fall through
236            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
237                mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
238                break;
239
240            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
241                mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
242                // fall through
243            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
244                mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
245                // fall through
246            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
247                mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
248                break;
249        }
250    }
251
252    private boolean validate(boolean editing) {
253        if (!editing) {
254            return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
255        }
256        if (mName.getText().length() == 0 || mServer.getText().length() == 0 ||
257                !validateAddresses(mDnsServers.getText().toString(), false) ||
258                !validateAddresses(mRoutes.getText().toString(), true)) {
259            return false;
260        }
261        switch (mType.getSelectedItemPosition()) {
262            case VpnProfile.TYPE_PPTP:
263            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
264                return true;
265
266            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
267            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
268                return mIpsecSecret.getText().length() != 0;
269
270            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
271            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
272                return mIpsecUserCert.getSelectedItemPosition() != 0;
273        }
274        return false;
275    }
276
277    private boolean validateAddresses(String addresses, boolean cidr) {
278        try {
279            for (String address : addresses.split(" ")) {
280                if (address.isEmpty()) {
281                    continue;
282                }
283                // Legacy VPN currently only supports IPv4.
284                int prefixLength = 32;
285                if (cidr) {
286                    String[] parts = address.split("/", 2);
287                    address = parts[0];
288                    prefixLength = Integer.parseInt(parts[1]);
289                }
290                byte[] bytes = InetAddress.parseNumericAddress(address).getAddress();
291                int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 |
292                        (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24;
293                if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 ||
294                        (prefixLength < 32 && (integer << prefixLength) != 0)) {
295                    return false;
296                }
297            }
298        } catch (Exception e) {
299            return false;
300        }
301        return true;
302    }
303
304    private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) {
305        Context context = getContext();
306        String first = (firstId == 0) ? "" : context.getString(firstId);
307        String[] certificates = mKeyStore.saw(prefix);
308
309        if (certificates == null || certificates.length == 0) {
310            certificates = new String[] {first};
311        } else {
312            String[] array = new String[certificates.length + 1];
313            array[0] = first;
314            System.arraycopy(certificates, 0, array, 1, certificates.length);
315            certificates = array;
316        }
317
318        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
319                context, android.R.layout.simple_spinner_item, certificates);
320        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
321        spinner.setAdapter(adapter);
322
323        for (int i = 1; i < certificates.length; ++i) {
324            if (certificates[i].equals(selected)) {
325                spinner.setSelection(i);
326                break;
327            }
328        }
329    }
330
331    boolean isEditing() {
332        return mEditing;
333    }
334
335    VpnProfile getProfile() {
336        // First, save common fields.
337        VpnProfile profile = new VpnProfile(mProfile.key);
338        profile.name = mName.getText().toString();
339        profile.type = mType.getSelectedItemPosition();
340        profile.server = mServer.getText().toString().trim();
341        profile.username = mUsername.getText().toString();
342        profile.password = mPassword.getText().toString();
343        profile.searchDomains = mSearchDomains.getText().toString().trim();
344        profile.dnsServers = mDnsServers.getText().toString().trim();
345        profile.routes = mRoutes.getText().toString().trim();
346
347        // Then, save type-specific fields.
348        switch (profile.type) {
349            case VpnProfile.TYPE_PPTP:
350                profile.mppe = mMppe.isChecked();
351                break;
352
353            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
354                profile.l2tpSecret = mL2tpSecret.getText().toString();
355                // fall through
356            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
357                profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
358                profile.ipsecSecret = mIpsecSecret.getText().toString();
359                break;
360
361            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
362                profile.l2tpSecret = mL2tpSecret.getText().toString();
363                // fall through
364            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
365                if (mIpsecUserCert.getSelectedItemPosition() != 0) {
366                    profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
367                }
368                // fall through
369            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
370                if (mIpsecCaCert.getSelectedItemPosition() != 0) {
371                    profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
372                }
373                if (mIpsecServerCert.getSelectedItemPosition() != 0) {
374                    profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
375                }
376                break;
377        }
378
379        profile.saveLogin = mSaveLogin.isChecked();
380        return profile;
381    }
382}
383