diff options
author | Android (Google) Code Review <android-gerrit@google.com> | 2009-06-12 08:26:32 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-06-12 08:26:32 -0700 |
commit | 69b0db06543d4dfc49abcde3370fcb9e26b222ec (patch) | |
tree | 7053b1b65258028c86b92fa36c9ea1fc4730d23d /packages | |
parent | 64420d4f17b94adca197b7ae66bc6fa8cd0ef0e2 (diff) | |
parent | 83bb56c389d003162093eef0aaee251cefc1deed (diff) | |
download | frameworks_base-69b0db06543d4dfc49abcde3370fcb9e26b222ec.zip frameworks_base-69b0db06543d4dfc49abcde3370fcb9e26b222ec.tar.gz frameworks_base-69b0db06543d4dfc49abcde3370fcb9e26b222ec.tar.bz2 |
am 83bb56c3: Merge change 3424 into donut
Merge commit '83bb56c389d003162093eef0aaee251cefc1deed'
* commit '83bb56c389d003162093eef0aaee251cefc1deed':
Add the VPN services package, VPN service base classes and L2tpIpsecService.
Diffstat (limited to 'packages')
-rw-r--r-- | packages/VpnServices/Android.mk | 16 | ||||
-rw-r--r-- | packages/VpnServices/AndroidManifest.xml | 22 | ||||
-rw-r--r-- | packages/VpnServices/MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | packages/VpnServices/NOTICE | 190 | ||||
-rw-r--r-- | packages/VpnServices/res/drawable/vpn_connected.png | bin | 0 -> 757 bytes | |||
-rw-r--r-- | packages/VpnServices/res/drawable/vpn_disconnected.png | bin | 0 -> 717 bytes | |||
-rwxr-xr-x | packages/VpnServices/res/values/strings.xml | 8 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java | 258 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java | 65 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/L2tpService.java | 42 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java | 85 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java | 210 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/VpnService.java | 627 | ||||
-rw-r--r-- | packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java | 104 |
14 files changed, 1627 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..e48b2da --- /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="VPN Services"> + + <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..892850a --- /dev/null +++ b/packages/VpnServices/res/values/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="vpn_notification_title">%s VPN %s</string> + <string name="vpn_notification_connected">connected</string> + <string name="vpn_notification_disconnected">disconnected</string> + <string name="vpn_notification_connected_message">Up time: %s</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..a12db8c --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2007, 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. + * Each argument is sent as a C-style zero-terminated string. + */ + public void sendCommand(String ...args) throws IOException { + OutputStream out = getControlSocketOutput(); + for (String arg : args) outputString(out, arg); + checkSocketResult(); + } + + /** + * Sends a command with arguments to the service through the control socket. + */ + public void sendCommand2(String ...args) throws IOException { + OutputStream out = getControlSocketOutput(); + for (String arg : args) outputString2(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 { + out.write(s.getBytes()); + out.write(0); + out.flush(); + } + + private void outputString2(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/L2tpIpsecService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java new file mode 100644 index 0000000..ce56921 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2007, 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.Keystore; + +import java.io.IOException; + +/** + * The service that manages the L2TP-over-IPSec VPN connection. + */ +class L2tpIpsecService extends VpnService<L2tpIpsecProfile> { + private static final String IPSEC_SERVICE = "racoon"; + private static final String L2TP_SERVICE = "mtpd"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + String hostIp = getHostIp(); + + // IPSEC + AndroidServiceProxy ipsecService = startService(IPSEC_SERVICE); + ipsecService.sendCommand( + String.format("SETKEY %s %s", hostIp, serverIp)); + ipsecService.sendCommand(String.format("SET_CERTS %s %s %s %s", + serverIp, getCaCertPath(), getUserCertPath(), + getUserkeyPath())); + + // L2TP + AndroidServiceProxy l2tpService = startService(L2TP_SERVICE); + l2tpService.sendCommand2("l2tp", serverIp, "1701", "", + "file", getPppOptionFilePath(), + "name", username, + "password", password); + } + + private String getCaCertPath() { + return Keystore.getInstance().getCertificate( + getProfile().getCaCertificate()); + } + + private String getUserCertPath() { + return Keystore.getInstance().getCertificate( + getProfile().getUserCertificate()); + } + + private String getUserkeyPath() { + return Keystore.getInstance().getUserkey(getProfile().getUserkey()); + } +} 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..9aad7a1 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpService.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2007, 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> { + private static final String L2TP_SERVICE = "mtpd"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + String hostIp = getHostIp(); + + // L2TP + AndroidServiceProxy l2tpService = startService(L2TP_SERVICE); + l2tpService.sendCommand2("l2tp", serverIp, "1701", "", + "file", getPppOptionFilePath(), + "name", username, + "password", password); + } + +} 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..f20d85f --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2007, 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/ProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java new file mode 100644 index 0000000..14d7521 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2007, 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..01106b3 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/VpnService.java @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2007, 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.vpn.SingleServerProfile; +import android.net.vpn.VpnManager; +import android.net.vpn.VpnState; +import android.os.FileObserver; +import android.os.SystemProperties; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +/** + * The service base class for managing a type of VPN connection. + */ +abstract class VpnService<E extends SingleServerProfile> { + private static final int NOTIFICATION_ID = 1; + private static final String PROFILES_ROOT = VpnManager.PROFILES_PATH + "/"; + public static final String DEFAULT_CONFIG_PATH = "/etc"; + + private static final int DNS_TIMEOUT = 3000; // ms + private static final String DNS1 = "net.dns1"; + private static final String DNS2 = "net.dns2"; + private static final String REMOTE_IP = "net.ipremote"; + private static final String DNS_DOMAIN_SUFFICES = "net.dns.search"; + private static final String SERVER_IP = "net.vpn.server_ip"; + + private static final int VPN_TIMEOUT = 30000; // milliseconds + private static final int ONE_SECOND = 1000; // milliseconds + private static final int FIVE_SECOND = 5000; // milliseconds + + private static final String LOGWRAPPER = "/system/bin/logwrapper"; + 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 + + // monitors if the VPN connection is sucessfully established + private FileMonitor mConnectMonitor; + + // watch dog timer; fired up if the connection cannot be established within + // VPN_TIMEOUT + private Object mWatchdog; + + // 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); + } + + protected String getPppOptionFilePath() throws IOException { + String subpath = getProfileSubpath("/ppp/peers"); + String[] kids = new File(subpath).list(); + if ((kids == null) || (kids.length == 0)) { + throw new IOException("no option file found in " + subpath); + } + if (kids.length > 1) { + Log.w(TAG, "more than one option file found in " + subpath + + ", arbitrarily choose " + kids[0]); + } + return subpath + "/" + kids[0]; + } + + /** + * Returns the VPN profile associated with the connection. + */ + protected E getProfile() { + return mProfile; + } + + /** + * Returns the profile path where configuration files reside. + */ + protected String getProfilePath() throws IOException { + String path = PROFILES_ROOT + mProfile.getId(); + File dir = new File(path); + if (!dir.exists()) throw new IOException("Profile dir does not exist"); + return path; + } + + /** + * Returns the path where default configuration files reside. + */ + protected String getDefaultConfigPath() throws IOException { + return DEFAULT_CONFIG_PATH; + } + + /** + * Returns the host IP for establishing the VPN connection. + */ + protected String getHostIp() throws IOException { + if (mHostIp == null) mHostIp = reallyGetHostIp(); + return mHostIp; + } + + /** + * Returns the IP of the specified host name. + */ + protected String getIp(String hostName) throws IOException { + InetAddress iaddr = InetAddress.getByName(hostName); + byte[] aa = iaddr.getAddress(); + StringBuilder sb = new StringBuilder().append(byteToInt(aa[0])); + for (int i = 1; i < aa.length; i++) { + sb.append(".").append(byteToInt(aa[i])); + } + return sb.toString(); + } + + /** + * Returns the path of the script file that is executed when the VPN + * connection is established. + */ + protected String getConnectMonitorFile() { + return "/etc/ppp/ip-up"; + } + + /** + * 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()); + setSystemProperty(SERVER_IP, serverIp); + onBeforeConnect(); + + connect(serverIp, username, password); + } + + 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 createConnectMonitor() { + mConnectMonitor = new FileMonitor(getConnectMonitorFile(), + new Runnable() { + public void run() { + onConnectMonitorTriggered(); + } + }); + } + + private void onBeforeConnect() { + mNotification.disableNotification(); + + createConnectMonitor(); + mConnectMonitor.startWatching(); + saveOriginalDnsProperties(); + + mWatchdog = startTimer(VPN_TIMEOUT, new Runnable() { + public void run() { + synchronized (VpnService.this) { + if (mState == VpnState.CONNECTING) { + Log.d(TAG, " watchdog timer is fired !!"); + onError(); + } + } + } + }); + } + + private synchronized void onConnectMonitorTriggered() { + Log.d(TAG, "onConnectMonitorTriggered()"); + + stopTimer(mWatchdog); + mConnectMonitor.stopWatching(); + 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(); + if (mConnectMonitor != null) mConnectMonitor.stopWatching(); + if (mWatchdog != null) stopTimer(mWatchdog); + 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 saveOriginalDnsProperties() { + mOriginalDns1 = SystemProperties.get(DNS1); + mOriginalDns2 = SystemProperties.get(DNS2); + Log.d(TAG, String.format("save original dns prop: %s, %s", + mOriginalDns1, mOriginalDns2)); + } + + 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() { + mVpnDns1 = mVpnDns2 = ""; + for (int i = 0; i < 10; i++) { + mVpnDns1 = SystemProperties.get(DNS1); + mVpnDns2 = SystemProperties.get(DNS2); + if (mVpnDns1.equals(mOriginalDns1)) { + Log.d(TAG, "wait for vpn dns to settle in..." + i); + sleep(500); + } else { + Log.d(TAG, String.format("save vpn dns prop: %s, %s", + mVpnDns1, mVpnDns2)); + return; + } + } + Log.e(TAG, "saveVpnDnsProperties(): DNS not updated??"); + } + + private void restoreVpnDnsProperties() { + if (isNullOrEmpty(mVpnDns1) && isNullOrEmpty(mVpnDns2)) { + return; + } + Log.d(TAG, String.format("restore vpn dns prop: %s --> %s", + SystemProperties.get(DNS1), mVpnDns1)); + Log.d(TAG, String.format("restore vpn dns prop: %s --> %s", + SystemProperties.get(DNS2), mVpnDns2)); + SystemProperties.set(DNS1, mVpnDns1); + SystemProperties.set(DNS2, mVpnDns2); + } + + private void saveAndSetDomainSuffices() { + mOriginalDomainSuffices = SystemProperties.get(DNS_DOMAIN_SUFFICES); + Log.d(TAG, "save original dns search: " + mOriginalDomainSuffices); + String list = mProfile.getDomainSuffices(); + if (!isNullOrEmpty(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(ONE_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 Object startTimer(final int milliseconds, final Runnable task) { + Thread thread = new Thread(new Runnable() { + public void run() { + Log.d(TAG, "watchdog timer started"); + Thread t = Thread.currentThread(); + try { + synchronized (t) { + t.wait(milliseconds); + } + task.run(); + } catch (InterruptedException e) { + // ignored + } + Log.d(TAG, "watchdog timer stopped"); + } + }); + thread.start(); + return thread; + } + + private void stopTimer(Object timer) { + synchronized (timer) { + timer.notify(); + } + } + + private String reallyGetHostIp() throws IOException { + Socket s = new Socket(); + s.connect(new InetSocketAddress("www.google.com", 80), DNS_TIMEOUT); + String ipAddress = s.getLocalAddress().getHostAddress(); + Log.d(TAG, "Host IP: " + ipAddress); + s.close(); + return ipAddress; + } + + private String getProfileSubpath(String subpath) throws IOException { + String path = getProfilePath() + subpath; + if (new File(path).exists()) { + return path; + } else { + Log.w(TAG, "Profile subpath does not exist: " + path + + ", use default one"); + String path2 = getDefaultConfigPath() + subpath; + if (!new File(path2).exists()) { + throw new IOException("Profile subpath does not exist at " + + path + " or " + path2); + } + return path2; + } + } + + private void sleep(int ms) { + try { + Thread.currentThread().sleep(ms); + } catch (InterruptedException e) { + } + } + + private static boolean isNullOrEmpty(String message) { + return ((message == null) || (message.length() == 0)); + } + + private static int byteToInt(byte b) { + return ((int) b) & 0x0FF; + } + + 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(); + } + } + + private class FileMonitor extends FileObserver { + private Runnable mCallback; + + FileMonitor(String path, Runnable callback) { + super(path, CLOSE_NOWRITE); + mCallback = callback; + } + + @Override + public void onEvent(int event, String path) { + if ((event & CLOSE_NOWRITE) > 0) mCallback.run(); + } + } + + // 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 connectedOrNot = connected + ? mContext.getString(R.string.vpn_notification_connected) + : mContext.getString( + R.string.vpn_notification_disconnected); + return String.format( + mContext.getString(R.string.vpn_notification_title), + mProfile.getName(), connectedOrNot); + } + + private String getTimeFormat(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 String.format(mContext.getString( + R.string.vpn_notification_connected_message), + getTimeFormat(time)); + } else { + return ""; + } + } + } +} 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..7195ea6 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2007, 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.L2tpProfile; +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_IPSEC: + L2tpIpsecService l2tpIpsec = new L2tpIpsecService(); + l2tpIpsec.setContext(this, (L2tpIpsecProfile) p); + return l2tpIpsec; + + case L2TP: + L2tpService l2tp = new L2tpService(); + l2tp.setContext(this, (L2tpProfile) p); + return l2tp; + + default: + return null; + } + } + + private void broadcastConnectivity(String name, VpnState s) { + new VpnManager(this).broadcastConnectivity(name, s); + } +} |