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