diff options
Diffstat (limited to 'packages/VpnServices')
17 files changed, 1659 insertions, 0 deletions
diff --git a/packages/VpnServices/Android.mk b/packages/VpnServices/Android.mk new file mode 100644 index 0000000..eb27ed5 --- /dev/null +++ b/packages/VpnServices/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAVA_LIBRARIES := + +LOCAL_PACKAGE_NAME := VpnServices +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) + +######################## +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/packages/VpnServices/AndroidManifest.xml b/packages/VpnServices/AndroidManifest.xml new file mode 100644 index 0000000..6092e30 --- /dev/null +++ b/packages/VpnServices/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.vpn" + android:sharedUserId="android.uid.system" + > + <application android:label="@string/app_label"> + + <service android:name=".VpnServiceBinder" android:process=":remote"> + <intent-filter> + <!-- These are the interfaces supported by the service, which + you can bind to. --> + <action android:name="android.net.vpn.IVpnService" /> + <!-- This is an action code you can use to select the service + without explicitly supplying the implementation class. --> + <action android:name="android.net.vpn.SERVICE" /> + </intent-filter> + </service> + + </application> + + <uses-permission android:name="android.permission.INTERNET"></uses-permission> +</manifest> diff --git a/packages/VpnServices/MODULE_LICENSE_APACHE2 b/packages/VpnServices/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/VpnServices/MODULE_LICENSE_APACHE2 diff --git a/packages/VpnServices/NOTICE b/packages/VpnServices/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/packages/VpnServices/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/VpnServices/res/drawable/vpn_connected.png b/packages/VpnServices/res/drawable/vpn_connected.png Binary files differnew file mode 100644 index 0000000..65fc6db --- /dev/null +++ b/packages/VpnServices/res/drawable/vpn_connected.png diff --git a/packages/VpnServices/res/drawable/vpn_disconnected.png b/packages/VpnServices/res/drawable/vpn_disconnected.png Binary files differnew file mode 100644 index 0000000..2440c69 --- /dev/null +++ b/packages/VpnServices/res/drawable/vpn_disconnected.png diff --git a/packages/VpnServices/res/values/strings.xml b/packages/VpnServices/res/values/strings.xml new file mode 100755 index 0000000..074655e --- /dev/null +++ b/packages/VpnServices/res/values/strings.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Title for the VPN Services activity. --> + <string name="app_label">VPN Services</string> + + <string name="vpn_notification_title_connected">%s VPN connected</string> + <string name="vpn_notification_title_disconnected">%s VPN disconnected</string> + <string name="vpn_notification_hint_disconnected">Touch to reconnect to a VPN.</string> +</resources> + diff --git a/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java b/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java new file mode 100644 index 0000000..7dd9d9e --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.SystemProperties; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Proxy to start, stop and interact with an Android service defined in init.rc. + * The android service is expected to accept connection through Unix domain + * socket. When the proxy successfully starts the service, it will establish a + * socket connection with the service. The socket serves two purposes: (1) send + * commands to the service; (2) for the proxy to know whether the service is + * alive. + * + * After the service receives commands from the proxy, it should return either + * 0 if the service will close the socket (and the proxy will re-establish + * another connection immediately after), or 1 if the socket is remained alive. + */ +public class AndroidServiceProxy extends ProcessProxy { + private static final int WAITING_TIME = 15; // sec + + private static final String SVC_STATE_CMD_PREFIX = "init.svc."; + private static final String SVC_START_CMD = "ctl.start"; + private static final String SVC_STOP_CMD = "ctl.stop"; + private static final String SVC_STATE_RUNNING = "running"; + private static final String SVC_STATE_STOPPED = "stopped"; + + private static final int END_OF_ARGUMENTS = 255; + + private String mServiceName; + private String mSocketName; + private LocalSocket mKeepaliveSocket; + private boolean mControlSocketInUse; + private Integer mSocketResult = null; + private String mTag; + + /** + * Creates a proxy with the service name. + * @param serviceName the service name + */ + public AndroidServiceProxy(String serviceName) { + mServiceName = serviceName; + mSocketName = serviceName; + mTag = "SProxy_" + serviceName; + } + + @Override + public String getName() { + return "Service " + mServiceName; + } + + @Override + public synchronized void stop() { + if (isRunning()) setResultAndCloseControlSocket(-1); + SystemProperties.set(SVC_STOP_CMD, mServiceName); + } + + /** + * Sends a command with arguments to the service through the control socket. + */ + public void sendCommand(String ...args) throws IOException { + OutputStream out = getControlSocketOutput(); + for (String arg : args) outputString(out, arg); + out.write(END_OF_ARGUMENTS); + out.flush(); + checkSocketResult(); + } + + /** + * {@inheritDoc} + * The method returns when the service exits. + */ + @Override + protected void performTask() throws IOException { + String svc = mServiceName; + Log.d(mTag, "+++++ Execute: " + svc); + SystemProperties.set(SVC_START_CMD, svc); + + boolean success = blockUntil(SVC_STATE_RUNNING, WAITING_TIME); + + if (success) { + Log.d(mTag, "----- Running: " + svc + ", create keepalive socket"); + LocalSocket s = mKeepaliveSocket = createServiceSocket(); + setState(ProcessState.RUNNING); + + if (s == null) { + // no socket connection, stop hosting the service + stop(); + return; + } + try { + for (;;) { + InputStream in = s.getInputStream(); + int data = in.read(); + if (data >= 0) { + Log.d(mTag, "got data from keepalive socket: " + data); + + if (data == 0) { + // re-establish the connection: + // synchronized here so that checkSocketResult() + // returns when new mKeepaliveSocket is available for + // next cmd + synchronized (this) { + setResultAndCloseControlSocket((byte) data); + s = mKeepaliveSocket = createServiceSocket(); + } + } else { + // keep the socket + setSocketResult(data); + } + } else { + // service is gone + if (mControlSocketInUse) setSocketResult(-1); + break; + } + } + Log.d(mTag, "keepalive connection closed"); + } catch (IOException e) { + Log.d(mTag, "keepalive socket broken: " + e.getMessage()); + } + + // Wait 5 seconds for the service to exit + success = blockUntil(SVC_STATE_STOPPED, 5); + Log.d(mTag, "stopping " + svc + ", success? " + success); + } else { + setState(ProcessState.STOPPED); + throw new IOException("cannot start service: " + svc); + } + } + + private LocalSocket createServiceSocket() throws IOException { + LocalSocket s = new LocalSocket(); + LocalSocketAddress a = new LocalSocketAddress(mSocketName, + LocalSocketAddress.Namespace.RESERVED); + + // try a few times in case the service has not listen()ed + IOException excp = null; + for (int i = 0; i < 10; i++) { + try { + s.connect(a); + return s; + } catch (IOException e) { + Log.d(mTag, "service not yet listen()ing; try again"); + excp = e; + sleep(500); + } + } + throw excp; + } + + private OutputStream getControlSocketOutput() throws IOException { + if (mKeepaliveSocket != null) { + mControlSocketInUse = true; + mSocketResult = null; + return mKeepaliveSocket.getOutputStream(); + } else { + throw new IOException("no control socket available"); + } + } + + private synchronized void checkSocketResult() throws IOException { + try { + // will be notified when the result comes back from service + if (mSocketResult == null) wait(); + } catch (InterruptedException e) { + Log.d(mTag, "checkSocketResult(): " + e); + } finally { + mControlSocketInUse = false; + if ((mSocketResult == null) || (mSocketResult < 0)) { + throw new IOException("socket error, result from service: " + + mSocketResult); + } + } + } + + private synchronized void setSocketResult(int result) { + if (mControlSocketInUse) { + mSocketResult = result; + notifyAll(); + } + } + + private void setResultAndCloseControlSocket(int result) { + setSocketResult(result); + try { + mKeepaliveSocket.shutdownInput(); + mKeepaliveSocket.shutdownOutput(); + mKeepaliveSocket.close(); + } catch (IOException e) { + Log.e(mTag, "close keepalive socket", e); + } finally { + mKeepaliveSocket = null; + } + } + + /** + * Waits for the process to be in the expected state. The method returns + * false if after the specified duration (in seconds), the process is still + * not in the expected state. + */ + private boolean blockUntil(String expectedState, int waitTime) { + String cmd = SVC_STATE_CMD_PREFIX + mServiceName; + int sleepTime = 200; // ms + int n = waitTime * 1000 / sleepTime; + for (int i = 0; i < n; i++) { + if (expectedState.equals(SystemProperties.get(cmd))) { + Log.d(mTag, mServiceName + " is " + expectedState + " after " + + (i * sleepTime) + " msec"); + break; + } + sleep(sleepTime); + } + return expectedState.equals(SystemProperties.get(cmd)); + } + + private void outputString(OutputStream out, String s) throws IOException { + byte[] bytes = s.getBytes(); + out.write(bytes.length); + out.write(bytes); + out.flush(); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java new file mode 100644 index 0000000..6abf81c --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.net.vpn.L2tpIpsecPskProfile; + +import java.io.IOException; + +/** + * The service that manages the preshared key based L2TP-over-IPSec VPN + * connection. + */ +class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> { + private static final String IPSEC_DAEMON = "racoon"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + String hostIp = getHostIp(); + L2tpIpsecPskProfile p = getProfile(); + + // IPSEC + AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON); + ipsecService.sendCommand(hostIp, serverIp, L2tpService.L2TP_PORT, + p.getPresharedKey()); + + sleep(2000); // 2 seconds + + // L2TP + MtpdHelper.sendCommand(this, L2tpService.L2TP_DAEMON, serverIp, + L2tpService.L2TP_PORT, + (p.isSecretEnabled() ? p.getSecretString() : null), + username, password); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java new file mode 100644 index 0000000..825953c --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.net.vpn.L2tpIpsecProfile; +import android.security.CertTool; + +import java.io.IOException; + +/** + * The service that manages the certificate based L2TP-over-IPSec VPN connection. + */ +class L2tpIpsecService extends VpnService<L2tpIpsecProfile> { + private static final String IPSEC_DAEMON = "racoon"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + String hostIp = getHostIp(); + + // IPSEC + AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON); + ipsecService.sendCommand(hostIp, serverIp, L2tpService.L2TP_PORT, + getUserkeyPath(), getUserCertPath(), getCaCertPath()); + + sleep(2000); // 2 seconds + + // L2TP + L2tpIpsecProfile p = getProfile(); + MtpdHelper.sendCommand(this, L2tpService.L2TP_DAEMON, serverIp, + L2tpService.L2TP_PORT, + (p.isSecretEnabled() ? p.getSecretString() : null), + username, password); + } + + private String getCaCertPath() { + return CertTool.getInstance().getCaCertificate( + getProfile().getCaCertificate()); + } + + private String getUserCertPath() { + return CertTool.getInstance().getUserCertificate( + getProfile().getUserCertificate()); + } + + private String getUserkeyPath() { + return CertTool.getInstance().getUserPrivateKey( + getProfile().getUserCertificate()); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpService.java new file mode 100644 index 0000000..9273f35 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpService.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.net.vpn.L2tpProfile; + +import java.io.IOException; + +/** + * The service that manages the L2TP VPN connection. + */ +class L2tpService extends VpnService<L2tpProfile> { + static final String L2TP_DAEMON = "l2tp"; + static final String L2TP_PORT = "1701"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + L2tpProfile p = getProfile(); + MtpdHelper.sendCommand(this, L2TP_DAEMON, serverIp, L2TP_PORT, + (p.isSecretEnabled() ? p.getSecretString() : null), + username, password); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java b/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java new file mode 100644 index 0000000..16d253a --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A helper class for sending commands to the MTP daemon (mtpd). + */ +class MtpdHelper { + private static final String MTPD_SERVICE = "mtpd"; + private static final String VPN_LINKNAME = "vpn"; + private static final String PPP_ARGS_SEPARATOR = ""; + + static void sendCommand(VpnService<?> vpnService, String protocol, + String serverIp, String port, String secret, String username, + String password) throws IOException { + ArrayList<String> args = new ArrayList<String>(); + args.addAll(Arrays.asList(protocol, serverIp, port)); + if (secret != null) args.add(secret); + args.add(PPP_ARGS_SEPARATOR); + addPppArguments(vpnService, args, serverIp, username, password); + + AndroidServiceProxy mtpd = vpnService.startService(MTPD_SERVICE); + mtpd.sendCommand(args.toArray(new String[args.size()])); + } + + private static void addPppArguments(VpnService<?> vpnService, + ArrayList<String> args, String serverIp, String username, + String password) throws IOException { + args.addAll(Arrays.asList( + "linkname", VPN_LINKNAME, + "name", username, + "password", password, + "ipparam", serverIp + "@" + vpnService.getGatewayIp(), + "refuse-eap", "nodefaultroute", "usepeerdns", + "idle", "1800", + "mtu", "1400", + "mru", "1400")); + } + + private MtpdHelper() { + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java new file mode 100644 index 0000000..f0bbc34 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; + +/** + * Proxy to perform a command with arguments. + */ +public class NormalProcessProxy extends ProcessProxy { + private Process mProcess; + private String[] mArgs; + private String mTag; + + /** + * Creates a proxy with the arguments. + * @param args the argument list with the first one being the command + */ + public NormalProcessProxy(String ...args) { + if ((args == null) || (args.length == 0)) { + throw new IllegalArgumentException(); + } + mArgs = args; + mTag = "PProxy_" + getName(); + } + + @Override + public String getName() { + return mArgs[0]; + } + + @Override + public synchronized void stop() { + if (isStopped()) return; + getHostThread().interrupt(); + // TODO: not sure how to reliably kill a process + mProcess.destroy(); + setState(ProcessState.STOPPING); + } + + @Override + protected void performTask() throws IOException, InterruptedException { + String[] args = mArgs; + Log.d(mTag, "+++++ Execute: " + getEntireCommand()); + ProcessBuilder pb = new ProcessBuilder(args); + setState(ProcessState.RUNNING); + Process p = mProcess = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + p.getInputStream())); + while (true) { + String line = reader.readLine(); + if ((line == null) || isStopping()) break; + Log.d(mTag, line); + } + + Log.d(mTag, "----- p.waitFor(): " + getName()); + p.waitFor(); + Log.d(mTag, "----- Done: " + getName()); + } + + private CharSequence getEntireCommand() { + String[] args = mArgs; + StringBuilder sb = new StringBuilder(args[0]); + for (int i = 1; i < args.length; i++) sb.append(' ').append(args[i]); + return sb; + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/PptpService.java b/packages/VpnServices/src/com/android/server/vpn/PptpService.java new file mode 100644 index 0000000..01362a5 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/PptpService.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.net.vpn.PptpProfile; + +import java.io.IOException; + +/** + * The service that manages the PPTP VPN connection. + */ +class PptpService extends VpnService<PptpProfile> { + static final String PPTP_DAEMON = "pptp"; + static final String PPTP_PORT = "1723"; + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + MtpdHelper.sendCommand(this, PPTP_DAEMON, serverIp, PPTP_PORT, null, + username, password); + } + +} diff --git a/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java new file mode 100644 index 0000000..50fbf4b --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.os.ConditionVariable; + +import java.io.IOException; + +/** + * A proxy class that spawns a process to accomplish a certain task. + */ +public abstract class ProcessProxy { + /** + * Defines the interface to call back when the process is finished or an + * error occurs during execution. + */ + public static interface Callback { + /** + * Called when the process is finished. + * @param proxy the proxy that hosts the process + */ + void done(ProcessProxy proxy); + + /** + * Called when some error occurs. + * @param proxy the proxy that hosts the process + */ + void error(ProcessProxy proxy, Throwable error); + } + + protected enum ProcessState { + STOPPED, STARTING, RUNNING, STOPPING, ERROR + } + + private ProcessState mState = ProcessState.STOPPED; + private ConditionVariable mLock = new ConditionVariable(); + private Thread mThread; + + /** + * Returns the name of the process. + */ + public abstract String getName(); + + /** + * Starts the process with a callback. + * @param callback the callback to get notified when the process is finished + * or an error occurs during execution + * @throws IOException when the process is already running or failed to + * start + */ + public synchronized void start(final Callback callback) throws IOException { + if (!isStopped()) { + throw new IOException("Process is already running: " + this); + } + mLock.close(); + setState(ProcessState.STARTING); + Thread thread = new Thread(new Runnable() { + public void run() { + try { + performTask(); + setState(ProcessState.STOPPED); + mLock.open(); + if (callback != null) callback.done(ProcessProxy.this); + } catch (Throwable e) { + setState(ProcessState.ERROR); + if (callback != null) callback.error(ProcessProxy.this, e); + } finally { + mThread = null; + } + } + }); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + mThread = thread; + if (!waitUntilRunning()) { + throw new IOException("Failed to start the process: " + this); + } + } + + /** + * Starts the process. + * @throws IOException when the process is already running or failed to + * start + */ + public synchronized void start() throws IOException { + start(null); + if (!waitUntilDone()) { + throw new IOException("Failed to complete the process: " + this); + } + } + + /** + * Returns the thread that hosts the process. + */ + public Thread getHostThread() { + return mThread; + } + + /** + * Blocks the current thread until the hosted process is finished. + * + * @return true if the process is finished normally; false if an error + * occurs + */ + public boolean waitUntilDone() { + while (!mLock.block(1000)) { + if (isStopped() || isInError()) break; + } + return isStopped(); + } + + /** + * Blocks the current thread until the hosted process is running. + * + * @return true if the process is running normally; false if the process + * is in another state + */ + private boolean waitUntilRunning() { + for (;;) { + if (!isStarting()) break; + } + return isRunning(); + } + + /** + * Stops and destroys the process. + */ + public abstract void stop(); + + /** + * Checks whether the process is finished. + * @return true if the process is stopped + */ + public boolean isStopped() { + return (mState == ProcessState.STOPPED); + } + + /** + * Checks whether the process is being stopped. + * @return true if the process is being stopped + */ + public boolean isStopping() { + return (mState == ProcessState.STOPPING); + } + + /** + * Checks whether the process is being started. + * @return true if the process is being started + */ + public boolean isStarting() { + return (mState == ProcessState.STARTING); + } + + /** + * Checks whether the process is running. + * @return true if the process is running + */ + public boolean isRunning() { + return (mState == ProcessState.RUNNING); + } + + /** + * Checks whether some error has occurred and the process is stopped. + * @return true if some error has occurred and the process is stopped + */ + public boolean isInError() { + return (mState == ProcessState.ERROR); + } + + /** + * Performs the actual task. Subclasses must make sure that the method + * is blocked until the task is done or an error occurs. + */ + protected abstract void performTask() + throws IOException, InterruptedException; + + /** + * Sets the process state. + * @param state the new state to be in + */ + protected void setState(ProcessState state) { + mState = state; + } + + /** + * Makes the current thread sleep for the specified time. + * @param msec time to sleep in miliseconds + */ + protected void sleep(int msec) { + try { + Thread.currentThread().sleep(msec); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnService.java b/packages/VpnServices/src/com/android/server/vpn/VpnService.java new file mode 100644 index 0000000..a60788a --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/VpnService.java @@ -0,0 +1,520 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.net.NetworkUtils; +import android.net.vpn.VpnManager; +import android.net.vpn.VpnProfile; +import android.net.vpn.VpnState; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * The service base class for managing a type of VPN connection. + */ +abstract class VpnService<E extends VpnProfile> { + private static final int NOTIFICATION_ID = 1; + + private static final String DNS1 = "net.dns1"; + private static final String DNS2 = "net.dns2"; + private static final String VPN_DNS1 = "vpn.dns1"; + private static final String VPN_DNS2 = "vpn.dns2"; + private static final String VPN_UP = "vpn.up"; + private static final String VPN_IS_UP = "1"; + private static final String VPN_IS_DOWN = "0"; + + private static final String REMOTE_IP = "net.ipremote"; + private static final String DNS_DOMAIN_SUFFICES = "net.dns.search"; + + private final String TAG = VpnService.class.getSimpleName(); + + E mProfile; + VpnServiceBinder mContext; + + private VpnState mState = VpnState.IDLE; + private boolean mInError; + + // connection settings + private String mOriginalDns1; + private String mOriginalDns2; + private String mVpnDns1 = ""; + private String mVpnDns2 = ""; + private String mOriginalDomainSuffices; + private String mHostIp; + + private long mStartTime; // VPN connection start time + + // for helping managing multiple Android services + private ServiceHelper mServiceHelper = new ServiceHelper(); + + // for helping showing, updating notification + private NotificationHelper mNotification = new NotificationHelper(); + + /** + * Establishes a VPN connection with the specified username and password. + */ + protected abstract void connect(String serverIp, String username, + String password) throws IOException; + + /** + * Tears down the VPN connection. The base class simply terminates all the + * Android services. A subclass may need to do some clean-up before that. + */ + protected void disconnect() { + } + + /** + * Starts an Android service defined in init.rc. + */ + protected AndroidServiceProxy startService(String serviceName) + throws IOException { + return mServiceHelper.startService(serviceName); + } + + /** + * Returns the VPN profile associated with the connection. + */ + protected E getProfile() { + return mProfile; + } + + /** + * Returns the host IP for establishing the VPN connection. + */ + protected String getHostIp() throws IOException { + if (mHostIp == null) mHostIp = reallyGetHostIp(); + return mHostIp; + } + + /** + * Returns the IP address of the specified host name. + */ + protected String getIp(String hostName) throws IOException { + return InetAddress.getByName(hostName).getHostAddress(); + } + + /** + * Returns the IP address of the default gateway. + */ + protected String getGatewayIp() throws IOException { + Enumeration<NetworkInterface> ifces = + NetworkInterface.getNetworkInterfaces(); + for (; ifces.hasMoreElements(); ) { + NetworkInterface ni = ifces.nextElement(); + int gateway = NetworkUtils.getDefaultRoute(ni.getName()); + if (gateway == 0) continue; + return toInetAddress(gateway).getHostAddress(); + } + throw new IOException("Default gateway is not available"); + } + + /** + * Sets the system property. The method is blocked until the value is + * settled in. + * @param name the name of the property + * @param value the value of the property + * @throws IOException if it fails to set the property within 2 seconds + */ + protected void setSystemProperty(String name, String value) + throws IOException { + SystemProperties.set(name, value); + for (int i = 0; i < 5; i++) { + String v = SystemProperties.get(name); + if (v.equals(value)) { + return; + } else { + Log.d(TAG, "sys_prop: wait for " + name + " to settle in"); + sleep(400); + } + } + throw new IOException("Failed to set system property: " + name); + } + + void setContext(VpnServiceBinder context, E profile) { + mContext = context; + mProfile = profile; + } + + VpnState getState() { + return mState; + } + + synchronized void onConnect(String username, String password) + throws IOException { + mState = VpnState.CONNECTING; + broadcastConnectivity(VpnState.CONNECTING); + + String serverIp = getIp(getProfile().getServerName()); + + onBeforeConnect(); + connect(serverIp, username, password); + waitUntilConnectedOrTimedout(); + } + + synchronized void onDisconnect(boolean cleanUpServices) { + try { + mState = VpnState.DISCONNECTING; + broadcastConnectivity(VpnState.DISCONNECTING); + mNotification.showDisconnect(); + + // subclass implementation + if (cleanUpServices) disconnect(); + + mServiceHelper.stop(); + } catch (Throwable e) { + Log.e(TAG, "onError()", e); + onFinalCleanUp(); + } + } + + synchronized void onError() { + // error may occur during or after connection setup + // and it may be due to one or all services gone + mInError = true; + switch (mState) { + case CONNECTED: + onDisconnect(true); + break; + + case CONNECTING: + onDisconnect(false); + break; + } + } + + private void onBeforeConnect() { + mNotification.disableNotification(); + + SystemProperties.set(VPN_DNS1, "-"); + SystemProperties.set(VPN_DNS2, "-"); + SystemProperties.set(VPN_UP, VPN_IS_DOWN); + Log.d(TAG, " VPN UP: " + SystemProperties.get(VPN_UP)); + } + + private void waitUntilConnectedOrTimedout() { + sleep(2000); // 2 seconds + for (int i = 0; i < 60; i++) { + if (VPN_IS_UP.equals(SystemProperties.get(VPN_UP))) { + onConnected(); + return; + } + sleep(500); // 0.5 second + } + + synchronized (this) { + if (mState == VpnState.CONNECTING) { + Log.d(TAG, " connecting timed out !!"); + onError(); + } + } + } + + private synchronized void onConnected() { + Log.d(TAG, "onConnected()"); + + saveVpnDnsProperties(); + saveAndSetDomainSuffices(); + startConnectivityMonitor(); + + mState = VpnState.CONNECTED; + broadcastConnectivity(VpnState.CONNECTED); + } + + private synchronized void onFinalCleanUp() { + Log.d(TAG, "onFinalCleanUp()"); + + if (mState == VpnState.IDLE) return; + + // keep the notification when error occurs + if (!mInError) mNotification.disableNotification(); + + restoreOriginalDnsProperties(); + restoreOriginalDomainSuffices(); + mState = VpnState.IDLE; + broadcastConnectivity(VpnState.IDLE); + + // stop the service itself + mContext.stopSelf(); + } + + private synchronized void onOneServiceGone() { + switch (mState) { + case IDLE: + case DISCONNECTING: + break; + + default: + onError(); + } + } + + private synchronized void onAllServicesGone() { + switch (mState) { + case IDLE: + break; + + case DISCONNECTING: + // daemons are gone; now clean up everything + onFinalCleanUp(); + break; + + default: + onError(); + } + } + + private void restoreOriginalDnsProperties() { + // restore only if they are not overridden + if (mVpnDns1.equals(SystemProperties.get(DNS1))) { + Log.d(TAG, String.format("restore original dns prop: %s --> %s", + SystemProperties.get(DNS1), mOriginalDns1)); + Log.d(TAG, String.format("restore original dns prop: %s --> %s", + SystemProperties.get(DNS2), mOriginalDns2)); + SystemProperties.set(DNS1, mOriginalDns1); + SystemProperties.set(DNS2, mOriginalDns2); + } + } + + private void saveVpnDnsProperties() { + mOriginalDns1 = mOriginalDns2 = ""; + for (int i = 0; i < 10; i++) { + mVpnDns1 = SystemProperties.get(VPN_DNS1); + mVpnDns2 = SystemProperties.get(VPN_DNS2); + if (mOriginalDns1.equals(mVpnDns1)) { + Log.d(TAG, "wait for vpn dns to settle in..." + i); + sleep(500); + } else { + mOriginalDns1 = SystemProperties.get(DNS1); + mOriginalDns2 = SystemProperties.get(DNS2); + SystemProperties.set(DNS1, mVpnDns1); + SystemProperties.set(DNS2, mVpnDns2); + Log.d(TAG, String.format("save original dns prop: %s, %s", + mOriginalDns1, mOriginalDns2)); + Log.d(TAG, String.format("set vpn dns prop: %s, %s", + mVpnDns1, mVpnDns2)); + return; + } + } + Log.e(TAG, "saveVpnDnsProperties(): DNS not updated??"); + } + + private void saveAndSetDomainSuffices() { + mOriginalDomainSuffices = SystemProperties.get(DNS_DOMAIN_SUFFICES); + Log.d(TAG, "save original dns search: " + mOriginalDomainSuffices); + String list = mProfile.getDomainSuffices(); + if (!TextUtils.isEmpty(list)) { + SystemProperties.set(DNS_DOMAIN_SUFFICES, list); + } + } + + private void restoreOriginalDomainSuffices() { + Log.d(TAG, "restore original dns search --> " + mOriginalDomainSuffices); + SystemProperties.set(DNS_DOMAIN_SUFFICES, mOriginalDomainSuffices); + } + + private void broadcastConnectivity(VpnState s) { + new VpnManager(mContext).broadcastConnectivity(mProfile.getName(), s); + } + + private void startConnectivityMonitor() { + mStartTime = System.currentTimeMillis(); + + new Thread(new Runnable() { + public void run() { + Log.d(TAG, " +++++ connectivity monitor running"); + try { + for (;;) { + synchronized (VpnService.this) { + if (mState != VpnState.CONNECTED) break; + mNotification.update(); + checkConnectivity(); + VpnService.this.wait(1000); // 1 second + } + } + } catch (InterruptedException e) { + Log.e(TAG, "connectivity monitor", e); + } + Log.d(TAG, " ----- connectivity monitor stopped"); + } + }).start(); + } + + private void checkConnectivity() { + checkDnsProperties(); + } + + private void checkDnsProperties() { + String dns1 = SystemProperties.get(DNS1); + if (!mVpnDns1.equals(dns1)) { + Log.w(TAG, " @@ !!! dns being overridden"); + onError(); + } + } + + private String reallyGetHostIp() throws IOException { + Enumeration<NetworkInterface> ifces = + NetworkInterface.getNetworkInterfaces(); + for (; ifces.hasMoreElements(); ) { + NetworkInterface ni = ifces.nextElement(); + int gateway = NetworkUtils.getDefaultRoute(ni.getName()); + if (gateway == 0) continue; + Enumeration<InetAddress> addrs = ni.getInetAddresses(); + for (; addrs.hasMoreElements(); ) { + return addrs.nextElement().getHostAddress(); + } + } + throw new IOException("Host IP is not available"); + } + + protected void sleep(int ms) { + try { + Thread.currentThread().sleep(ms); + } catch (InterruptedException e) { + } + } + + private InetAddress toInetAddress(int addr) throws IOException { + byte[] aa = new byte[4]; + for (int i= 0; i < aa.length; i++) { + aa[i] = (byte) (addr & 0x0FF); + addr >>= 8; + } + return InetAddress.getByAddress(aa); + } + + private class ServiceHelper implements ProcessProxy.Callback { + private List<AndroidServiceProxy> mServiceList = + new ArrayList<AndroidServiceProxy>(); + + // starts an Android service + AndroidServiceProxy startService(String serviceName) + throws IOException { + AndroidServiceProxy service = new AndroidServiceProxy(serviceName); + mServiceList.add(service); + service.start(this); + return service; + } + + // stops all the Android services + void stop() { + if (mServiceList.isEmpty()) { + onFinalCleanUp(); + } else { + for (AndroidServiceProxy s : mServiceList) s.stop(); + } + } + + //@Override + public void done(ProcessProxy p) { + Log.d(TAG, "service done: " + p.getName()); + commonCallback((AndroidServiceProxy) p); + } + + //@Override + public void error(ProcessProxy p, Throwable e) { + Log.e(TAG, "service error: " + p.getName(), e); + commonCallback((AndroidServiceProxy) p); + } + + private void commonCallback(AndroidServiceProxy service) { + mServiceList.remove(service); + onOneServiceGone(); + if (mServiceList.isEmpty()) onAllServicesGone(); + } + } + + // Helper class for showing, updating notification. + private class NotificationHelper { + void update() { + String title = getNotificationTitle(true); + Notification n = new Notification(R.drawable.vpn_connected, title, + mStartTime); + n.setLatestEventInfo(mContext, title, + getNotificationMessage(true), prepareNotificationIntent()); + n.flags |= Notification.FLAG_NO_CLEAR; + n.flags |= Notification.FLAG_ONGOING_EVENT; + enableNotification(n); + } + + void showDisconnect() { + String title = getNotificationTitle(false); + Notification n = new Notification(R.drawable.vpn_disconnected, + title, System.currentTimeMillis()); + n.setLatestEventInfo(mContext, title, + getNotificationMessage(false), prepareNotificationIntent()); + n.flags |= Notification.FLAG_AUTO_CANCEL; + disableNotification(); + enableNotification(n); + } + + void disableNotification() { + ((NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE)).cancel(NOTIFICATION_ID); + } + + private void enableNotification(Notification n) { + ((NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, n); + } + + private PendingIntent prepareNotificationIntent() { + return PendingIntent.getActivity(mContext, 0, + new VpnManager(mContext).createSettingsActivityIntent(), 0); + } + + private String getNotificationTitle(boolean connected) { + String formatString = connected + ? mContext.getString( + R.string.vpn_notification_title_connected) + : mContext.getString( + R.string.vpn_notification_title_disconnected); + return String.format(formatString, mProfile.getName()); + } + + private String getFormattedTime(long duration) { + long hours = duration / 3600; + StringBuilder sb = new StringBuilder(); + if (hours > 0) sb.append(hours).append(':'); + sb.append(String.format("%02d:%02d", (duration % 3600 / 60), + (duration % 60))); + return sb.toString(); + } + + private String getNotificationMessage(boolean connected) { + if (connected) { + long time = (System.currentTimeMillis() - mStartTime) / 1000; + return getFormattedTime(time); + } else { + return mContext.getString( + R.string.vpn_notification_hint_disconnected); + } + } + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java new file mode 100644 index 0000000..617875e --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2009, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vpn; + +import android.app.Service; +import android.content.Intent; +import android.net.vpn.IVpnService; +import android.net.vpn.L2tpIpsecProfile; +import android.net.vpn.L2tpIpsecPskProfile; +import android.net.vpn.L2tpProfile; +import android.net.vpn.PptpProfile; +import android.net.vpn.VpnManager; +import android.net.vpn.VpnProfile; +import android.net.vpn.VpnState; +import android.os.IBinder; +import android.util.Log; + +import java.io.IOException; + +/** + * The service class for managing a VPN connection. It implements the + * {@link IVpnService} binder interface. + */ +public class VpnServiceBinder extends Service { + private final String TAG = VpnServiceBinder.class.getSimpleName(); + + // The actual implementation is delegated to the VpnService class. + private VpnService<? extends VpnProfile> mService; + + private final IBinder mBinder = new IVpnService.Stub() { + public boolean connect(VpnProfile p, String username, String password) { + return VpnServiceBinder.this.connect(p, username, password); + } + + public void disconnect() { + if (mService != null) mService.onDisconnect(true); + } + + public void checkStatus(VpnProfile p) { + VpnServiceBinder.this.checkStatus(p); + } + }; + + public IBinder onBind(Intent intent) { + return mBinder; + } + + private synchronized boolean connect( + VpnProfile p, String username, String password) { + if (mService != null) return false; + try { + mService = createService(p); + mService.onConnect(username, password); + return true; + } catch (Throwable e) { + Log.e(TAG, "connect()", e); + if (mService != null) mService.onError(); + return false; + } + } + + private synchronized void checkStatus(VpnProfile p) { + if (mService == null) broadcastConnectivity(p.getName(), VpnState.IDLE); + + if (!p.getName().equals(mService.mProfile.getName())) { + broadcastConnectivity(p.getName(), VpnState.IDLE); + } else { + broadcastConnectivity(p.getName(), mService.getState()); + } + } + + private VpnService<? extends VpnProfile> createService(VpnProfile p) { + switch (p.getType()) { + case L2TP: + L2tpService l2tp = new L2tpService(); + l2tp.setContext(this, (L2tpProfile) p); + return l2tp; + + case PPTP: + PptpService pptp = new PptpService(); + pptp.setContext(this, (PptpProfile) p); + return pptp; + + case L2TP_IPSEC_PSK: + L2tpIpsecPskService psk = new L2tpIpsecPskService(); + psk.setContext(this, (L2tpIpsecPskProfile) p); + return psk; + + case L2TP_IPSEC: + L2tpIpsecService l2tpIpsec = new L2tpIpsecService(); + l2tpIpsec.setContext(this, (L2tpIpsecProfile) p); + return l2tpIpsec; + + default: + return null; + } + } + + private void broadcastConnectivity(String name, VpnState s) { + new VpnManager(this).broadcastConnectivity(name, s); + } +} |