• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.Context;
20 import android.content.DialogInterface;
21 import android.net.Proxy;
22 import android.net.ProxyInfo;
23 import android.os.Bundle;
24 import android.os.SystemProperties;
25 import android.security.Credentials;
26 import android.security.KeyStore;
27 import android.text.Editable;
28 import android.text.TextWatcher;
29 import android.view.View;
30 import android.view.WindowManager;
31 import android.widget.AdapterView;
32 import android.widget.ArrayAdapter;
33 import android.widget.CheckBox;
34 import android.widget.CompoundButton;
35 import android.widget.Spinner;
36 import android.widget.TextView;
37 
38 import androidx.appcompat.app.AlertDialog;
39 
40 import com.android.internal.net.VpnProfile;
41 import com.android.settings.R;
42 
43 import java.net.InetAddress;
44 
45 /**
46  * Dialog showing information about a VPN configuration. The dialog
47  * can be launched to either edit or prompt for credentials to connect
48  * to a user-added VPN.
49  *
50  * {@see AppDialog}
51  */
52 class ConfigDialog extends AlertDialog implements TextWatcher,
53         View.OnClickListener, AdapterView.OnItemSelectedListener,
54         CompoundButton.OnCheckedChangeListener {
55     private final KeyStore mKeyStore = KeyStore.getInstance();
56     private final DialogInterface.OnClickListener mListener;
57     private final VpnProfile mProfile;
58 
59     private boolean mEditing;
60     private boolean mExists;
61 
62     private View mView;
63 
64     private TextView mName;
65     private Spinner mType;
66     private TextView mServer;
67     private TextView mUsername;
68     private TextView mPassword;
69     private TextView mSearchDomains;
70     private TextView mDnsServers;
71     private TextView mRoutes;
72     private Spinner mProxySettings;
73     private TextView mProxyHost;
74     private TextView mProxyPort;
75     private CheckBox mMppe;
76     private TextView mL2tpSecret;
77     private TextView mIpsecIdentifier;
78     private TextView mIpsecSecret;
79     private Spinner mIpsecUserCert;
80     private Spinner mIpsecCaCert;
81     private Spinner mIpsecServerCert;
82     private CheckBox mSaveLogin;
83     private CheckBox mShowOptions;
84     private CheckBox mAlwaysOnVpn;
85     private TextView mAlwaysOnInvalidReason;
86 
ConfigDialog(Context context, DialogInterface.OnClickListener listener, VpnProfile profile, boolean editing, boolean exists)87     ConfigDialog(Context context, DialogInterface.OnClickListener listener,
88             VpnProfile profile, boolean editing, boolean exists) {
89         super(context);
90 
91         mListener = listener;
92         mProfile = profile;
93         mEditing = editing;
94         mExists = exists;
95     }
96 
97     @Override
onCreate(Bundle savedState)98     protected void onCreate(Bundle savedState) {
99         mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
100         setView(mView);
101 
102         Context context = getContext();
103 
104         // First, find out all the fields.
105         mName = (TextView) mView.findViewById(R.id.name);
106         mType = (Spinner) mView.findViewById(R.id.type);
107         mServer = (TextView) mView.findViewById(R.id.server);
108         mUsername = (TextView) mView.findViewById(R.id.username);
109         mPassword = (TextView) mView.findViewById(R.id.password);
110         mSearchDomains = (TextView) mView.findViewById(R.id.search_domains);
111         mDnsServers = (TextView) mView.findViewById(R.id.dns_servers);
112         mRoutes = (TextView) mView.findViewById(R.id.routes);
113         mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings);
114         mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host);
115         mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port);
116         mMppe = (CheckBox) mView.findViewById(R.id.mppe);
117         mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret);
118         mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
119         mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
120         mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
121         mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
122         mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
123         mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
124         mShowOptions = (CheckBox) mView.findViewById(R.id.show_options);
125         mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn);
126         mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason);
127 
128         // Second, copy values from the profile.
129         mName.setText(mProfile.name);
130         mType.setSelection(mProfile.type);
131         mServer.setText(mProfile.server);
132         if (mProfile.saveLogin) {
133             mUsername.setText(mProfile.username);
134             mPassword.setText(mProfile.password);
135         }
136         mSearchDomains.setText(mProfile.searchDomains);
137         mDnsServers.setText(mProfile.dnsServers);
138         mRoutes.setText(mProfile.routes);
139         if (mProfile.proxy != null) {
140             mProxyHost.setText(mProfile.proxy.getHost());
141             int port = mProfile.proxy.getPort();
142             mProxyPort.setText(port == 0 ? "" : Integer.toString(port));
143         }
144         mMppe.setChecked(mProfile.mppe);
145         mL2tpSecret.setText(mProfile.l2tpSecret);
146         mL2tpSecret.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
147         mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
148         mIpsecSecret.setText(mProfile.ipsecSecret);
149         loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY, 0, mProfile.ipsecUserCert);
150         loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE,
151                 R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
152         loadCertificates(mIpsecServerCert, Credentials.USER_CERTIFICATE,
153                 R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
154         mSaveLogin.setChecked(mProfile.saveLogin);
155         mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
156         mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
157 
158         // Hide lockdown VPN on devices that require IMS authentication
159         if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
160             mAlwaysOnVpn.setVisibility(View.GONE);
161         }
162 
163         // Third, add listeners to required fields.
164         mName.addTextChangedListener(this);
165         mType.setOnItemSelectedListener(this);
166         mServer.addTextChangedListener(this);
167         mUsername.addTextChangedListener(this);
168         mPassword.addTextChangedListener(this);
169         mDnsServers.addTextChangedListener(this);
170         mRoutes.addTextChangedListener(this);
171         mProxySettings.setOnItemSelectedListener(this);
172         mProxyHost.addTextChangedListener(this);
173         mProxyPort.addTextChangedListener(this);
174         mIpsecSecret.addTextChangedListener(this);
175         mIpsecUserCert.setOnItemSelectedListener(this);
176         mShowOptions.setOnClickListener(this);
177         mAlwaysOnVpn.setOnCheckedChangeListener(this);
178 
179         // Fourth, determine whether to do editing or connecting.
180         mEditing = mEditing || !validate(true /*editing*/);
181 
182         if (mEditing) {
183             setTitle(R.string.vpn_edit);
184 
185             // Show common fields.
186             mView.findViewById(R.id.editor).setVisibility(View.VISIBLE);
187 
188             // Show type-specific fields.
189             changeType(mProfile.type);
190 
191             // Hide 'save login' when we are editing.
192             mSaveLogin.setVisibility(View.GONE);
193 
194             // Switch to advanced view immediately if any advanced options are on
195             if (!mProfile.searchDomains.isEmpty() || !mProfile.dnsServers.isEmpty() ||
196                     !mProfile.routes.isEmpty() || (mProfile.proxy != null &&
197                     (!mProfile.proxy.getHost().isEmpty() || mProfile.proxy.getPort() != 0))) {
198                 showAdvancedOptions();
199             }
200 
201             // Create a button to forget the profile if it has already been saved..
202             if (mExists) {
203                 setButton(DialogInterface.BUTTON_NEUTRAL,
204                         context.getString(R.string.vpn_forget), mListener);
205             }
206 
207             // Create a button to save the profile.
208             setButton(DialogInterface.BUTTON_POSITIVE,
209                     context.getString(R.string.vpn_save), mListener);
210         } else {
211             setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
212 
213             // Create a button to connect the network.
214             setButton(DialogInterface.BUTTON_POSITIVE,
215                     context.getString(R.string.vpn_connect), mListener);
216         }
217 
218         // Always provide a cancel button.
219         setButton(DialogInterface.BUTTON_NEGATIVE,
220                 context.getString(R.string.vpn_cancel), mListener);
221 
222         // Let AlertDialog create everything.
223         super.onCreate(savedState);
224 
225         // Update UI controls according to the current configuration.
226         updateUiControls();
227 
228         // Workaround to resize the dialog for the input method.
229         getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
230                 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
231     }
232 
233     @Override
onRestoreInstanceState(Bundle savedState)234     public void onRestoreInstanceState(Bundle savedState) {
235         super.onRestoreInstanceState(savedState);
236 
237         // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced
238         // options here if they were already revealed or set.
239         if (mShowOptions.isChecked()) {
240             showAdvancedOptions();
241         }
242     }
243 
244     @Override
afterTextChanged(Editable field)245     public void afterTextChanged(Editable field) {
246         updateUiControls();
247     }
248 
249     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)250     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
251     }
252 
253     @Override
onTextChanged(CharSequence s, int start, int before, int count)254     public void onTextChanged(CharSequence s, int start, int before, int count) {
255     }
256 
257     @Override
onClick(View view)258     public void onClick(View view) {
259         if (view == mShowOptions) {
260             showAdvancedOptions();
261         }
262     }
263 
264     @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)265     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
266         if (parent == mType) {
267             changeType(position);
268         } else if (parent == mProxySettings) {
269             updateProxyFieldsVisibility(position);
270         }
271         updateUiControls();
272     }
273 
274     @Override
onNothingSelected(AdapterView<?> parent)275     public void onNothingSelected(AdapterView<?> parent) {
276     }
277 
278     @Override
onCheckedChanged(CompoundButton compoundButton, boolean b)279     public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
280         if (compoundButton == mAlwaysOnVpn) {
281             updateUiControls();
282         }
283     }
284 
isVpnAlwaysOn()285     public boolean isVpnAlwaysOn() {
286         return mAlwaysOnVpn.isChecked();
287     }
288 
289     /**
290      * Updates the UI according to the current configuration entered by the user.
291      *
292      * These include:
293      * "Always-on VPN" checkbox
294      * Reason for "Always-on VPN" being disabled, when necessary
295      * Proxy info if manually configured
296      * "Save account information" checkbox
297      * "Save" and "Connect" buttons
298      */
updateUiControls()299     private void updateUiControls() {
300         VpnProfile profile = getProfile();
301 
302         // Always-on VPN
303         if (profile.isValidLockdownProfile()) {
304             mAlwaysOnVpn.setEnabled(true);
305             mAlwaysOnInvalidReason.setVisibility(View.GONE);
306         } else {
307             mAlwaysOnVpn.setChecked(false);
308             mAlwaysOnVpn.setEnabled(false);
309             if (!profile.isTypeValidForLockdown()) {
310                 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_type);
311             } else if (!profile.isServerAddressNumeric()) {
312                 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_server);
313             } else if (!profile.hasDns()) {
314                 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_no_dns);
315             } else if (!profile.areDnsAddressesNumeric()) {
316                 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_dns);
317             } else {
318                 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_other);
319             }
320             mAlwaysOnInvalidReason.setVisibility(View.VISIBLE);
321         }
322 
323         // Show proxy fields if any proxy field is filled.
324         if (mProfile.proxy != null && (!mProfile.proxy.getHost().isEmpty() ||
325                 mProfile.proxy.getPort() != 0)) {
326             mProxySettings.setSelection(VpnProfile.PROXY_MANUAL);
327             updateProxyFieldsVisibility(VpnProfile.PROXY_MANUAL);
328         }
329 
330         // Save account information
331         if (mAlwaysOnVpn.isChecked()) {
332             mSaveLogin.setChecked(true);
333             mSaveLogin.setEnabled(false);
334         } else {
335             mSaveLogin.setChecked(mProfile.saveLogin);
336             mSaveLogin.setEnabled(true);
337         }
338 
339         // Save or Connect button
340         getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
341     }
342 
updateProxyFieldsVisibility(int position)343     private void updateProxyFieldsVisibility(int position) {
344         final int visible = position == VpnProfile.PROXY_MANUAL ? View.VISIBLE : View.GONE;
345         mView.findViewById(R.id.vpn_proxy_fields).setVisibility(visible);
346     }
347 
showAdvancedOptions()348     private void showAdvancedOptions() {
349         mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
350         mShowOptions.setVisibility(View.GONE);
351     }
352 
changeType(int type)353     private void changeType(int type) {
354         // First, hide everything.
355         mMppe.setVisibility(View.GONE);
356         mView.findViewById(R.id.l2tp).setVisibility(View.GONE);
357         mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
358         mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
359         mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
360 
361         // Then, unhide type-specific fields.
362         switch (type) {
363             case VpnProfile.TYPE_PPTP:
364                 mMppe.setVisibility(View.VISIBLE);
365                 break;
366 
367             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
368                 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
369                 // fall through
370             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
371                 mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
372                 break;
373 
374             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
375                 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
376                 // fall through
377             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
378                 mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
379                 // fall through
380             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
381                 mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
382                 break;
383         }
384     }
385 
validate(boolean editing)386     private boolean validate(boolean editing) {
387         if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) {
388             return false;
389         }
390         if (!editing) {
391             return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
392         }
393         if (mName.getText().length() == 0 || mServer.getText().length() == 0 ||
394                 !validateAddresses(mDnsServers.getText().toString(), false) ||
395                 !validateAddresses(mRoutes.getText().toString(), true)) {
396             return false;
397         }
398 
399         if (!validateProxy()) {
400             return false;
401         }
402 
403         switch (mType.getSelectedItemPosition()) {
404             case VpnProfile.TYPE_PPTP:
405             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
406                 return true;
407 
408             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
409             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
410                 return mIpsecSecret.getText().length() != 0;
411 
412             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
413             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
414                 return mIpsecUserCert.getSelectedItemPosition() != 0;
415         }
416         return false;
417     }
418 
validateAddresses(String addresses, boolean cidr)419     private boolean validateAddresses(String addresses, boolean cidr) {
420         try {
421             for (String address : addresses.split(" ")) {
422                 if (address.isEmpty()) {
423                     continue;
424                 }
425                 // Legacy VPN currently only supports IPv4.
426                 int prefixLength = 32;
427                 if (cidr) {
428                     String[] parts = address.split("/", 2);
429                     address = parts[0];
430                     prefixLength = Integer.parseInt(parts[1]);
431                 }
432                 byte[] bytes = InetAddress.parseNumericAddress(address).getAddress();
433                 int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 |
434                         (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24;
435                 if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 ||
436                         (prefixLength < 32 && (integer << prefixLength) != 0)) {
437                     return false;
438                 }
439             }
440         } catch (Exception e) {
441             return false;
442         }
443         return true;
444     }
445 
loadCertificates(Spinner spinner, String prefix, int firstId, String selected)446     private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) {
447         Context context = getContext();
448         String first = (firstId == 0) ? "" : context.getString(firstId);
449         String[] certificates = mKeyStore.list(prefix);
450 
451         if (certificates == null || certificates.length == 0) {
452             certificates = new String[] {first};
453         } else {
454             String[] array = new String[certificates.length + 1];
455             array[0] = first;
456             System.arraycopy(certificates, 0, array, 1, certificates.length);
457             certificates = array;
458         }
459 
460         ArrayAdapter<String> adapter = new ArrayAdapter<String>(
461                 context, android.R.layout.simple_spinner_item, certificates);
462         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
463         spinner.setAdapter(adapter);
464 
465         for (int i = 1; i < certificates.length; ++i) {
466             if (certificates[i].equals(selected)) {
467                 spinner.setSelection(i);
468                 break;
469             }
470         }
471     }
472 
isEditing()473     boolean isEditing() {
474         return mEditing;
475     }
476 
hasProxy()477     boolean hasProxy() {
478         return mProxySettings.getSelectedItemPosition() == VpnProfile.PROXY_MANUAL;
479     }
480 
getProfile()481     VpnProfile getProfile() {
482         // First, save common fields.
483         VpnProfile profile = new VpnProfile(mProfile.key);
484         profile.name = mName.getText().toString();
485         profile.type = mType.getSelectedItemPosition();
486         profile.server = mServer.getText().toString().trim();
487         profile.username = mUsername.getText().toString();
488         profile.password = mPassword.getText().toString();
489         profile.searchDomains = mSearchDomains.getText().toString().trim();
490         profile.dnsServers = mDnsServers.getText().toString().trim();
491         profile.routes = mRoutes.getText().toString().trim();
492         if (hasProxy()) {
493             String proxyHost = mProxyHost.getText().toString().trim();
494             String proxyPort = mProxyPort.getText().toString().trim();
495             // 0 is a last resort default, but the interface validates that the proxy port is
496             // present and non-zero.
497             int port = proxyPort.isEmpty() ? 0 : Integer.parseInt(proxyPort);
498             profile.proxy = new ProxyInfo(proxyHost, port, null);
499         } else {
500             profile.proxy = null;
501         }
502         // Then, save type-specific fields.
503         switch (profile.type) {
504             case VpnProfile.TYPE_PPTP:
505                 profile.mppe = mMppe.isChecked();
506                 break;
507 
508             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
509                 profile.l2tpSecret = mL2tpSecret.getText().toString();
510                 // fall through
511             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
512                 profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
513                 profile.ipsecSecret = mIpsecSecret.getText().toString();
514                 break;
515 
516             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
517                 profile.l2tpSecret = mL2tpSecret.getText().toString();
518                 // fall through
519             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
520                 if (mIpsecUserCert.getSelectedItemPosition() != 0) {
521                     profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
522                 }
523                 // fall through
524             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
525                 if (mIpsecCaCert.getSelectedItemPosition() != 0) {
526                     profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
527                 }
528                 if (mIpsecServerCert.getSelectedItemPosition() != 0) {
529                     profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
530                 }
531                 break;
532         }
533 
534         final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
535         profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin);
536         return profile;
537     }
538 
validateProxy()539     private boolean validateProxy() {
540         if (!hasProxy()) {
541             return true;
542         }
543 
544         final String host = mProxyHost.getText().toString().trim();
545         final String port = mProxyPort.getText().toString().trim();
546         return Proxy.validate(host, port, "") == Proxy.PROXY_VALID;
547     }
548 
549 }
550