diff options
Diffstat (limited to 'core/java/android/net')
49 files changed, 13406 insertions, 0 deletions
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java new file mode 100644 index 0000000..213813a --- /dev/null +++ b/core/java/android/net/ConnectivityManager.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import android.os.RemoteException; + +/** + * Class that answers queries about the state of network connectivity. It also + * notifies applications when network connectivity changes. Get an instance + * of this class by calling + * {@link android.content.Context#getSystemService(String) Context.getSystemService(Context.CONNECTIVITY_SERVICE)}. + * <p> + * The primary responsibilities of this class are to: + * <ol> + * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li> + * <li>Send broadcast intents when network connectivity changes</li> + * <li>Attempt to "fail over" to another network when connectivity to a network + * is lost</li> + * <li>Provide an API that allows applications to query the coarse-grained or fine-grained + * state of the available networks</li> + * </ol> + */ +public class ConnectivityManager +{ + /** + * A change in network connectivity has occurred. A connection has either + * been established or lost. The NetworkInfo for the affected network is + * sent as an extra; it should be consulted to see what kind of + * connectivity event occurred. + * <p/> + * If this is a connection that was the result of failing over from a + * disconnected network, then the FAILOVER_CONNECTION boolean extra is + * set to true. + * <p/> + * For a loss of connectivity, if the connectivity manager is attempting + * to connect (or has already connected) to another network, the + * NetworkInfo for the new network is also passed as an extra. This lets + * any receivers of the broadcast know that they should not necessarily + * tell the user that no data traffic will be possible. Instead, the + * reciever should expect another broadcast soon, indicating either that + * the failover attempt succeeded (and so there is still overall data + * connectivity), or that the failover attempt failed, meaning that all + * connectivity has been lost. + * <p/> + * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY + * is set to {@code true} if there are no connected networks at all. + */ + public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + /** + * The lookup key for a {@link NetworkInfo} object. Retrieve with + * {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_NETWORK_INFO = "networkInfo"; + /** + * The lookup key for a boolean that indicates whether a connect event + * is for a network to which the connectivity manager was failing over + * following a disconnect on another network. + * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}. + */ + public static final String EXTRA_IS_FAILOVER = "isFailover"; + /** + * The lookup key for a {@link NetworkInfo} object. This is supplied when + * there is another network that it may be possible to connect to. Retrieve with + * {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork"; + /** + * The lookup key for a boolean that indicates whether there is a + * complete lack of connectivity, i.e., no network is available. + * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}. + */ + public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity"; + /** + * The lookup key for a string that indicates why an attempt to connect + * to a network failed. The string has no particular structure. It is + * intended to be used in notifications presented to users. Retrieve + * it with {@link android.content.Intent#getStringExtra(String)}. + */ + public static final String EXTRA_REASON = "reason"; + /** + * The lookup key for a string that provides optionally supplied + * extra information about the network state. The information + * may be passed up from the lower networking layers, and its + * meaning may be specific to a particular network type. Retrieve + * it with {@link android.content.Intent#getStringExtra(String)}. + */ + public static final String EXTRA_EXTRA_INFO = "extraInfo"; + + public static final int TYPE_MOBILE = 0; + public static final int TYPE_WIFI = 1; + + public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI; + + private IConnectivityManager mService; + + static public boolean isNetworkTypeValid(int networkType) { + return networkType == TYPE_WIFI || networkType == TYPE_MOBILE; + } + + public void setNetworkPreference(int preference) { + try { + mService.setNetworkPreference(preference); + } catch (RemoteException e) { + } + } + + public int getNetworkPreference() { + try { + return mService.getNetworkPreference(); + } catch (RemoteException e) { + return -1; + } + } + + public NetworkInfo getActiveNetworkInfo() { + try { + return mService.getActiveNetworkInfo(); + } catch (RemoteException e) { + return null; + } + } + + public NetworkInfo getNetworkInfo(int networkType) { + try { + return mService.getNetworkInfo(networkType); + } catch (RemoteException e) { + return null; + } + } + + public NetworkInfo[] getAllNetworkInfo() { + try { + return mService.getAllNetworkInfo(); + } catch (RemoteException e) { + return null; + } + } + + /** {@hide} */ + public boolean setRadios(boolean turnOn) { + try { + return mService.setRadios(turnOn); + } catch (RemoteException e) { + return false; + } + } + + /** {@hide} */ + public boolean setRadio(int networkType, boolean turnOn) { + try { + return mService.setRadio(networkType, turnOn); + } catch (RemoteException e) { + return false; + } + } + + /** + * Tells the underlying networking system that the caller wants to + * begin using the named feature. The interpretation of {@code feature} + * is completely up to each networking implementation. + * @param networkType specifies which network the request pertains to + * @param feature the name of the feature to be used + * @return an integer value representing the outcome of the request. + * The interpretation of this value is specific to each networking + * implementation+feature combination, except that the value {@code -1} + * always indicates failure. + */ + public int startUsingNetworkFeature(int networkType, String feature) { + try { + return mService.startUsingNetworkFeature(networkType, feature); + } catch (RemoteException e) { + return -1; + } + } + + /** + * Tells the underlying networking system that the caller is finished + * using the named feature. The interpretation of {@code feature} + * is completely up to each networking implementation. + * @param networkType specifies which network the request pertains to + * @param feature the name of the feature that is no longer needed + * @return an integer value representing the outcome of the request. + * The interpretation of this value is specific to each networking + * implementation+feature combination, except that the value {@code -1} + * always indicates failure. + */ + public int stopUsingNetworkFeature(int networkType, String feature) { + try { + return mService.stopUsingNetworkFeature(networkType, feature); + } catch (RemoteException e) { + return -1; + } + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. An attempt to add a route that + * already exists is ignored, but treated as successful. + * @param networkType the type of the network over which traffic to the specified + * host is to be routed + * @param hostAddress the IP address of the host to which the route is desired + * @return {@code true} on success, {@code false} on failure + */ + public boolean requestRouteToHost(int networkType, int hostAddress) { + try { + return mService.requestRouteToHost(networkType, hostAddress); + } catch (RemoteException e) { + return false; + } + } + + /** + * Don't allow use of default constructor. + */ + @SuppressWarnings({"UnusedDeclaration"}) + private ConnectivityManager() { + } + + /** + * {@hide} + */ + public ConnectivityManager(IConnectivityManager service) { + if (service == null) { + throw new IllegalArgumentException( + "ConnectivityManager() cannot be constructed with null service"); + } + mService = service; + } +} diff --git a/core/java/android/net/Credentials.java b/core/java/android/net/Credentials.java new file mode 100644 index 0000000..7f6cf9d --- /dev/null +++ b/core/java/android/net/Credentials.java @@ -0,0 +1,48 @@ +/* + * 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 android.net; + +/** + * A class for representing UNIX credentials passed via ancillary data + * on UNIX domain sockets. See "man 7 unix" on a desktop linux distro. + */ +public class Credentials { + /** pid of process. root peers may lie. */ + private final int pid; + /** uid of process. root peers may lie. */ + private final int uid; + /** gid of process. root peers may lie. */ + private final int gid; + + public Credentials (int pid, int uid, int gid) { + this.pid = pid; + this.uid = uid; + this.gid = gid; + } + + public int getPid() { + return pid; + } + + public int getUid() { + return uid; + } + + public int getGid() { + return gid; + } +} diff --git a/core/java/android/net/DhcpInfo.aidl b/core/java/android/net/DhcpInfo.aidl new file mode 100644 index 0000000..29cd21f --- /dev/null +++ b/core/java/android/net/DhcpInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 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. + * 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 android.net; + +parcelable DhcpInfo; diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java new file mode 100644 index 0000000..1178bec --- /dev/null +++ b/core/java/android/net/DhcpInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import android.os.Parcelable; +import android.os.Parcel; + +/** + * A simple object for retrieving the results of a DHCP request. + */ +public class DhcpInfo implements Parcelable { + public int ipAddress; + public int gateway; + public int netmask; + + public int dns1; + public int dns2; + + public int serverAddress; + public int leaseDuration; + + public DhcpInfo() { + super(); + } + + public String toString() { + StringBuffer str = new StringBuffer(); + + str.append("ipaddr "); putAddress(str, ipAddress); + str.append(" gateway "); putAddress(str, gateway); + str.append(" netmask "); putAddress(str, netmask); + str.append(" dns1 "); putAddress(str, dns1); + str.append(" dns2 "); putAddress(str, dns2); + str.append(" DHCP server "); putAddress(str, serverAddress); + str.append(" lease ").append(leaseDuration).append(" seconds"); + + return str.toString(); + } + + private static void putAddress(StringBuffer buf, int addr) { + buf.append(addr & 0xff).append('.'). + append((addr >>>= 8) & 0xff).append('.'). + append((addr >>>= 8) & 0xff).append('.'). + append((addr >>>= 8) & 0xff); + } + + /** Implement the Parcelable interface {@hide} */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface {@hide} */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(ipAddress); + dest.writeInt(gateway); + dest.writeInt(netmask); + dest.writeInt(dns1); + dest.writeInt(dns2); + dest.writeInt(serverAddress); + dest.writeInt(leaseDuration); + } + + /** Implement the Parcelable interface {@hide} */ + public static final Creator<DhcpInfo> CREATOR = + new Creator<DhcpInfo>() { + public DhcpInfo createFromParcel(Parcel in) { + DhcpInfo info = new DhcpInfo(); + info.ipAddress = in.readInt(); + info.gateway = in.readInt(); + info.netmask = in.readInt(); + info.dns1 = in.readInt(); + info.dns2 = in.readInt(); + info.serverAddress = in.readInt(); + info.leaseDuration = in.readInt(); + return info; + } + + public DhcpInfo[] newArray(int size) { + return new DhcpInfo[size]; + } + }; +} diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl new file mode 100644 index 0000000..e1d921f --- /dev/null +++ b/core/java/android/net/IConnectivityManager.aidl @@ -0,0 +1,47 @@ +/** + * Copyright (c) 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. + * 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 android.net; + +import android.net.NetworkInfo; + +/** + * Interface that answers queries about, and allows changing, the + * state of network connectivity. + */ +/** {@hide} */ +interface IConnectivityManager +{ + void setNetworkPreference(int pref); + + int getNetworkPreference(); + + NetworkInfo getActiveNetworkInfo(); + + NetworkInfo getNetworkInfo(int networkType); + + NetworkInfo[] getAllNetworkInfo(); + + boolean setRadios(boolean onOff); + + boolean setRadio(int networkType, boolean turnOn); + + int startUsingNetworkFeature(int networkType, in String feature); + + int stopUsingNetworkFeature(int networkType, in String feature); + + boolean requestRouteToHost(int networkType, int hostAddress); +} diff --git a/core/java/android/net/LocalServerSocket.java b/core/java/android/net/LocalServerSocket.java new file mode 100644 index 0000000..2b93fc2 --- /dev/null +++ b/core/java/android/net/LocalServerSocket.java @@ -0,0 +1,117 @@ +/* + * 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 android.net; + +import java.io.IOException; +import java.io.FileDescriptor; + +/** + * non-standard class for creating inbound UNIX-domain socket + * on the Android platform, this is created in the Linux non-filesystem + * namespace. + * + * On simulator platforms, this may be created in a temporary directory on + * the filesystem + */ +public class LocalServerSocket { + private final LocalSocketImpl impl; + private final LocalSocketAddress localAddress; + + /** 50 seems a bit much, but it's what was here */ + private static final int LISTEN_BACKLOG = 50; + + /** + * Crewates a new server socket listening at specified name. + * On the Android platform, the name is created in the Linux + * abstract namespace (instead of on the filesystem). + * + * @param name address for socket + * @throws IOException + */ + public LocalServerSocket(String name) throws IOException + { + impl = new LocalSocketImpl(); + + impl.create(true); + + localAddress = new LocalSocketAddress(name); + impl.bind(localAddress); + + impl.listen(LISTEN_BACKLOG); + } + + /** + * Create a LocalServerSocket from a file descriptor that's already + * been created and bound. listen() will be called immediately on it. + * Used for cases where file descriptors are passed in via environment + * variables + * + * @param fd bound file descriptor + * @throws IOException + */ + public LocalServerSocket(FileDescriptor fd) throws IOException + { + impl = new LocalSocketImpl(fd); + impl.listen(LISTEN_BACKLOG); + localAddress = impl.getSockAddress(); + } + + /** + * Obtains the socket's local address + * + * @return local address + */ + public LocalSocketAddress getLocalSocketAddress() + { + return localAddress; + } + + /** + * Accepts a new connection to the socket. Blocks until a new + * connection arrives. + * + * @return a socket representing the new connection. + * @throws IOException + */ + public LocalSocket accept() throws IOException + { + LocalSocketImpl acceptedImpl = new LocalSocketImpl(); + + impl.accept (acceptedImpl); + + return new LocalSocket(acceptedImpl); + } + + /** + * Returns file descriptor or null if not yet open/already closed + * + * @return fd or null + */ + public FileDescriptor getFileDescriptor() { + return impl.getFileDescriptor(); + } + + /** + * Closes server socket. + * + * @throws IOException + */ + public void close() throws IOException + { + impl.close(); + } +} diff --git a/core/java/android/net/LocalSocket.java b/core/java/android/net/LocalSocket.java new file mode 100644 index 0000000..4039a69 --- /dev/null +++ b/core/java/android/net/LocalSocket.java @@ -0,0 +1,288 @@ +/* + * 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 android.net; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketOptions; + +/** + * Creates a (non-server) socket in the UNIX-domain namespace. The interface + * here is not entirely unlike that of java.net.Socket + */ +public class LocalSocket { + + private LocalSocketImpl impl; + private volatile boolean implCreated; + private LocalSocketAddress localAddress; + private boolean isBound; + private boolean isConnected; + + /** + * Creates a AF_LOCAL/UNIX domain stream socket. + */ + public LocalSocket() { + this(new LocalSocketImpl()); + isBound = false; + isConnected = false; + } + + /** + * for use with AndroidServerSocket + * @param impl a SocketImpl + */ + /*package*/ LocalSocket(LocalSocketImpl impl) { + this.impl = impl; + this.isConnected = false; + this.isBound = false; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return super.toString() + " impl:" + impl; + } + + /** + * It's difficult to discern from the spec when impl.create() should be + * called, but it seems like a reasonable rule is "as soon as possible, + * but not in a context where IOException cannot be thrown" + * + * @throws IOException from SocketImpl.create() + */ + private void implCreateIfNeeded() throws IOException { + if (!implCreated) { + synchronized (this) { + if (!implCreated) { + implCreated = true; + impl.create(true); + } + } + } + } + + /** + * Connects this socket to an endpoint. May only be called on an instance + * that has not yet been connected. + * + * @param endpoint endpoint address + * @throws IOException if socket is in invalid state or the address does + * not exist. + */ + public void connect(LocalSocketAddress endpoint) throws IOException { + synchronized (this) { + if (isConnected) { + throw new IOException("already connected"); + } + + implCreateIfNeeded(); + impl.connect(endpoint, 0); + isConnected = true; + isBound = true; + } + } + + /** + * Binds this socket to an endpoint name. May only be called on an instance + * that has not yet been bound. + * + * @param bindpoint endpoint address + * @throws IOException + */ + public void bind(LocalSocketAddress bindpoint) throws IOException { + implCreateIfNeeded(); + + synchronized (this) { + if (isBound) { + throw new IOException("already bound"); + } + + localAddress = bindpoint; + impl.bind(localAddress); + isBound = true; + } + } + + /** + * Retrieves the name that this socket is bound to, if any. + * + * @return Local address or null if anonymous + */ + public LocalSocketAddress getLocalSocketAddress() { + return localAddress; + } + + /** + * Retrieves the input stream for this instance. + * + * @return input stream + * @throws IOException if socket has been closed or cannot be created. + */ + public InputStream getInputStream() throws IOException { + implCreateIfNeeded(); + return impl.getInputStream(); + } + + /** + * Retrieves the output stream for this instance. + * + * @return output stream + * @throws IOException if socket has been closed or cannot be created. + */ + public OutputStream getOutputStream() throws IOException { + implCreateIfNeeded(); + return impl.getOutputStream(); + } + + /** + * Closes the socket. + * + * @throws IOException + */ + public void close() throws IOException { + implCreateIfNeeded(); + impl.close(); + } + + /** + * Shuts down the input side of the socket. + * + * @throws IOException + */ + public void shutdownInput() throws IOException { + implCreateIfNeeded(); + impl.shutdownInput(); + } + + /** + * Shuts down the output side of the socket. + * + * @throws IOException + */ + public void shutdownOutput() throws IOException { + implCreateIfNeeded(); + impl.shutdownOutput(); + } + + public void setReceiveBufferSize(int size) throws IOException { + impl.setOption(SocketOptions.SO_RCVBUF, Integer.valueOf(size)); + } + + public int getReceiveBufferSize() throws IOException { + return ((Integer) impl.getOption(SocketOptions.SO_RCVBUF)).intValue(); + } + + public void setSoTimeout(int n) throws IOException { + impl.setOption(SocketOptions.SO_TIMEOUT, Integer.valueOf(n)); + } + + public int getSoTimeout() throws IOException { + return ((Integer) impl.getOption(SocketOptions.SO_TIMEOUT)).intValue(); + } + + public void setSendBufferSize(int n) throws IOException { + impl.setOption(SocketOptions.SO_SNDBUF, Integer.valueOf(n)); + } + + public int getSendBufferSize() throws IOException { + return ((Integer) impl.getOption(SocketOptions.SO_SNDBUF)).intValue(); + } + + //???SEC + public LocalSocketAddress getRemoteSocketAddress() { + throw new UnsupportedOperationException(); + } + + //???SEC + public synchronized boolean isConnected() { + return isConnected; + } + + //???SEC + public boolean isClosed() { + throw new UnsupportedOperationException(); + } + + //???SEC + public synchronized boolean isBound() { + return isBound; + } + + //???SEC + public boolean isOutputShutdown() { + throw new UnsupportedOperationException(); + } + + //???SEC + public boolean isInputShutdown() { + throw new UnsupportedOperationException(); + } + + //???SEC + public void connect(LocalSocketAddress endpoint, int timeout) + throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * Enqueues a set of file descriptors to send to the peer. The queue + * is one deep. The file descriptors will be sent with the next write + * of normal data, and will be delivered in a single ancillary message. + * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine. + * + * @param fds non-null; file descriptors to send. + */ + public void setFileDescriptorsForSend(FileDescriptor[] fds) { + impl.setFileDescriptorsForSend(fds); + } + + /** + * Retrieves a set of file descriptors that a peer has sent through + * an ancillary message. This method retrieves the most recent set sent, + * and then returns null until a new set arrives. + * File descriptors may only be passed along with regular data, so this + * method can only return a non-null after a read operation. + * + * @return null or file descriptor array + * @throws IOException + */ + public FileDescriptor[] getAncillaryFileDescriptors() throws IOException { + return impl.getAncillaryFileDescriptors(); + } + + /** + * Retrieves the credentials of this socket's peer. Only valid on + * connected sockets. + * + * @return non-null; peer credentials + * @throws IOException + */ + public Credentials getPeerCredentials() throws IOException { + return impl.getPeerCredentials(); + } + + /** + * Returns file descriptor or null if not yet open/already closed + * + * @return fd or null + */ + public FileDescriptor getFileDescriptor() { + return impl.getFileDescriptor(); + } +} diff --git a/core/java/android/net/LocalSocketAddress.java b/core/java/android/net/LocalSocketAddress.java new file mode 100644 index 0000000..8265b85 --- /dev/null +++ b/core/java/android/net/LocalSocketAddress.java @@ -0,0 +1,100 @@ +/* + * 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 android.net; + +/** + * A UNIX-domain (AF_LOCAL) socket address. For use with + * android.net.LocalSocket and android.net.LocalServerSocket. + * + * On the Android system, these names refer to names in the Linux + * abstract (non-filesystem) UNIX domain namespace. + */ +public class LocalSocketAddress +{ + /** + * The namespace that this address exists in. See also + * include/cutils/sockets.h ANDROID_SOCKET_NAMESPACE_* + */ + public enum Namespace { + /** A socket in the Linux abstract namespace */ + ABSTRACT(0), + /** + * A socket in the Android reserved namespace in /dev/socket. + * Only the init process may create a socket here. + */ + RESERVED(1), + /** + * A socket named with a normal filesystem path. + */ + FILESYSTEM(2); + + /** The id matches with a #define in include/cutils/sockets.h */ + private int id; + Namespace (int id) { + this.id = id; + } + + /** + * @return int constant shared with native code + */ + /*package*/ int getId() { + return id; + } + } + + private final String name; + private final Namespace namespace; + + /** + * Creates an instance with a given name. + * + * @param name non-null name + * @param namespace namespace the name should be created in. + */ + public LocalSocketAddress(String name, Namespace namespace) { + this.name = name; + this.namespace = namespace; + } + + /** + * Creates an instance with a given name in the {@link Namespace#ABSTRACT} + * namespace + * + * @param name non-null name + */ + public LocalSocketAddress(String name) { + this(name,Namespace.ABSTRACT); + } + + /** + * Retrieves the string name of this address + * @return string name + */ + public String getName() + { + return name; + } + + /** + * Returns the namespace used by this address. + * + * @return non-null a namespace + */ + public Namespace getNamespace() { + return namespace; + } +} diff --git a/core/java/android/net/LocalSocketImpl.java b/core/java/android/net/LocalSocketImpl.java new file mode 100644 index 0000000..6c36a7d --- /dev/null +++ b/core/java/android/net/LocalSocketImpl.java @@ -0,0 +1,490 @@ +/* + * 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 android.net; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.FileDescriptor; +import java.net.SocketOptions; + +/** + * Socket implementation used for android.net.LocalSocket and + * android.net.LocalServerSocket. Supports only AF_LOCAL sockets. + */ +class LocalSocketImpl +{ + private SocketInputStream fis; + private SocketOutputStream fos; + private Object readMonitor = new Object(); + private Object writeMonitor = new Object(); + + /** null if closed or not yet created */ + private FileDescriptor fd; + + // These fields are accessed by native code; + /** file descriptor array received during a previous read */ + FileDescriptor[] inboundFileDescriptors; + /** file descriptor array that should be written during next write */ + FileDescriptor[] outboundFileDescriptors; + + /** + * An input stream for local sockets. Needed because we may + * need to read ancillary data. + */ + class SocketInputStream extends InputStream { + /** {@inheritDoc} */ + @Override + public int available() throws IOException { + return available_native(fd); + } + + /** {@inheritDoc} */ + @Override + public void close() throws IOException { + LocalSocketImpl.this.close(); + } + + /** {@inheritDoc} */ + @Override + public int read() throws IOException { + int ret; + synchronized (readMonitor) { + FileDescriptor myFd = fd; + if (myFd == null) throw new IOException("socket closed"); + + ret = read_native(myFd); + return ret; + } + } + + /** {@inheritDoc} */ + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + /** {@inheritDoc} */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + synchronized (readMonitor) { + FileDescriptor myFd = fd; + if (myFd == null) throw new IOException("socket closed"); + + if (off < 0 || len < 0 || (off + len) > b.length ) { + throw new ArrayIndexOutOfBoundsException(); + } + + int ret = readba_native(b, off, len, myFd); + + return ret; + } + } + } + + /** + * An output stream for local sockets. Needed because we may + * need to read ancillary data. + */ + class SocketOutputStream extends OutputStream { + /** {@inheritDoc} */ + @Override + public void close() throws IOException { + LocalSocketImpl.this.close(); + } + + /** {@inheritDoc} */ + @Override + public void write (byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** {@inheritDoc} */ + @Override + public void write (byte[] b, int off, int len) throws IOException { + synchronized (writeMonitor) { + FileDescriptor myFd = fd; + if (myFd == null) throw new IOException("socket closed"); + + if (off < 0 || len < 0 || (off + len) > b.length ) { + throw new ArrayIndexOutOfBoundsException(); + } + writeba_native(b, off, len, myFd); + } + } + + /** {@inheritDoc} */ + @Override + public void write (int b) throws IOException { + synchronized (writeMonitor) { + FileDescriptor myFd = fd; + if (myFd == null) throw new IOException("socket closed"); + write_native(b, myFd); + } + } + } + + private native int available_native(FileDescriptor fd) throws IOException; + private native void close_native(FileDescriptor fd) throws IOException; + private native int read_native(FileDescriptor fd) throws IOException; + private native int readba_native(byte[] b, int off, int len, + FileDescriptor fd) throws IOException; + private native void writeba_native(byte[] b, int off, int len, + FileDescriptor fd) throws IOException; + private native void write_native(int b, FileDescriptor fd) + throws IOException; + private native void connectLocal(FileDescriptor fd, String name, + int namespace) throws IOException; + private native void bindLocal(FileDescriptor fd, String name, int namespace) + throws IOException; + private native FileDescriptor create_native(boolean stream) + throws IOException; + private native void listen_native(FileDescriptor fd, int backlog) + throws IOException; + private native void shutdown(FileDescriptor fd, boolean shutdownInput); + private native Credentials getPeerCredentials_native( + FileDescriptor fd) throws IOException; + private native int getOption_native(FileDescriptor fd, int optID) + throws IOException; + private native void setOption_native(FileDescriptor fd, int optID, + int b, int value) throws IOException; + +// private native LocalSocketAddress getSockName_native +// (FileDescriptor fd) throws IOException; + + /** + * Accepts a connection on a server socket. + * + * @param fd file descriptor of server socket + * @param s socket implementation that will become the new socket + * @return file descriptor of new socket + */ + private native FileDescriptor accept + (FileDescriptor fd, LocalSocketImpl s) throws IOException; + + /** + * Create a new instance. + */ + /*package*/ LocalSocketImpl() + { + } + + /** + * Create a new instance from a file descriptor representing + * a bound socket. The state of the file descriptor is not checked here + * but the caller can verify socket state by calling listen(). + * + * @param fd non-null; bound file descriptor + */ + /*package*/ LocalSocketImpl(FileDescriptor fd) throws IOException + { + this.fd = fd; + } + + public String toString() { + return super.toString() + " fd:" + fd; + } + + /** + * Creates a socket in the underlying OS. + * + * @param stream true if this should be a stream socket, false for + * datagram. + * @throws IOException + */ + public void create (boolean stream) throws IOException { + // no error if socket already created + // need this for LocalServerSocket.accept() + if (fd == null) { + fd = create_native(stream); + } + } + + /** + * Closes the socket. + * + * @throws IOException + */ + public void close() throws IOException { + synchronized (LocalSocketImpl.this) { + if (fd == null) return; + close_native(fd); + fd = null; + } + } + + /** note timeout presently ignored */ + protected void connect(LocalSocketAddress address, int timeout) + throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + connectLocal(fd, address.getName(), address.getNamespace().getId()); + } + + /** + * Binds this socket to an endpoint name. May only be called on an instance + * that has not yet been bound. + * + * @param endpoint endpoint address + * @throws IOException + */ + public void bind(LocalSocketAddress endpoint) throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + bindLocal(fd, endpoint.getName(), endpoint.getNamespace().getId()); + } + + protected void listen(int backlog) throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + listen_native(fd, backlog); + } + + /** + * Accepts a new connection to the socket. Blocks until a new + * connection arrives. + * + * @param s a socket that will be used to represent the new connection. + * @throws IOException + */ + protected void accept(LocalSocketImpl s) throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + s.fd = accept(fd, s); + } + + /** + * Retrieves the input stream for this instance. + * + * @return input stream + * @throws IOException if socket has been closed or cannot be created. + */ + protected InputStream getInputStream() throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + synchronized (this) { + if (fis == null) { + fis = new SocketInputStream(); + } + + return fis; + } + } + + /** + * Retrieves the output stream for this instance. + * + * @return output stream + * @throws IOException if socket has been closed or cannot be created. + */ + protected OutputStream getOutputStream() throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + synchronized (this) { + if (fos == null) { + fos = new SocketOutputStream(); + } + + return fos; + } + } + + /** + * Returns the number of bytes available for reading without blocking. + * + * @return >= 0 count bytes available + * @throws IOException + */ + protected int available() throws IOException + { + return getInputStream().available(); + } + + /** + * Shuts down the input side of the socket. + * + * @throws IOException + */ + protected void shutdownInput() throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + shutdown(fd, true); + } + + /** + * Shuts down the output side of the socket. + * + * @throws IOException + */ + protected void shutdownOutput() throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + shutdown(fd, false); + } + + protected FileDescriptor getFileDescriptor() + { + return fd; + } + + protected boolean supportsUrgentData() + { + return false; + } + + protected void sendUrgentData(int data) throws IOException + { + throw new RuntimeException ("not impled"); + } + + public Object getOption(int optID) throws IOException + { + if (fd == null) { + throw new IOException("socket not created"); + } + + if (optID == SocketOptions.SO_TIMEOUT) { + return 0; + } + + int value = getOption_native(fd, optID); + switch (optID) + { + case SocketOptions.SO_RCVBUF: + case SocketOptions.SO_SNDBUF: + return value; + case SocketOptions.SO_REUSEADDR: + default: + return value; + } + } + + public void setOption(int optID, Object value) + throws IOException { + /* + * Boolean.FALSE is used to disable some options, so it + * is important to distinguish between FALSE and unset. + * We define it here that -1 is unset, 0 is FALSE, and 1 + * is TRUE. + */ + int boolValue = -1; + int intValue = 0; + + if (fd == null) { + throw new IOException("socket not created"); + } + + if (value instanceof Integer) { + intValue = (Integer)value; + } else if (value instanceof Boolean) { + boolValue = ((Boolean) value)? 1 : 0; + } else { + throw new IOException("bad value: " + value); + } + + setOption_native(fd, optID, boolValue, intValue); + } + + /** + * Enqueues a set of file descriptors to send to the peer. The queue + * is one deep. The file descriptors will be sent with the next write + * of normal data, and will be delivered in a single ancillary message. + * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine. + * + * @param fds non-null; file descriptors to send. + * @throws IOException + */ + public void setFileDescriptorsForSend(FileDescriptor[] fds) { + synchronized(writeMonitor) { + outboundFileDescriptors = fds; + } + } + + /** + * Retrieves a set of file descriptors that a peer has sent through + * an ancillary message. This method retrieves the most recent set sent, + * and then returns null until a new set arrives. + * File descriptors may only be passed along with regular data, so this + * method can only return a non-null after a read operation. + * + * @return null or file descriptor array + * @throws IOException + */ + public FileDescriptor[] getAncillaryFileDescriptors() throws IOException { + synchronized(readMonitor) { + FileDescriptor[] result = inboundFileDescriptors; + + inboundFileDescriptors = null; + return result; + } + } + + /** + * Retrieves the credentials of this socket's peer. Only valid on + * connected sockets. + * + * @return non-null; peer credentials + * @throws IOException + */ + public Credentials getPeerCredentials() throws IOException + { + return getPeerCredentials_native(fd); + } + + /** + * Retrieves the socket name from the OS. + * + * @return non-null; socket name + * @throws IOException on failure + */ + public LocalSocketAddress getSockAddress() throws IOException + { + return null; + //TODO implement this + //return getSockName_native(fd); + } + + @Override + protected void finalize() throws IOException { + close(); + } +} + diff --git a/core/java/android/net/MailTo.java b/core/java/android/net/MailTo.java new file mode 100644 index 0000000..ca28f86 --- /dev/null +++ b/core/java/android/net/MailTo.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * + * MailTo URL parser + * + * This class parses a mailto scheme URL and then can be queried for + * the parsed parameters. This implements RFC 2368. + * + */ +public class MailTo { + + static public final String MAILTO_SCHEME = "mailto:"; + + // All the parsed content is added to the headers. + private HashMap<String, String> mHeaders; + + // Well known headers + static private final String TO = "to"; + static private final String BODY = "body"; + static private final String CC = "cc"; + static private final String SUBJECT = "subject"; + + + /** + * Test to see if the given string is a mailto URL + * @param url string to be tested + * @return true if the string is a mailto URL + */ + public static boolean isMailTo(String url) { + if (url != null && url.startsWith(MAILTO_SCHEME)) { + return true; + } + return false; + } + + /** + * Parse and decode a mailto scheme string. This parser implements + * RFC 2368. The returned object can be queried for the parsed parameters. + * @param url String containing a mailto URL + * @return MailTo object + * @exception ParseException if the scheme is not a mailto URL + */ + public static MailTo parse(String url) throws ParseException { + if (url == null) { + throw new NullPointerException(); + } + if (!isMailTo(url)) { + throw new ParseException("Not a mailto scheme"); + } + // Strip the scheme as the Uri parser can't cope with it. + String noScheme = url.substring(MAILTO_SCHEME.length()); + Uri email = Uri.parse(noScheme); + MailTo m = new MailTo(); + + // Parse out the query parameters + String query = email.getQuery(); + if (query != null ) { + String[] queries = query.split("&"); + for (String q : queries) { + String[] nameval = q.split("="); + if (nameval.length == 0) { + continue; + } + // insert the headers with the name in lowercase so that + // we can easily find common headers + m.mHeaders.put(Uri.decode(nameval[0]).toLowerCase(), + nameval.length > 1 ? Uri.decode(nameval[1]) : null); + } + } + + // Address can be specified in both the headers and just after the + // mailto line. Join the two together. + String address = email.getPath(); + if (address != null) { + String addr = m.getTo(); + if (addr != null) { + address += ", " + addr; + } + m.mHeaders.put(TO, address); + } + + return m; + } + + /** + * Retrieve the To address line from the parsed mailto URL. This could be + * several email address that are comma-space delimited. + * If no To line was specified, then null is return + * @return comma delimited email addresses or null + */ + public String getTo() { + return mHeaders.get(TO); + } + + /** + * Retrieve the CC address line from the parsed mailto URL. This could be + * several email address that are comma-space delimited. + * If no CC line was specified, then null is return + * @return comma delimited email addresses or null + */ + public String getCc() { + return mHeaders.get(CC); + } + + /** + * Retrieve the subject line from the parsed mailto URL. + * If no subject line was specified, then null is return + * @return subject or null + */ + public String getSubject() { + return mHeaders.get(SUBJECT); + } + + /** + * Retrieve the body line from the parsed mailto URL. + * If no body line was specified, then null is return + * @return body or null + */ + public String getBody() { + return mHeaders.get(BODY); + } + + /** + * Retrieve all the parsed email headers from the mailto URL + * @return map containing all parsed values + */ + public Map<String, String> getHeaders() { + return mHeaders; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(MAILTO_SCHEME); + sb.append('?'); + for (Map.Entry<String,String> header : mHeaders.entrySet()) { + sb.append(Uri.encode(header.getKey())); + sb.append('='); + sb.append(Uri.encode(header.getValue())); + sb.append('&'); + } + return sb.toString(); + } + + /** + * Private constructor. The only way to build a Mailto object is through + * the parse() method. + */ + private MailTo() { + mHeaders = new HashMap<String, String>(); + } +} diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java new file mode 100644 index 0000000..ae74e6f --- /dev/null +++ b/core/java/android/net/MobileDataStateTracker.java @@ -0,0 +1,479 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.RemoteException; +import android.os.Handler; +import android.os.ServiceManager; +import android.os.SystemProperties; +import com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.TelephonyIntents; +import android.net.NetworkInfo.DetailedState; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.Config; +import android.text.TextUtils; + +import java.util.List; +import java.util.ArrayList; + +/** + * Track the state of mobile data connectivity. This is done by + * receiving broadcast intents from the Phone process whenever + * the state of data connectivity changes. + * + * {@hide} + */ +public class MobileDataStateTracker extends NetworkStateTracker { + + private static final String TAG = "MobileDataStateTracker"; + private static final boolean DBG = false; + + private Phone.DataState mMobileDataState; + private ITelephony mPhoneService; + private static final String[] sDnsPropNames = { + "net.rmnet0.dns1", + "net.rmnet0.dns2", + "net.eth0.dns1", + "net.eth0.dns2", + "net.eth0.dns3", + "net.eth0.dns4", + "net.gprs.dns1", + "net.gprs.dns2" + }; + private List<String> mDnsServers; + private String mInterfaceName; + private int mDefaultGatewayAddr; + private int mLastCallingPid = -1; + + /** + * Create a new MobileDataStateTracker + * @param context the application context of the caller + * @param target a message handler for getting callbacks about state changes + */ + public MobileDataStateTracker(Context context, Handler target) { + super(context, target, ConnectivityManager.TYPE_MOBILE); + mPhoneService = null; + mDnsServers = new ArrayList<String>(); + } + + /** + * Begin monitoring mobile data connectivity. + */ + public void startMonitoring() { + + IntentFilter filter = new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED); + + Intent intent = mContext.registerReceiver(new MobileDataStateReceiver(), filter); + if (intent != null) + mMobileDataState = getMobileDataState(intent); + else + mMobileDataState = Phone.DataState.DISCONNECTED; + } + + private static Phone.DataState getMobileDataState(Intent intent) { + String str = intent.getStringExtra(Phone.STATE_KEY); + if (str != null) + return Enum.valueOf(Phone.DataState.class, str); + else + return Phone.DataState.DISCONNECTED; + } + + private class MobileDataStateReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { + Phone.DataState state = getMobileDataState(intent); + String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY); + String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); + boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, false); + if (DBG) Log.d(TAG, "Received " + intent.getAction() + + " broadcast - state = " + state + + ", unavailable = " + unavailable + + ", reason = " + (reason == null ? "(unspecified)" : reason)); + mNetworkInfo.setIsAvailable(!unavailable); + if (mMobileDataState != state) { + mMobileDataState = state; + + switch (state) { + case DISCONNECTED: + setDetailedState(DetailedState.DISCONNECTED, reason, apnName); + if (mInterfaceName != null) { + NetworkUtils.resetConnections(mInterfaceName); + } + mInterfaceName = null; + mDefaultGatewayAddr = 0; + break; + case CONNECTING: + setDetailedState(DetailedState.CONNECTING, reason, apnName); + break; + case SUSPENDED: + setDetailedState(DetailedState.SUSPENDED, reason, apnName); + break; + case CONNECTED: + mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY); + if (mInterfaceName == null) { + Log.d(TAG, "CONNECTED event did not supply interface name."); + } + setupDnsProperties(); + setDetailedState(DetailedState.CONNECTED, reason, apnName); + break; + } + } + } else if (intent.getAction().equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) { + String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY); + String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); + if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" + + reason == null ? "" : "(" + reason + ")"); + setDetailedState(DetailedState.FAILED, reason, apnName); + } + } + } + + /** + * Make sure that route(s) exist to the carrier DNS server(s). + */ + public void addPrivateRoutes() { + if (mInterfaceName != null) { + for (String addrString : mDnsServers) { + int addr = NetworkUtils.lookupHost(addrString); + if (addr != -1) { + NetworkUtils.addHostRoute(mInterfaceName, addr); + } + } + } + } + + public void removePrivateRoutes() { + if(mInterfaceName != null) { + NetworkUtils.removeHostRoutes(mInterfaceName); + } + } + + public void removeDefaultRoute() { + if(mInterfaceName != null) { + mDefaultGatewayAddr = NetworkUtils.getDefaultRoute(mInterfaceName); + NetworkUtils.removeDefaultRoute(mInterfaceName); + } + } + + public void restoreDefaultRoute() { + // 0 is not a valid address for a gateway + if (mInterfaceName != null && mDefaultGatewayAddr != 0) { + NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr); + } + } + + private void getPhoneService(boolean forceRefresh) { + if ((mPhoneService == null) || forceRefresh) { + mPhoneService = ITelephony.Stub.asInterface(ServiceManager.getService("phone")); + } + } + + /** + * Report whether data connectivity is possible. + */ + public boolean isAvailable() { + getPhoneService(false); + + /* + * If the phone process has crashed in the past, we'll get a + * RemoteException and need to re-reference the service. + */ + for (int retry = 0; retry < 2; retry++) { + if (mPhoneService == null) break; + + try { + return mPhoneService.isDataConnectivityPossible(); + } catch (RemoteException e) { + // First-time failed, get the phone service again + if (retry == 0) getPhoneService(true); + } + } + + return false; + } + + /** + * Return the IP addresses of the DNS servers available for the mobile data + * network interface. + * @return a list of DNS addresses, with no holes. + */ + public String[] getNameServers() { + return getNameServerList(sDnsPropNames); + } + + /** + * Return the system properties name associated with the tcp buffer sizes + * for this network. + */ + public String getTcpBufferSizesPropName() { + String networkTypeStr = "unknown"; + TelephonyManager tm = new TelephonyManager(mContext); + switch(tm.getNetworkType()) { + case TelephonyManager.NETWORK_TYPE_GPRS: + networkTypeStr = "gprs"; + break; + case TelephonyManager.NETWORK_TYPE_EDGE: + networkTypeStr = "edge"; + break; + case TelephonyManager.NETWORK_TYPE_UMTS: + networkTypeStr = "umts"; + break; + } + return "net.tcp.buffersize." + networkTypeStr; + } + + /** + * Tear down mobile data connectivity, i.e., disable the ability to create + * mobile data connections. + */ + @Override + public boolean teardown() { + getPhoneService(false); + /* + * If the phone process has crashed in the past, we'll get a + * RemoteException and need to re-reference the service. + */ + for (int retry = 0; retry < 2; retry++) { + if (mPhoneService == null) { + Log.w(TAG, + "Ignoring mobile data teardown request because could not acquire PhoneService"); + break; + } + + try { + return mPhoneService.disableDataConnectivity(); + } catch (RemoteException e) { + if (retry == 0) getPhoneService(true); + } + } + + Log.w(TAG, "Failed to tear down mobile data connectivity"); + return false; + } + + /** + * Re-enable mobile data connectivity after a {@link #teardown()}. + */ + public boolean reconnect() { + getPhoneService(false); + /* + * If the phone process has crashed in the past, we'll get a + * RemoteException and need to re-reference the service. + */ + for (int retry = 0; retry < 2; retry++) { + if (mPhoneService == null) { + Log.w(TAG, + "Ignoring mobile data connect request because could not acquire PhoneService"); + break; + } + + try { + return mPhoneService.enableDataConnectivity(); + } catch (RemoteException e) { + if (retry == 0) getPhoneService(true); + } + } + + Log.w(TAG, "Failed to set up mobile data connectivity"); + return false; + } + + /** + * Turn on or off the mobile radio. No connectivity will be possible while the + * radio is off. The operation is a no-op if the radio is already in the desired state. + * @param turnOn {@code true} if the radio should be turned on, {@code false} if + */ + public boolean setRadio(boolean turnOn) { + getPhoneService(false); + /* + * If the phone process has crashed in the past, we'll get a + * RemoteException and need to re-reference the service. + */ + for (int retry = 0; retry < 2; retry++) { + if (mPhoneService == null) { + Log.w(TAG, + "Ignoring mobile radio request because could not acquire PhoneService"); + break; + } + + try { + return mPhoneService.setRadio(turnOn); + } catch (RemoteException e) { + if (retry == 0) getPhoneService(true); + } + } + + Log.w(TAG, "Could not set radio power to " + (turnOn ? "on" : "off")); + return false; + } + + /** + * Tells the phone sub-system that the caller wants to + * begin using the named feature. The only supported feature at + * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application + * to specify that it wants to send and/or receive MMS data. + * @param feature the name of the feature to be used + * @param callingPid the process ID of the process that is issuing this request + * @param callingUid the user ID of the process that is issuing this request + * @return an integer value representing the outcome of the request. + * The interpretation of this value is feature-specific. + * specific, except that the value {@code -1} + * always indicates failure. For {@code Phone.FEATURE_ENABLE_MMS}, + * the other possible return values are + * <ul> + * <li>{@code Phone.APN_ALREADY_ACTIVE}</li> + * <li>{@code Phone.APN_REQUEST_STARTED}</li> + * <li>{@code Phone.APN_TYPE_NOT_AVAILABLE}</li> + * <li>{@code Phone.APN_REQUEST_FAILED}</li> + * </ul> + */ + public int startUsingNetworkFeature(String feature, int callingPid, int callingUid) { + if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) { + mLastCallingPid = callingPid; + return setEnableApn(Phone.APN_TYPE_MMS, true); + } else { + return -1; + } + } + + /** + * Tells the phone sub-system that the caller is finished is + * finished using the named feature. The only supported feature at + * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application + * to specify that it wants to send and/or receive MMS data. + * @param feature the name of the feature that is no longer needed + * @param callingPid the process ID of the process that is issuing this request + * @param callingUid the user ID of the process that is issuing this request + * @return an integer value representing the outcome of the request. + * The interpretation of this value is feature-specific, except that + * the value {@code -1} always indicates failure. + */ + public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid) { + if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) { + return setEnableApn(Phone.APN_TYPE_MMS, false); + } else { + return -1; + } + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the mobile data network. + * @param hostAddress the IP address of the host to which the route is desired, + * in network byte order. + * @return {@code true} on success, {@code false} on failure + */ + @Override + public boolean requestRouteToHost(int hostAddress) { + if (mInterfaceName != null && hostAddress != -1) { + if (DBG) { + Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress)); + } + return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0; + } else { + return false; + } + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer("Mobile data state: "); + + sb.append(mMobileDataState); + return sb.toString(); + } + + private void setupDnsProperties() { + mDnsServers.clear(); + // Set up per-process DNS server list on behalf of the MMS process + int i = 1; + if (mInterfaceName != null) { + for (String propName : sDnsPropNames) { + if (propName.indexOf(mInterfaceName) != -1) { + String propVal = SystemProperties.get(propName); + if (propVal != null && propVal.length() != 0 && !propVal.equals("0.0.0.0")) { + mDnsServers.add(propVal); + if (mLastCallingPid != -1) { + SystemProperties.set("net.dns" + i + "." + mLastCallingPid, propVal); + } + ++i; + } + } + } + } + if (i == 1) { + Log.d(TAG, "DNS server addresses are not known."); + } else if (mLastCallingPid != -1) { + /* + * Bump the property that tells the name resolver library + * to reread the DNS server list from the properties. + */ + String propVal = SystemProperties.get("net.dnschange"); + if (propVal.length() != 0) { + try { + int n = Integer.parseInt(propVal); + SystemProperties.set("net.dnschange", "" + (n+1)); + } catch (NumberFormatException e) { + } + } + } + mLastCallingPid = -1; + } + + /** + * Internal method supporting the ENABLE_MMS feature. + * @param apnType the type of APN to be enabled or disabled (e.g., mms) + * @param enable {@code true} to enable the specified APN type, + * {@code false} to disable it. + * @return an integer value representing the outcome of the request. + */ + private int setEnableApn(String apnType, boolean enable) { + getPhoneService(false); + /* + * If the phone process has crashed in the past, we'll get a + * RemoteException and need to re-reference the service. + */ + for (int retry = 0; retry < 2; retry++) { + if (mPhoneService == null) { + Log.w(TAG, + "Ignoring feature request because could not acquire PhoneService"); + break; + } + + try { + if (enable) { + return mPhoneService.enableApnType(apnType); + } else { + return mPhoneService.disableApnType(apnType); + } + } catch (RemoteException e) { + if (retry == 0) getPhoneService(true); + } + } + + Log.w(TAG, "Could not " + (enable ? "enable" : "disable") + + " APN type \"" + apnType + "\""); + return Phone.APN_REQUEST_FAILED; + } +} diff --git a/core/java/android/net/NetworkConnectivityListener.java b/core/java/android/net/NetworkConnectivityListener.java new file mode 100644 index 0000000..858fc77 --- /dev/null +++ b/core/java/android/net/NetworkConnectivityListener.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2006 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 android.net; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * A wrapper for a broadcast receiver that provides network connectivity + * state information, independent of network type (mobile, Wi-Fi, etc.). + * {@hide} + */ +public class NetworkConnectivityListener { + private static final String TAG = "NetworkConnectivityListener"; + private static final boolean DBG = false; + + private Context mContext; + private HashMap<Handler, Integer> mHandlers = new HashMap<Handler, Integer>(); + private State mState; + private boolean mListening; + private String mReason; + private boolean mIsFailover; + + /** Network connectivity information */ + private NetworkInfo mNetworkInfo; + + /** + * In case of a Disconnect, the connectivity manager may have + * already established, or may be attempting to establish, connectivity + * with another network. If so, {@code mOtherNetworkInfo} will be non-null. + */ + private NetworkInfo mOtherNetworkInfo; + + private ConnectivityBroadcastReceiver mReceiver; + + private class ConnectivityBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION) || + mListening == false) { + Log.w(TAG, "onReceived() called with " + mState.toString() + " and " + intent); + return; + } + + boolean noConnectivity = + intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + + if (noConnectivity) { + mState = State.NOT_CONNECTED; + } else { + mState = State.CONNECTED; + } + + mNetworkInfo = (NetworkInfo) + intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + mOtherNetworkInfo = (NetworkInfo) + intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO); + + mReason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON); + mIsFailover = + intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false); + + if (DBG) { + Log.d(TAG, "onReceive(): mNetworkInfo=" + mNetworkInfo + " mOtherNetworkInfo = " + + (mOtherNetworkInfo == null ? "[none]" : mOtherNetworkInfo + + " noConn=" + noConnectivity) + " mState=" + mState.toString()); + } + + // Notifiy any handlers. + Iterator<Handler> it = mHandlers.keySet().iterator(); + while (it.hasNext()) { + Handler target = it.next(); + Message message = Message.obtain(target, mHandlers.get(target)); + target.sendMessage(message); + } + } + }; + + public enum State { + UNKNOWN, + + /** This state is returned if there is connectivity to any network **/ + CONNECTED, + /** + * This state is returned if there is no connectivity to any network. This is set + * to true under two circumstances: + * <ul> + * <li>When connectivity is lost to one network, and there is no other available + * network to attempt to switch to.</li> + * <li>When connectivity is lost to one network, and the attempt to switch to + * another network fails.</li> + */ + NOT_CONNECTED + } + + /** + * Create a new NetworkConnectivityListener. + */ + public NetworkConnectivityListener() { + mState = State.UNKNOWN; + mReceiver = new ConnectivityBroadcastReceiver(); + } + + /** + * This method starts listening for network connectivity state changes. + * @param context + */ + public synchronized void startListening(Context context) { + if (!mListening) { + mContext = context; + + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(mReceiver, filter); + mListening = true; + } + } + + /** + * This method stops this class from listening for network changes. + */ + public synchronized void stopListening() { + if (mListening) { + mContext.unregisterReceiver(mReceiver); + mContext = null; + mNetworkInfo = null; + mOtherNetworkInfo = null; + mIsFailover = false; + mReason = null; + mListening = false; + } + } + + /** + * This methods registers a Handler to be called back onto with the specified what code when + * the network connectivity state changes. + * + * @param target The target handler. + * @param what The what code to be used when posting a message to the handler. + */ + public void registerHandler(Handler target, int what) { + mHandlers.put(target, what); + } + + /** + * This methods unregisters the specified Handler. + * @param target + */ + public void unregisterHandler(Handler target) { + mHandlers.remove(target); + } + + public State getState() { + return mState; + } + + /** + * Return the NetworkInfo associated with the most recent connectivity event. + * @return {@code NetworkInfo} for the network that had the most recent connectivity event. + */ + public NetworkInfo getNetworkInfo() { + return mNetworkInfo; + } + + /** + * If the most recent connectivity event was a DISCONNECT, return + * any information supplied in the broadcast about an alternate + * network that might be available. If this returns a non-null + * value, then another broadcast should follow shortly indicating + * whether connection to the other network succeeded. + * + * @return NetworkInfo + */ + public NetworkInfo getOtherNetworkInfo() { + return mOtherNetworkInfo; + } + + /** + * Returns true if the most recent event was for an attempt to switch over to + * a new network following loss of connectivity on another network. + * @return {@code true} if this was a failover attempt, {@code false} otherwise. + */ + public boolean isFailover() { + return mIsFailover; + } + + /** + * An optional reason for the connectivity state change may have been supplied. + * This returns it. + * @return the reason for the state change, if available, or {@code null} + * otherwise. + */ + public String getReason() { + return mReason; + } +} diff --git a/core/java/android/net/NetworkInfo.aidl b/core/java/android/net/NetworkInfo.aidl new file mode 100644 index 0000000..f501873 --- /dev/null +++ b/core/java/android/net/NetworkInfo.aidl @@ -0,0 +1,19 @@ +/** + * 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 android.net; + +parcelable NetworkInfo; diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java new file mode 100644 index 0000000..f776abf --- /dev/null +++ b/core/java/android/net/NetworkInfo.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import android.os.Parcelable; +import android.os.Parcel; + +import java.util.EnumMap; + +/** + * Describes the status of a network interface of a given type + * (currently either Mobile or Wifi). + */ +public class NetworkInfo implements Parcelable { + + /** + * Coarse-grained network state. This is probably what most applications should + * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}. + * The mapping between the two is as follows: + * <br/><br/> + * <table> + * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr> + * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>SCANNING</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>CONNECTED</code></td><td<code>CONNECTED</code></td></tr> + * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr> + * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>UNAVAILABLE</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr> + * </table> + */ + public enum State { + CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN + } + + /** + * The fine-grained state of a network connection. This level of detail + * is probably of interest to few applications. Most should use + * {@link android.net.NetworkInfo.State State} instead. + */ + public enum DetailedState { + /** Ready to start data connection setup. */ + IDLE, + /** Searching for an available access point. */ + SCANNING, + /** Currently setting up data connection. */ + CONNECTING, + /** Network link established, performing authentication. */ + AUTHENTICATING, + /** Awaiting response from DHCP server in order to assign IP address information. */ + OBTAINING_IPADDR, + /** IP traffic should be available. */ + CONNECTED, + /** IP traffic is suspended */ + SUSPENDED, + /** Currently tearing down data connection. */ + DISCONNECTING, + /** IP traffic not available. */ + DISCONNECTED, + /** Attempt to connect failed. */ + FAILED + } + + /** + * This is the map described in the Javadoc comment above. The positions + * of the elements of the array must correspond to the ordinal values + * of <code>DetailedState</code>. + */ + private static final EnumMap<DetailedState, State> stateMap = + new EnumMap<DetailedState, State>(DetailedState.class); + + static { + stateMap.put(DetailedState.IDLE, State.DISCONNECTED); + stateMap.put(DetailedState.SCANNING, State.DISCONNECTED); + stateMap.put(DetailedState.CONNECTING, State.CONNECTING); + stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING); + stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING); + stateMap.put(DetailedState.CONNECTED, State.CONNECTED); + stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED); + stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING); + stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED); + stateMap.put(DetailedState.FAILED, State.DISCONNECTED); + } + + private int mNetworkType; + private State mState; + private DetailedState mDetailedState; + private String mReason; + private String mExtraInfo; + private boolean mIsFailover; + /** + * Indicates whether network connectivity is possible: + */ + private boolean mIsAvailable; + + public NetworkInfo(int type) { + if (!ConnectivityManager.isNetworkTypeValid(type)) { + throw new IllegalArgumentException("Invalid network type: " + type); + } + this.mNetworkType = type; + setDetailedState(DetailedState.IDLE, null, null); + mState = State.UNKNOWN; + mIsAvailable = true; + } + + /** + * Reports the type of network (currently mobile or Wi-Fi) to which the + * info in this object pertains. + * @return the network type + */ + public int getType() { + return mNetworkType; + } + + /** + * Indicates whether network connectivity exists or is in the process + * of being established. This is good for applications that need to + * do anything related to the network other than read or write data. + * For the latter, call {@link #isConnected()} instead, which guarantees + * that the network is fully usable. + * @return {@code true} if network connectivity exists or is in the process + * of being established, {@code false} otherwise. + */ + public boolean isConnectedOrConnecting() { + return mState == State.CONNECTED || mState == State.CONNECTING; + } + + /** + * Indicates whether network connectivity exists and it is possible to establish + * connections and pass data. + * @return {@code true} if network connectivity exists, {@code false} otherwise. + */ + public boolean isConnected() { + return mState == State.CONNECTED; + } + + /** + * Indicates whether network connectivity is possible. A network is unavailable + * when a persistent or semi-persistent condition prevents the possibility + * of connecting to that network. Examples include + * <ul> + * <li>The device is out of the coverage area for any network of this type.</li> + * <li>The device is on a network other than the home network (i.e., roaming), and + * data roaming has been disabled.</li> + * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li> + * </ul> + * @return {@code true} if the network is available, {@code false} otherwise + */ + public boolean isAvailable() { + return mIsAvailable; + } + + /** + * Sets if the network is available, ie, if the connectivity is possible. + * @param isAvailable the new availability value. + * + * {@hide} + */ + public void setIsAvailable(boolean isAvailable) { + mIsAvailable = isAvailable; + } + + /** + * Indicates whether the current attempt to connect to the network + * resulted from the ConnectivityManager trying to fail over to this + * network following a disconnect from another network. + * @return {@code true} if this is a failover attempt, {@code false} + * otherwise. + */ + public boolean isFailover() { + return mIsFailover; + } + + /** {@hide} */ + public void setFailover(boolean isFailover) { + mIsFailover = isFailover; + } + + /** + * Reports the current coarse-grained state of the network. + * @return the coarse-grained state + */ + public State getState() { + return mState; + } + + /** + * Reports the current fine-grained state of the network. + * @return the fine-grained state + */ + public DetailedState getDetailedState() { + return mDetailedState; + } + + /** + * Sets the fine-grained state of the network. + * @param detailedState the {@link DetailedState}. + * @param reason a {@code String} indicating the reason for the state change, + * if one was supplied. May be {@code null}. + * @param extraInfo an optional {@code String} providing addditional network state + * information passed up from the lower networking layers. + * + * {@hide} + */ + void setDetailedState(DetailedState detailedState, String reason, String extraInfo) { + this.mDetailedState = detailedState; + this.mState = stateMap.get(detailedState); + this.mReason = reason; + this.mExtraInfo = extraInfo; + } + + /** + * Report the reason an attempt to establish connectivity failed, + * if one is available. + * @return the reason for failure, or null if not available + */ + public String getReason() { + return mReason; + } + + /** + * Report the extra information about the network state, if any was + * provided by the lower networking layers., + * if one is available. + * @return the extra information, or null if not available + */ + public String getExtraInfo() { + return mExtraInfo; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("NetworkInfo: "); + builder.append("type: ").append(getTypeName()).append(", state: ").append(mState). + append("/").append(mDetailedState). + append(", reason: ").append(mReason == null ? "(unspecified)" : mReason). + append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo). + append(", failover: ").append(mIsFailover). + append(", isAvailable: ").append(mIsAvailable); + return builder.toString(); + } + + public String getTypeName() { + switch (mNetworkType) { + case ConnectivityManager.TYPE_WIFI: + return "WIFI"; + case ConnectivityManager.TYPE_MOBILE: + return "MOBILE"; + default: + return "<invalid>"; + } + } + + /** Implement the Parcelable interface {@hide} */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface {@hide} */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mNetworkType); + dest.writeString(mState.name()); + dest.writeString(mDetailedState.name()); + dest.writeInt(mIsFailover ? 1 : 0); + dest.writeInt(mIsAvailable ? 1 : 0); + dest.writeString(mReason); + dest.writeString(mExtraInfo); + } + + /** Implement the Parcelable interface {@hide} */ + public static final Creator<NetworkInfo> CREATOR = + new Creator<NetworkInfo>() { + public NetworkInfo createFromParcel(Parcel in) { + int netType = in.readInt(); + NetworkInfo netInfo = new NetworkInfo(netType); + netInfo.mState = State.valueOf(in.readString()); + netInfo.mDetailedState = DetailedState.valueOf(in.readString()); + netInfo.mIsFailover = in.readInt() != 0; + netInfo.mIsAvailable = in.readInt() != 0; + netInfo.mReason = in.readString(); + netInfo.mExtraInfo = in.readString(); + return netInfo; + } + + public NetworkInfo[] newArray(int size) { + return new NetworkInfo[size]; + } + }; +} diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java new file mode 100644 index 0000000..4e1efa6 --- /dev/null +++ b/core/java/android/net/NetworkStateTracker.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import java.io.FileWriter; +import java.io.IOException; + +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.os.PowerManager; +import android.content.Context; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; + +/** + * Each subclass of this class keeps track of the state of connectivity + * of a network interface. All state information for a network should + * be kept in a Tracker class. This superclass manages the + * network-type-independent aspects of network state. + * + * {@hide} + */ +public abstract class NetworkStateTracker extends Handler { + + protected NetworkInfo mNetworkInfo; + protected Context mContext; + protected Handler mTarget; + + private static boolean DBG = Config.LOGV; + private static final String TAG = "NetworkStateTracker"; + + public static final int EVENT_STATE_CHANGED = 1; + public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2; + /** + * arg1: 1 to show, 0 to hide + * arg2: ID of the notification + * obj: Notification (if showing) + */ + public static final int EVENT_NOTIFICATION_CHANGED = 3; + public static final int EVENT_CONFIGURATION_CHANGED = 4; + + public NetworkStateTracker(Context context, Handler target, int networkType) { + super(); + mContext = context; + mTarget = target; + this.mNetworkInfo = new NetworkInfo(networkType); + } + + public NetworkInfo getNetworkInfo() { + return mNetworkInfo; + } + + /** + * Return the list of DNS servers associated with this network. + * @return a list of the IP addresses of the DNS servers available + * for the network. + */ + public abstract String[] getNameServers(); + + /** + * Return the system properties name associated with the tcp buffer sizes + * for this network. + */ + public abstract String getTcpBufferSizesPropName(); + + /** + * Return the IP addresses of the DNS servers available for this + * network interface. + * @param propertyNames the names of the system properties whose values + * give the IP addresses. Properties with no values are skipped. + * @return an array of {@code String}s containing the IP addresses + * of the DNS servers, in dot-notation. This may have fewer + * non-null entries than the list of names passed in, since + * some of the passed-in names may have empty values. + */ + static protected String[] getNameServerList(String[] propertyNames) { + String[] dnsAddresses = new String[propertyNames.length]; + int i, j; + + for (i = 0, j = 0; i < propertyNames.length; i++) { + String value = SystemProperties.get(propertyNames[i]); + // The GSM layer sometimes sets a bogus DNS server address of + // 0.0.0.0 + if (!TextUtils.isEmpty(value) && !TextUtils.equals(value, "0.0.0.0")) { + dnsAddresses[j++] = value; + } + } + return dnsAddresses; + } + + /** + * Reads the network specific TCP buffer sizes from SystemProperties + * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system + * wide use + */ + public void updateNetworkSettings() { + String key = getTcpBufferSizesPropName(); + String bufferSizes = SystemProperties.get(key); + + if (bufferSizes.length() == 0) { + Log.e(TAG, key + " not found in system properties. Using defaults"); + + // Setting to default values so we won't be stuck to previous values + key = "net.tcp.buffersize.default"; + bufferSizes = SystemProperties.get(key); + } + + // Set values in kernel + if (bufferSizes.length() != 0) { + if (DBG) { + Log.v(TAG, "Setting TCP values: [" + bufferSizes + + "] which comes from [" + key + "]"); + } + setBufferSize(bufferSizes); + } + } + + /** + * Release the wakelock, if any, that may be held while handling a + * disconnect operation. + */ + public void releaseWakeLock() { + } + + /** + * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max] + * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem + * + * @param bufferSizes in the format of "readMin, readInitial, readMax, + * writeMin, writeInitial, writeMax" + */ + private void setBufferSize(String bufferSizes) { + try { + String[] values = bufferSizes.split(","); + + if (values.length == 6) { + final String prefix = "/sys/kernel/ipv4/tcp_"; + stringToFile(prefix + "rmem_min", values[0]); + stringToFile(prefix + "rmem_def", values[1]); + stringToFile(prefix + "rmem_max", values[2]); + stringToFile(prefix + "wmem_min", values[3]); + stringToFile(prefix + "wmem_def", values[4]); + stringToFile(prefix + "wmem_max", values[5]); + } else { + Log.e(TAG, "Invalid buffersize string: " + bufferSizes); + } + } catch (IOException e) { + Log.e(TAG, "Can't set tcp buffer sizes:" + e); + } + } + + /** + * Writes string to file. Basically same as "echo -n $string > $filename" + * + * @param filename + * @param string + * @throws IOException + */ + private void stringToFile(String filename, String string) throws IOException { + FileWriter out = new FileWriter(filename); + try { + out.write(string); + } finally { + out.close(); + } + } + + /** + * Record the detailed state of a network, and if it is a + * change from the previous state, send a notification to + * any listeners. + * @param state the new @{code DetailedState} + */ + public void setDetailedState(NetworkInfo.DetailedState state) { + setDetailedState(state, null, null); + } + + /** + * Record the detailed state of a network, and if it is a + * change from the previous state, send a notification to + * any listeners. + * @param state the new @{code DetailedState} + * @param reason a {@code String} indicating a reason for the state change, + * if one was supplied. May be {@code null}. + * @param extraInfo optional {@code String} providing extra information about the state change + */ + public void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) { + if (state != mNetworkInfo.getDetailedState()) { + boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING); + String lastReason = mNetworkInfo.getReason(); + /* + * If a reason was supplied when the CONNECTING state was entered, and no + * reason was supplied for entering the CONNECTED state, then retain the + * reason that was supplied when going to CONNECTING. + */ + if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null + && lastReason != null) + reason = lastReason; + mNetworkInfo.setDetailedState(state, reason, extraInfo); + Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); + msg.sendToTarget(); + } + } + + protected void setDetailedStateInternal(NetworkInfo.DetailedState state) { + mNetworkInfo.setDetailedState(state, null, null); + } + + /** + * Send a notification that the results of a scan for network access + * points has completed, and results are available. + */ + protected void sendScanResultsAvailable() { + Message msg = mTarget.obtainMessage(EVENT_SCAN_RESULTS_AVAILABLE, mNetworkInfo); + msg.sendToTarget(); + } + + public abstract void startMonitoring(); + + /** + * Disable connectivity to a network + * @return {@code true} if a teardown occurred, {@code false} if the + * teardown did not occur. + */ + public abstract boolean teardown(); + + /** + * Reenable connectivity to a network after a {@link #teardown()}. + */ + public abstract boolean reconnect(); + + /** + * Turn the wireless radio off for a network. + * @param turnOn {@code true} to turn the radio on, {@code false} + */ + public abstract boolean setRadio(boolean turnOn); + + /** + * Returns an indication of whether this network is available for + * connections. A value of {@code false} means that some quasi-permanent + * condition prevents connectivity to this network. + */ + public abstract boolean isAvailable(); + + /** + * Tells the underlying networking system that the caller wants to + * begin using the named feature. The interpretation of {@code feature} + * is completely up to each networking implementation. + * @param feature the name of the feature to be used + * @param callingPid the process ID of the process that is issuing this request + * @param callingUid the user ID of the process that is issuing this request + * @return an integer value representing the outcome of the request. + * The interpretation of this value is specific to each networking + * implementation+feature combination, except that the value {@code -1} + * always indicates failure. + */ + public abstract int startUsingNetworkFeature(String feature, int callingPid, int callingUid); + + /** + * Tells the underlying networking system that the caller is finished + * using the named feature. The interpretation of {@code feature} + * is completely up to each networking implementation. + * @param feature the name of the feature that is no longer needed. + * @param callingPid the process ID of the process that is issuing this request + * @param callingUid the user ID of the process that is issuing this request + * @return an integer value representing the outcome of the request. + * The interpretation of this value is specific to each networking + * implementation+feature combination, except that the value {@code -1} + * always indicates failure. + */ + public abstract int stopUsingNetworkFeature(String feature, int callingPid, int callingUid); + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via this network interface. + * @param hostAddress the IP address of the host to which the route is desired + * @return {@code true} on success, {@code false} on failure + */ + public boolean requestRouteToHost(int hostAddress) { + return false; + } + + /** + * Interprets scan results. This will be called at a safe time for + * processing, and from a safe thread. + */ + public void interpretScanResultsAvailable() { + } + +} diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java new file mode 100644 index 0000000..129248a --- /dev/null +++ b/core/java/android/net/NetworkUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Native methods for managing network interfaces. + * + * {@hide} + */ +public class NetworkUtils { + /** Bring the named network interface down. */ + public native static int disableInterface(String interfaceName); + + /** Add a route to the specified host via the named interface. */ + public native static int addHostRoute(String interfaceName, int hostaddr); + + /** Add a default route for the named interface. */ + public native static int setDefaultRoute(String interfaceName, int gwayAddr); + + /** Return the gateway address for the default route for the named interface. */ + public native static int getDefaultRoute(String interfaceName); + + /** Remove host routes that uses the named interface. */ + public native static int removeHostRoutes(String interfaceName); + + /** Remove the default route for the named interface. */ + public native static int removeDefaultRoute(String interfaceName); + + /** Reset any sockets that are connected via the named interface. */ + public native static int resetConnections(String interfaceName); + + /** + * Start the DHCP client daemon, in order to have it request addresses + * for the named interface, and then configure the interface with those + * addresses. This call blocks until it obtains a result (either success + * or failure) from the daemon. + * @param interfaceName the name of the interface to configure + * @param ipInfo if the request succeeds, this object is filled in with + * the IP address information. + * @return {@code true} for success, {@code false} for failure + */ + public native static boolean runDhcp(String interfaceName, DhcpInfo ipInfo); + + /** + * Shut down the DHCP client daemon. + * @param interfaceName the name of the interface for which the daemon + * should be stopped + * @return {@code true} for success, {@code false} for failure + */ + public native static boolean stopDhcp(String interfaceName); + + /** + * Return the last DHCP-related error message that was recorded. + * <p/>NOTE: This string is not localized, but currently it is only + * used in logging. + * @return the most recent error message, if any + */ + public native static String getDhcpError(); + + /** + * When static IP configuration has been specified, configure the network + * interface according to the values supplied. + * @param interfaceName the name of the interface to configure + * @param ipInfo the IP address, default gateway, and DNS server addresses + * with which to configure the interface. + * @return {@code true} for success, {@code false} for failure + */ + public static boolean configureInterface(String interfaceName, DhcpInfo ipInfo) { + return configureNative(interfaceName, + ipInfo.ipAddress, + ipInfo.netmask, + ipInfo.gateway, + ipInfo.dns1, + ipInfo.dns2); + } + + private native static boolean configureNative( + String interfaceName, int ipAddress, int netmask, int gateway, int dns1, int dns2); + + /** + * Look up a host name and return the result as an int. Works if the argument + * is an IP address in dot notation. Obviously, this can only be used for IPv4 + * addresses. + * @param hostname the name of the host (or the IP address) + * @return the IP address as an {@code int} in network byte order + */ + public static int lookupHost(String hostname) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(hostname); + } catch (UnknownHostException e) { + return -1; + } + byte[] addrBytes; + int addr; + addrBytes = inetAddress.getAddress(); + addr = ((addrBytes[3] & 0xff) << 24) + | ((addrBytes[2] & 0xff) << 16) + | ((addrBytes[1] & 0xff) << 8) + | (addrBytes[0] & 0xff); + return addr; + } +} diff --git a/core/java/android/net/ParseException.java b/core/java/android/net/ParseException.java new file mode 100644 index 0000000..000fa68 --- /dev/null +++ b/core/java/android/net/ParseException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2006 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 android.net; + +/** + * + * + * When WebAddress Parser Fails, this exception is thrown + */ +public class ParseException extends RuntimeException { + public String response; + + ParseException(String response) { + this.response = response; + } +} diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java new file mode 100644 index 0000000..86e1d5b --- /dev/null +++ b/core/java/android/net/Proxy.java @@ -0,0 +1,120 @@ +/* + * 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 android.net; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.SystemProperties; +import android.provider.Settings; +import android.util.Log; + +import junit.framework.Assert; + +/** + * A convenience class for accessing the user and default proxy + * settings. + */ +final public class Proxy { + + static final public String PROXY_CHANGE_ACTION = + "android.intent.action.PROXY_CHANGE"; + + /** + * Return the proxy host set by the user. + * @param ctx A Context used to get the settings for the proxy host. + * @return String containing the host name. If the user did not set a host + * name it returns the default host. A null value means that no + * host is to be used. + */ + static final public String getHost(Context ctx) { + ContentResolver contentResolver = ctx.getContentResolver(); + Assert.assertNotNull(contentResolver); + String host = Settings.System.getString( + contentResolver, + Settings.System.HTTP_PROXY); + if (host != null) { + int i = host.indexOf(':'); + if (i == -1) { + if (android.util.Config.DEBUG) { + Assert.assertTrue(host.length() == 0); + } + return null; + } + return host.substring(0, i); + } + return getDefaultHost(); + } + + /** + * Return the proxy port set by the user. + * @param ctx A Context used to get the settings for the proxy port. + * @return The port number to use or -1 if no proxy is to be used. + */ + static final public int getPort(Context ctx) { + ContentResolver contentResolver = ctx.getContentResolver(); + Assert.assertNotNull(contentResolver); + String host = Settings.System.getString( + contentResolver, + Settings.System.HTTP_PROXY); + if (host != null) { + int i = host.indexOf(':'); + if (i == -1) { + if (android.util.Config.DEBUG) { + Assert.assertTrue(host.length() == 0); + } + return -1; + } + if (android.util.Config.DEBUG) { + Assert.assertTrue(i < host.length()); + } + return Integer.parseInt(host.substring(i+1)); + } + return getDefaultPort(); + } + + /** + * Return the default proxy host specified by the carrier. + * @return String containing the host name or null if there is no proxy for + * this carrier. + */ + static final public String getDefaultHost() { + String host = SystemProperties.get("net.gprs.http-proxy"); + if (host != null) { + Uri u = Uri.parse(host); + host = u.getHost(); + return host; + } else { + return null; + } + } + + /** + * Return the default proxy port specified by the carrier. + * @return The port number to be used with the proxy host or -1 if there is + * no proxy for this carrier. + */ + static final public int getDefaultPort() { + String host = SystemProperties.get("net.gprs.http-proxy"); + if (host != null) { + Uri u = Uri.parse(host); + return u.getPort(); + } else { + return -1; + } + } + +}; diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java new file mode 100644 index 0000000..f816caa --- /dev/null +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import android.util.Log; +import android.util.Config; +import android.net.http.DomainNameChecker; +import android.os.SystemProperties; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.NoSuchAlgorithmException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +public class SSLCertificateSocketFactory extends SSLSocketFactory { + + private static final boolean DBG = true; + private static final String LOG_TAG = "SSLCertificateSocketFactory"; + + private static X509TrustManager sDefaultTrustManager; + + private final int socketReadTimeoutForSslHandshake; + + static { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore)null); + TrustManager[] tms = tmf.getTrustManagers(); + if (tms != null) { + for (TrustManager tm : tms) { + if (tm instanceof X509TrustManager) { + sDefaultTrustManager = (X509TrustManager)tm; + break; + } + } + } + } catch (NoSuchAlgorithmException e) { + Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e); + } catch (KeyStoreException e) { + Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e); + } + } + + private static final TrustManager[] TRUST_MANAGER = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, + String authType) { } + + public void checkServerTrusted(X509Certificate[] certs, + String authType) { } + } + }; + + private SSLSocketFactory factory; + + public SSLCertificateSocketFactory(int socketReadTimeoutForSslHandshake) + throws NoSuchAlgorithmException, KeyManagementException { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, TRUST_MANAGER, new java.security.SecureRandom()); + factory = (SSLSocketFactory) context.getSocketFactory(); + this.socketReadTimeoutForSslHandshake = socketReadTimeoutForSslHandshake; + } + + /** + * Returns a default instantiation of a new socket factory which + * only allows SSL connections with valid certificates. + * + * @param socketReadTimeoutForSslHandshake the socket read timeout used for performing + * ssl handshake. The socket read timeout is set back to 0 after the handshake. + * @return a new SocketFactory, or null on error + */ + public static SocketFactory getDefault(int socketReadTimeoutForSslHandshake) { + try { + return new SSLCertificateSocketFactory(socketReadTimeoutForSslHandshake); + } catch (NoSuchAlgorithmException e) { + Log.e(LOG_TAG, + "SSLCertifcateSocketFactory.getDefault" + + " NoSuchAlgorithmException " , e); + return null; + } catch (KeyManagementException e) { + Log.e(LOG_TAG, + "SSLCertifcateSocketFactory.getDefault" + + " KeyManagementException " , e); + return null; + } + } + + private boolean hasValidCertificateChain(Certificate[] certs) + throws IOException { + if (sDefaultTrustManager == null) { + if (Config.LOGD) { + Log.d(LOG_TAG,"hasValidCertificateChain():" + + " null default trust manager!"); + } + throw new IOException("null default trust manager"); + } + + boolean trusted = (certs != null && (certs.length > 0)); + + if (trusted) { + try { + // the authtype we pass in doesn't actually matter + sDefaultTrustManager.checkServerTrusted((X509Certificate[]) certs, "RSA"); + } catch (GeneralSecurityException e) { + String exceptionMessage = e != null ? e.getMessage() : "none"; + if (Config.LOGD) { + Log.d(LOG_TAG,"hasValidCertificateChain(): sec. exception: " + + exceptionMessage); + } + trusted = false; + } + } + + return trusted; + } + + private void validateSocket(SSLSocket sslSock, String destHost) + throws IOException + { + if (Config.LOGV) { + Log.v(LOG_TAG,"validateSocket() to host "+destHost); + } + + String relaxSslCheck = SystemProperties.get("socket.relaxsslcheck"); + String secure = SystemProperties.get("ro.secure"); + + // only allow relaxing the ssl check on non-secure builds where the relaxation is + // specifically requested. + if ("0".equals(secure) && "yes".equals(relaxSslCheck)) { + if (Config.LOGD) { + Log.d(LOG_TAG,"sys prop socket.relaxsslcheck is set," + + " ignoring invalid certs"); + } + return; + } + + Certificate[] certs = null; + sslSock.setUseClientMode(true); + sslSock.startHandshake(); + certs = sslSock.getSession().getPeerCertificates(); + + // check that the root certificate in the chain belongs to + // a CA we trust + if (certs == null) { + Log.e(LOG_TAG, + "[SSLCertificateSocketFactory] no trusted root CA"); + throw new IOException("no trusted root CA"); + } + + if (Config.LOGV) { + Log.v(LOG_TAG,"validateSocket # certs = " +certs.length); + } + + if (!hasValidCertificateChain(certs)) { + if (Config.LOGD) { + Log.d(LOG_TAG,"validateSocket(): certificate untrusted!"); + } + throw new IOException("Certificate untrusted"); + } + + X509Certificate lastChainCert = (X509Certificate) certs[0]; + + if (!DomainNameChecker.match(lastChainCert, destHost)) { + if (Config.LOGD) { + Log.d(LOG_TAG,"validateSocket(): domain name check failed"); + } + throw new IOException("Domain Name check failed"); + } + } + + public Socket createSocket(Socket socket, String s, int i, boolean flag) + throws IOException + { + throw new IOException("Cannot validate certification without a hostname"); + } + + public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j) + throws IOException + { + throw new IOException("Cannot validate certification without a hostname"); + } + + public Socket createSocket(InetAddress inaddr, int i) throws IOException { + throw new IOException("Cannot validate certification without a hostname"); + } + + public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException { + SSLSocket sslSock = (SSLSocket) factory.createSocket(s, i, inaddr, j); + + if (socketReadTimeoutForSslHandshake >= 0) { + sslSock.setSoTimeout(socketReadTimeoutForSslHandshake); + } + + validateSocket(sslSock,s); + sslSock.setSoTimeout(0); + + return sslSock; + } + + public Socket createSocket(String s, int i) throws IOException { + SSLSocket sslSock = (SSLSocket) factory.createSocket(s, i); + + if (socketReadTimeoutForSslHandshake >= 0) { + sslSock.setSoTimeout(socketReadTimeoutForSslHandshake); + } + + validateSocket(sslSock,s); + sslSock.setSoTimeout(0); + + return sslSock; + } + + public String[] getDefaultCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } +} + + diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java new file mode 100644 index 0000000..28134b2 --- /dev/null +++ b/core/java/android/net/SntpClient.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 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. + * 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 android.net; + +import android.os.SystemClock; +import android.util.Config; +import android.util.Log; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; + +/** + * {@hide} + * + * Simple SNTP client class for retrieving network time. + * + * Sample usage: + * <pre>SntpClient client = new SntpClient(); + * if (client.requestTime("time.foo.com")) { + * long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference(); + * } + * </pre> + */ +public class SntpClient +{ + private static final String TAG = "SntpClient"; + + private static final int REFERENCE_TIME_OFFSET = 16; + private static final int ORIGINATE_TIME_OFFSET = 24; + private static final int RECEIVE_TIME_OFFSET = 32; + private static final int TRANSMIT_TIME_OFFSET = 40; + private static final int NTP_PACKET_SIZE = 48; + + private static final int NTP_PORT = 123; + private static final int NTP_MODE_CLIENT = 3; + private static final int NTP_VERSION = 3; + + // Number of seconds between Jan 1, 1900 and Jan 1, 1970 + // 70 years plus 17 leap days + private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L; + + // system time computed from NTP server response + private long mNtpTime; + + // value of SystemClock.elapsedRealtime() corresponding to mNtpTime + private long mNtpTimeReference; + + // round trip time in milliseconds + private long mRoundTripTime; + + /** + * Sends an SNTP request to the given host and processes the response. + * + * @param host host name of the server. + * @param timeout network timeout in milliseconds. + * @return true if the transaction was successful. + */ + public boolean requestTime(String host, int timeout) { + try { + DatagramSocket socket = new DatagramSocket(); + socket.setSoTimeout(timeout); + InetAddress address = InetAddress.getByName(host); + byte[] buffer = new byte[NTP_PACKET_SIZE]; + DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT); + + // set mode = 3 (client) and version = 3 + // mode is in low 3 bits of first byte + // version is in bits 3-5 of first byte + buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3); + + // get current time and write it to the request packet + long requestTime = System.currentTimeMillis(); + long requestTicks = SystemClock.elapsedRealtime(); + writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime); + + socket.send(request); + + // read the response + DatagramPacket response = new DatagramPacket(buffer, buffer.length); + socket.receive(response); + long responseTicks = SystemClock.elapsedRealtime(); + long responseTime = requestTime + (responseTicks - requestTicks); + socket.close(); + + // extract the results + long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET); + long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET); + long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET); + long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime); + long clockOffset = (receiveTime - originateTime) + (transmitTime - responseTime); + if (Config.LOGD) Log.d(TAG, "round trip: " + roundTripTime + " ms"); + if (Config.LOGD) Log.d(TAG, "clock offset: " + clockOffset + " ms"); + + // save our results + mNtpTime = requestTime + clockOffset; + mNtpTimeReference = requestTicks; + mRoundTripTime = roundTripTime; + } catch (Exception e) { + if (Config.LOGD) Log.d(TAG, "request time failed: " + e); + return false; + } + + return true; + } + + /** + * Returns the time computed from the NTP transaction. + * + * @return time value computed from NTP server response. + */ + public long getNtpTime() { + return mNtpTime; + } + + /** + * Returns the reference clock value (value of SystemClock.elapsedRealtime()) + * corresponding to the NTP time. + * + * @return reference clock corresponding to the NTP time. + */ + public long getNtpTimeReference() { + return mNtpTimeReference; + } + + /** + * Returns the round trip time of the NTP transaction + * + * @return round trip time in milliseconds. + */ + public long getRoundTripTime() { + return mRoundTripTime; + } + + /** + * Reads an unsigned 32 bit big endian number from the given offset in the buffer. + */ + private long read32(byte[] buffer, int offset) { + byte b0 = buffer[offset]; + byte b1 = buffer[offset+1]; + byte b2 = buffer[offset+2]; + byte b3 = buffer[offset+3]; + + // convert signed bytes to unsigned values + int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0); + int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1); + int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2); + int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3); + + return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3; + } + + /** + * Reads the NTP time stamp at the given offset in the buffer and returns + * it as a system time (milliseconds since January 1, 1970). + */ + private long readTimeStamp(byte[] buffer, int offset) { + long seconds = read32(buffer, offset); + long fraction = read32(buffer, offset + 4); + return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L); + } + + /** + * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp + * at the given offset in the buffer. + */ + private void writeTimeStamp(byte[] buffer, int offset, long time) { + long seconds = time / 1000L; + long milliseconds = time - seconds * 1000L; + seconds += OFFSET_1900_TO_1970; + + // write seconds in big endian format + buffer[offset++] = (byte)(seconds >> 24); + buffer[offset++] = (byte)(seconds >> 16); + buffer[offset++] = (byte)(seconds >> 8); + buffer[offset++] = (byte)(seconds >> 0); + + long fraction = milliseconds * 0x100000000L / 1000L; + // write fraction in big endian format + buffer[offset++] = (byte)(fraction >> 24); + buffer[offset++] = (byte)(fraction >> 16); + buffer[offset++] = (byte)(fraction >> 8); + // low order bits should be random data + buffer[offset++] = (byte)(Math.random() * 255.0); + } +} diff --git a/core/java/android/net/Uri.aidl b/core/java/android/net/Uri.aidl new file mode 100755 index 0000000..6bd3be5 --- /dev/null +++ b/core/java/android/net/Uri.aidl @@ -0,0 +1,19 @@ +/** + * 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 android.net; + +parcelable Uri; diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java new file mode 100644 index 0000000..32a26e4 --- /dev/null +++ b/core/java/android/net/Uri.java @@ -0,0 +1,2251 @@ +/* + * 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 android.net; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.ByteArrayOutputStream; +import java.net.URLEncoder; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.RandomAccess; + +/** + * Immutable URI reference. A URI reference includes a URI and a fragment, the + * component of the URI following a '#'. Builds and parses URI references + * which conform to + * <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC 2396</a>. + * + * <p>In the interest of performance, this class performs little to no + * validation. Behavior is undefined for invalid input. This class is very + * forgiving--in the face of invalid input, it will return garbage + * rather than throw an exception unless otherwise specified. + */ +public abstract class Uri implements Parcelable, Comparable<Uri> { + + /* + + This class aims to do as little up front work as possible. To accomplish + that, we vary the implementation dependending on what the user passes in. + For example, we have one implementation if the user passes in a + URI string (StringUri) and another if the user passes in the + individual components (OpaqueUri). + + *Concurrency notes*: Like any truly immutable object, this class is safe + for concurrent use. This class uses a caching pattern in some places where + it doesn't use volatile or synchronized. This is safe to do with ints + because getting or setting an int is atomic. It's safe to do with a String + because the internal fields are final and the memory model guarantees other + threads won't see a partially initialized instance. We are not guaranteed + that some threads will immediately see changes from other threads on + certain platforms, but we don't mind if those threads reconstruct the + cached result. As a result, we get thread safe caching with no concurrency + overhead, which means the most common case, access from a single thread, + is as fast as possible. + + From the Java Language spec.: + + "17.5 Final Field Semantics + + ... when the object is seen by another thread, that thread will always + see the correctly constructed version of that object's final fields. + It will also see versions of any object or array referenced by + those final fields that are at least as up-to-date as the final fields + are." + + In that same vein, all non-transient fields within Uri + implementations should be final and immutable so as to ensure true + immutability for clients even when they don't use proper concurrency + control. + + For reference, from RFC 2396: + + "4.3. Parsing a URI Reference + + A URI reference is typically parsed according to the four main + components and fragment identifier in order to determine what + components are present and whether the reference is relative or + absolute. The individual components are then parsed for their + subparts and, if not opaque, to verify their validity. + + Although the BNF defines what is allowed in each component, it is + ambiguous in terms of differentiating between an authority component + and a path component that begins with two slash characters. The + greedy algorithm is used for disambiguation: the left-most matching + rule soaks up as much of the URI reference string as it is capable of + matching. In other words, the authority component wins." + + The "four main components" of a hierarchical URI consist of + <scheme>://<authority><path>?<query> + + */ + + /** Log tag. */ + private static final String LOG = Uri.class.getSimpleName(); + + /** + * The empty URI, equivalent to "". + */ + public static final Uri EMPTY = new HierarchicalUri(null, Part.NULL, + PathPart.EMPTY, Part.NULL, Part.NULL); + + /** + * Prevents external subclassing. + */ + private Uri() {} + + /** + * Returns true if this URI is hierarchical like "http://google.com". + * Absolute URIs are hierarchical if the scheme-specific part starts with + * a '/'. Relative URIs are always hierarchical. + */ + public abstract boolean isHierarchical(); + + /** + * Returns true if this URI is opaque like "mailto:nobody@google.com". The + * scheme-specific part of an opaque URI cannot start with a '/'. + */ + public boolean isOpaque() { + return !isHierarchical(); + } + + /** + * Returns true if this URI is relative, i.e. if it doesn't contain an + * explicit scheme. + * + * @return true if this URI is relative, false if it's absolute + */ + public abstract boolean isRelative(); + + /** + * Returns true if this URI is absolute, i.e. if it contains an + * explicit scheme. + * + * @return true if this URI is absolute, false if it's relative + */ + public boolean isAbsolute() { + return !isRelative(); + } + + /** + * Gets the scheme of this URI. Example: "http" + * + * @return the scheme or null if this is a relative URI + */ + public abstract String getScheme(); + + /** + * Gets the scheme-specific part of this URI, i.e. everything between the + * scheme separator ':' and the fragment separator '#'. If this is a + * relative URI, this method returns the entire URI. Decodes escaped octets. + * + * <p>Example: "//www.google.com/search?q=android" + * + * @return the decoded scheme-specific-part + */ + public abstract String getSchemeSpecificPart(); + + /** + * Gets the scheme-specific part of this URI, i.e. everything between the + * scheme separator ':' and the fragment separator '#'. If this is a + * relative URI, this method returns the entire URI. Leaves escaped octets + * intact. + * + * <p>Example: "//www.google.com/search?q=android" + * + * @return the decoded scheme-specific-part + */ + public abstract String getEncodedSchemeSpecificPart(); + + /** + * Gets the decoded authority part of this URI. For + * server addresses, the authority is structured as follows: + * {@code [ userinfo '@' ] host [ ':' port ]} + * + * <p>Examples: "google.com", "bob@google.com:80" + * + * @return the authority for this URI or null if not present + */ + public abstract String getAuthority(); + + /** + * Gets the encoded authority part of this URI. For + * server addresses, the authority is structured as follows: + * {@code [ userinfo '@' ] host [ ':' port ]} + * + * <p>Examples: "google.com", "bob@google.com:80" + * + * @return the authority for this URI or null if not present + */ + public abstract String getEncodedAuthority(); + + /** + * Gets the decoded user information from the authority. + * For example, if the authority is "nobody@google.com", this method will + * return "nobody". + * + * @return the user info for this URI or null if not present + */ + public abstract String getUserInfo(); + + /** + * Gets the encoded user information from the authority. + * For example, if the authority is "nobody@google.com", this method will + * return "nobody". + * + * @return the user info for this URI or null if not present + */ + public abstract String getEncodedUserInfo(); + + /** + * Gets the encoded host from the authority for this URI. For example, + * if the authority is "bob@google.com", this method will return + * "google.com". + * + * @return the host for this URI or null if not present + */ + public abstract String getHost(); + + /** + * Gets the port from the authority for this URI. For example, + * if the authority is "google.com:80", this method will return 80. + * + * @return the port for this URI or -1 if invalid or not present + */ + public abstract int getPort(); + + /** + * Gets the decoded path. + * + * @return the decoded path, or null if this is not a hierarchical URI + * (like "mailto:nobody@google.com") or the URI is invalid + */ + public abstract String getPath(); + + /** + * Gets the encoded path. + * + * @return the encoded path, or null if this is not a hierarchical URI + * (like "mailto:nobody@google.com") or the URI is invalid + */ + public abstract String getEncodedPath(); + + /** + * Gets the decoded query component from this URI. The query comes after + * the query separator ('?') and before the fragment separator ('#'). This + * method would return "q=android" for + * "http://www.google.com/search?q=android". + * + * @return the decoded query or null if there isn't one + */ + public abstract String getQuery(); + + /** + * Gets the encoded query component from this URI. The query comes after + * the query separator ('?') and before the fragment separator ('#'). This + * method would return "q=android" for + * "http://www.google.com/search?q=android". + * + * @return the encoded query or null if there isn't one + */ + public abstract String getEncodedQuery(); + + /** + * Gets the decoded fragment part of this URI, everything after the '#'. + * + * @return the decoded fragment or null if there isn't one + */ + public abstract String getFragment(); + + /** + * Gets the encoded fragment part of this URI, everything after the '#'. + * + * @return the encoded fragment or null if there isn't one + */ + public abstract String getEncodedFragment(); + + /** + * Gets the decoded path segments. + * + * @return decoded path segments, each without a leading or trailing '/' + */ + public abstract List<String> getPathSegments(); + + /** + * Gets the decoded last segment in the path. + * + * @return the decoded last segment or null if the path is empty + */ + public abstract String getLastPathSegment(); + + /** + * Compares this Uri to another object for equality. Returns true if the + * encoded string representations of this Uri and the given Uri are + * equal. Case counts. Paths are not normalized. If one Uri specifies a + * default port explicitly and the other leaves it implicit, they will not + * be considered equal. + */ + public boolean equals(Object o) { + if (!(o instanceof Uri)) { + return false; + } + + Uri other = (Uri) o; + + return toString().equals(other.toString()); + } + + /** + * Hashes the encoded string represention of this Uri consistently with + * {@link #equals(Object)}. + */ + public int hashCode() { + return toString().hashCode(); + } + + /** + * Compares the string representation of this Uri with that of + * another. + */ + public int compareTo(Uri other) { + return toString().compareTo(other.toString()); + } + + /** + * Returns the encoded string representation of this URI. + * Example: "http://google.com/" + */ + public abstract String toString(); + + /** + * Constructs a new builder, copying the attributes from this Uri. + */ + public abstract Builder buildUpon(); + + /** Index of a component which was not found. */ + private final static int NOT_FOUND = -1; + + /** Placeholder value for an index which hasn't been calculated yet. */ + private final static int NOT_CALCULATED = -2; + + /** + * Placeholder for strings which haven't been cached. This enables us + * to cache null. We intentionally create a new String instance so we can + * compare its identity and there is no chance we will confuse it with + * user data. + */ + @SuppressWarnings("RedundantStringConstructorCall") + private static final String NOT_CACHED = new String("NOT CACHED"); + + /** + * Error message presented when a user tries to treat an opaque URI as + * hierarchical. + */ + private static final String NOT_HIERARCHICAL + = "This isn't a hierarchical URI."; + + /** Default encoding. */ + private static final String DEFAULT_ENCODING = "UTF-8"; + + /** + * Creates a Uri which parses the given encoded URI string. + * + * @param uriString an RFC 3296-compliant, encoded URI + * @throws NullPointerException if uriString is null + * @return Uri for this given uri string + */ + public static Uri parse(String uriString) { + return new StringUri(uriString); + } + + /** + * Creates a Uri from a file. The URI has the form + * "file://<absolute path>". Encodes path characters with the exception of + * '/'. + * + * <p>Example: "file:///tmp/android.txt" + * + * @throws NullPointerException if file is null + * @return a Uri for the given file + */ + public static Uri fromFile(File file) { + if (file == null) { + throw new NullPointerException("file"); + } + + PathPart path = PathPart.fromDecoded(file.getAbsolutePath()); + return new HierarchicalUri( + "file", Part.EMPTY, path, Part.NULL, Part.NULL); + } + + /** + * An implementation which wraps a String URI. This URI can be opaque or + * hierarchical, but we extend AbstractHierarchicalUri in case we need + * the hierarchical functionality. + */ + private static class StringUri extends AbstractHierarchicalUri { + + /** Used in parcelling. */ + static final int TYPE_ID = 1; + + /** URI string representation. */ + private final String uriString; + + private StringUri(String uriString) { + if (uriString == null) { + throw new NullPointerException("uriString"); + } + + this.uriString = uriString; + } + + static Uri readFrom(Parcel parcel) { + return new StringUri(parcel.readString()); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(TYPE_ID); + parcel.writeString(uriString); + } + + /** Cached scheme separator index. */ + private volatile int cachedSsi = NOT_CALCULATED; + + /** Finds the first ':'. Returns -1 if none found. */ + private int findSchemeSeparator() { + return cachedSsi == NOT_CALCULATED + ? cachedSsi = uriString.indexOf(':') + : cachedSsi; + } + + /** Cached fragment separator index. */ + private volatile int cachedFsi = NOT_CALCULATED; + + /** Finds the first '#'. Returns -1 if none found. */ + private int findFragmentSeparator() { + return cachedFsi == NOT_CALCULATED + ? cachedFsi = uriString.indexOf('#', findSchemeSeparator()) + : cachedFsi; + } + + public boolean isHierarchical() { + int ssi = findSchemeSeparator(); + + if (ssi == NOT_FOUND) { + // All relative URIs are hierarchical. + return true; + } + + if (uriString.length() == ssi + 1) { + // No ssp. + return false; + } + + // If the ssp starts with a '/', this is hierarchical. + return uriString.charAt(ssi + 1) == '/'; + } + + public boolean isRelative() { + // Note: We return true if the index is 0 + return findSchemeSeparator() == NOT_FOUND; + } + + private volatile String scheme = NOT_CACHED; + + public String getScheme() { + @SuppressWarnings("StringEquality") + boolean cached = (scheme != NOT_CACHED); + return cached ? scheme : (scheme = parseScheme()); + } + + private String parseScheme() { + int ssi = findSchemeSeparator(); + return ssi == NOT_FOUND ? null : uriString.substring(0, ssi); + } + + private Part ssp; + + private Part getSsp() { + return ssp == null ? ssp = Part.fromEncoded(parseSsp()) : ssp; + } + + public String getEncodedSchemeSpecificPart() { + return getSsp().getEncoded(); + } + + public String getSchemeSpecificPart() { + return getSsp().getDecoded(); + } + + private String parseSsp() { + int ssi = findSchemeSeparator(); + int fsi = findFragmentSeparator(); + + // Return everything between ssi and fsi. + return fsi == NOT_FOUND + ? uriString.substring(ssi + 1) + : uriString.substring(ssi + 1, fsi); + } + + private Part authority; + + private Part getAuthorityPart() { + if (authority == null) { + String encodedAuthority + = parseAuthority(this.uriString, findSchemeSeparator()); + return authority = Part.fromEncoded(encodedAuthority); + } + + return authority; + } + + public String getEncodedAuthority() { + return getAuthorityPart().getEncoded(); + } + + public String getAuthority() { + return getAuthorityPart().getDecoded(); + } + + private PathPart path; + + private PathPart getPathPart() { + return path == null + ? path = PathPart.fromEncoded(parsePath()) + : path; + } + + public String getPath() { + return getPathPart().getDecoded(); + } + + public String getEncodedPath() { + return getPathPart().getEncoded(); + } + + public List<String> getPathSegments() { + return getPathPart().getPathSegments(); + } + + private String parsePath() { + String uriString = this.uriString; + int ssi = findSchemeSeparator(); + + // If the URI is absolute. + if (ssi > -1) { + // Is there anything after the ':'? + boolean schemeOnly = ssi + 1 == uriString.length(); + if (schemeOnly) { + // Opaque URI. + return null; + } + + // A '/' after the ':' means this is hierarchical. + if (uriString.charAt(ssi + 1) != '/') { + // Opaque URI. + return null; + } + } else { + // All relative URIs are hierarchical. + } + + return parsePath(uriString, ssi); + } + + private Part query; + + private Part getQueryPart() { + return query == null + ? query = Part.fromEncoded(parseQuery()) : query; + } + + public String getEncodedQuery() { + return getQueryPart().getEncoded(); + } + + private String parseQuery() { + // It doesn't make sense to cache this index. We only ever + // calculate it once. + int qsi = uriString.indexOf('?', findSchemeSeparator()); + if (qsi == NOT_FOUND) { + return null; + } + + int fsi = findFragmentSeparator(); + + if (fsi == NOT_FOUND) { + return uriString.substring(qsi + 1); + } + + if (fsi < qsi) { + // Invalid. + return null; + } + + return uriString.substring(qsi + 1, fsi); + } + + public String getQuery() { + return getQueryPart().getDecoded(); + } + + private Part fragment; + + private Part getFragmentPart() { + return fragment == null + ? fragment = Part.fromEncoded(parseFragment()) : fragment; + } + + public String getEncodedFragment() { + return getFragmentPart().getEncoded(); + } + + private String parseFragment() { + int fsi = findFragmentSeparator(); + return fsi == NOT_FOUND ? null : uriString.substring(fsi + 1); + } + + public String getFragment() { + return getFragmentPart().getDecoded(); + } + + public String toString() { + return uriString; + } + + /** + * Parses an authority out of the given URI string. + * + * @param uriString URI string + * @param ssi scheme separator index, -1 for a relative URI + * + * @return the authority or null if none is found + */ + static String parseAuthority(String uriString, int ssi) { + int length = uriString.length(); + + // If "//" follows the scheme separator, we have an authority. + if (length > ssi + 2 + && uriString.charAt(ssi + 1) == '/' + && uriString.charAt(ssi + 2) == '/') { + // We have an authority. + + // Look for the start of the path, query, or fragment, or the + // end of the string. + int end = ssi + 3; + LOOP: while (end < length) { + switch (uriString.charAt(end)) { + case '/': // Start of path + case '?': // Start of query + case '#': // Start of fragment + break LOOP; + } + end++; + } + + return uriString.substring(ssi + 3, end); + } else { + return null; + } + + } + + /** + * Parses a path out of this given URI string. + * + * @param uriString URI string + * @param ssi scheme separator index, -1 for a relative URI + * + * @return the path + */ + static String parsePath(String uriString, int ssi) { + int length = uriString.length(); + + // Find start of path. + int pathStart; + if (length > ssi + 2 + && uriString.charAt(ssi + 1) == '/' + && uriString.charAt(ssi + 2) == '/') { + // Skip over authority to path. + pathStart = ssi + 3; + LOOP: while (pathStart < length) { + switch (uriString.charAt(pathStart)) { + case '?': // Start of query + case '#': // Start of fragment + return ""; // Empty path. + case '/': // Start of path! + break LOOP; + } + pathStart++; + } + } else { + // Path starts immediately after scheme separator. + pathStart = ssi + 1; + } + + // Find end of path. + int pathEnd = pathStart; + LOOP: while (pathEnd < length) { + switch (uriString.charAt(pathEnd)) { + case '?': // Start of query + case '#': // Start of fragment + break LOOP; + } + pathEnd++; + } + + return uriString.substring(pathStart, pathEnd); + } + + public Builder buildUpon() { + if (isHierarchical()) { + return new Builder() + .scheme(getScheme()) + .authority(getAuthorityPart()) + .path(getPathPart()) + .query(getQueryPart()) + .fragment(getFragmentPart()); + } else { + return new Builder() + .scheme(getScheme()) + .opaquePart(getSsp()) + .fragment(getFragmentPart()); + } + } + } + + /** + * Creates an opaque Uri from the given components. Encodes the ssp + * which means this method cannot be used to create hierarchical URIs. + * + * @param scheme of the URI + * @param ssp scheme-specific-part, everything between the + * scheme separator (':') and the fragment separator ('#'), which will + * get encoded + * @param fragment fragment, everything after the '#', null if undefined, + * will get encoded + * + * @throws NullPointerException if scheme or ssp is null + * @return Uri composed of the given scheme, ssp, and fragment + * + * @see Builder if you don't want the ssp and fragment to be encoded + */ + public static Uri fromParts(String scheme, String ssp, + String fragment) { + if (scheme == null) { + throw new NullPointerException("scheme"); + } + if (ssp == null) { + throw new NullPointerException("ssp"); + } + + return new OpaqueUri(scheme, Part.fromDecoded(ssp), + Part.fromDecoded(fragment)); + } + + /** + * Opaque URI. + */ + private static class OpaqueUri extends Uri { + + /** Used in parcelling. */ + static final int TYPE_ID = 2; + + private final String scheme; + private final Part ssp; + private final Part fragment; + + private OpaqueUri(String scheme, Part ssp, Part fragment) { + this.scheme = scheme; + this.ssp = ssp; + this.fragment = fragment == null ? Part.NULL : fragment; + } + + static Uri readFrom(Parcel parcel) { + return new OpaqueUri( + parcel.readString(), + Part.readFrom(parcel), + Part.readFrom(parcel) + ); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(TYPE_ID); + parcel.writeString(scheme); + ssp.writeTo(parcel); + fragment.writeTo(parcel); + } + + public boolean isHierarchical() { + return false; + } + + public boolean isRelative() { + return scheme == null; + } + + public String getScheme() { + return this.scheme; + } + + public String getEncodedSchemeSpecificPart() { + return ssp.getEncoded(); + } + + public String getSchemeSpecificPart() { + return ssp.getDecoded(); + } + + public String getAuthority() { + return null; + } + + public String getEncodedAuthority() { + return null; + } + + public String getPath() { + return null; + } + + public String getEncodedPath() { + return null; + } + + public String getQuery() { + return null; + } + + public String getEncodedQuery() { + return null; + } + + public String getFragment() { + return fragment.getDecoded(); + } + + public String getEncodedFragment() { + return fragment.getEncoded(); + } + + public List<String> getPathSegments() { + return Collections.emptyList(); + } + + public String getLastPathSegment() { + return null; + } + + public String getUserInfo() { + return null; + } + + public String getEncodedUserInfo() { + return null; + } + + public String getHost() { + return null; + } + + public int getPort() { + return -1; + } + + private volatile String cachedString = NOT_CACHED; + + public String toString() { + @SuppressWarnings("StringEquality") + boolean cached = cachedString != NOT_CACHED; + if (cached) { + return cachedString; + } + + StringBuilder sb = new StringBuilder(); + + sb.append(scheme).append(':'); + sb.append(getEncodedSchemeSpecificPart()); + + if (!fragment.isEmpty()) { + sb.append('#').append(fragment.getEncoded()); + } + + return cachedString = sb.toString(); + } + + public Builder buildUpon() { + return new Builder() + .scheme(this.scheme) + .opaquePart(this.ssp) + .fragment(this.fragment); + } + } + + /** + * Wrapper for path segment array. + */ + static class PathSegments extends AbstractList<String> + implements RandomAccess { + + static final PathSegments EMPTY = new PathSegments(null, 0); + + final String[] segments; + final int size; + + PathSegments(String[] segments, int size) { + this.segments = segments; + this.size = size; + } + + public String get(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException(); + } + + return segments[index]; + } + + public int size() { + return this.size; + } + } + + /** + * Builds PathSegments. + */ + static class PathSegmentsBuilder { + + String[] segments; + int size = 0; + + void add(String segment) { + if (segments == null) { + segments = new String[4]; + } else if (size + 1 == segments.length) { + String[] expanded = new String[segments.length * 2]; + System.arraycopy(segments, 0, expanded, 0, segments.length); + segments = expanded; + } + + segments[size++] = segment; + } + + PathSegments build() { + if (segments == null) { + return PathSegments.EMPTY; + } + + try { + return new PathSegments(segments, size); + } finally { + // Makes sure this doesn't get reused. + segments = null; + } + } + } + + /** + * Support for hierarchical URIs. + */ + private abstract static class AbstractHierarchicalUri extends Uri { + + public String getLastPathSegment() { + // TODO: If we haven't parsed all of the segments already, just + // grab the last one directly so we only allocate one string. + + List<String> segments = getPathSegments(); + int size = segments.size(); + if (size == 0) { + return null; + } + return segments.get(size - 1); + } + + private Part userInfo; + + private Part getUserInfoPart() { + return userInfo == null + ? userInfo = Part.fromEncoded(parseUserInfo()) : userInfo; + } + + public final String getEncodedUserInfo() { + return getUserInfoPart().getEncoded(); + } + + private String parseUserInfo() { + String authority = getEncodedAuthority(); + if (authority == null) { + return null; + } + + int end = authority.indexOf('@'); + return end == NOT_FOUND ? null : authority.substring(0, end); + } + + public String getUserInfo() { + return getUserInfoPart().getDecoded(); + } + + private volatile String host = NOT_CACHED; + + public String getHost() { + @SuppressWarnings("StringEquality") + boolean cached = (host != NOT_CACHED); + return cached ? host + : (host = parseHost()); + } + + private String parseHost() { + String authority = getAuthority(); + if (authority == null) { + return null; + } + + // Parse out user info and then port. + int userInfoSeparator = authority.indexOf('@'); + int portSeparator = authority.indexOf(':', userInfoSeparator); + + return portSeparator == NOT_FOUND + ? authority.substring(userInfoSeparator + 1) + : authority.substring(userInfoSeparator + 1, portSeparator); + } + + private volatile int port = NOT_CALCULATED; + + public int getPort() { + return port == NOT_CALCULATED + ? port = parsePort() + : port; + } + + private int parsePort() { + String authority = getAuthority(); + if (authority == null) { + return -1; + } + + // Make sure we look for the port separtor *after* the user info + // separator. We have URLs with a ':' in the user info. + int userInfoSeparator = authority.indexOf('@'); + int portSeparator = authority.indexOf(':', userInfoSeparator); + + if (portSeparator == NOT_FOUND) { + return -1; + } + + String portString = authority.substring(portSeparator + 1); + try { + return Integer.parseInt(portString); + } catch (NumberFormatException e) { + Log.w(LOG, "Error parsing port string.", e); + return -1; + } + } + } + + /** + * Hierarchical Uri. + */ + private static class HierarchicalUri extends AbstractHierarchicalUri { + + /** Used in parcelling. */ + static final int TYPE_ID = 3; + + private final String scheme; + private final Part authority; + private final PathPart path; + private final Part query; + private final Part fragment; + + private HierarchicalUri(String scheme, Part authority, PathPart path, + Part query, Part fragment) { + this.scheme = scheme; + this.authority = authority; + this.path = path; + this.query = query; + this.fragment = fragment; + } + + static Uri readFrom(Parcel parcel) { + return new HierarchicalUri( + parcel.readString(), + Part.readFrom(parcel), + PathPart.readFrom(parcel), + Part.readFrom(parcel), + Part.readFrom(parcel) + ); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(TYPE_ID); + parcel.writeString(scheme); + authority.writeTo(parcel); + path.writeTo(parcel); + query.writeTo(parcel); + fragment.writeTo(parcel); + } + + public boolean isHierarchical() { + return true; + } + + public boolean isRelative() { + return scheme == null; + } + + public String getScheme() { + return scheme; + } + + private Part ssp; + + private Part getSsp() { + return ssp == null + ? ssp = Part.fromEncoded(makeSchemeSpecificPart()) : ssp; + } + + public String getEncodedSchemeSpecificPart() { + return getSsp().getEncoded(); + } + + public String getSchemeSpecificPart() { + return getSsp().getDecoded(); + } + + /** + * Creates the encoded scheme-specific part from its sub parts. + */ + private String makeSchemeSpecificPart() { + StringBuilder builder = new StringBuilder(); + appendSspTo(builder); + return builder.toString(); + } + + private void appendSspTo(StringBuilder builder) { + if (authority != null) { + String encodedAuthority = authority.getEncoded(); + if (encodedAuthority != null) { + // Even if the authority is "", we still want to append "//". + builder.append("//").append(encodedAuthority); + } + } + + // path is never null. + String encodedPath = path.getEncoded(); + if (encodedPath != null) { + builder.append(encodedPath); + } + + if (query != null && !query.isEmpty()) { + builder.append('?').append(query.getEncoded()); + } + } + + public String getAuthority() { + return this.authority.getDecoded(); + } + + public String getEncodedAuthority() { + return this.authority.getEncoded(); + } + + public String getEncodedPath() { + return this.path.getEncoded(); + } + + public String getPath() { + return this.path.getDecoded(); + } + + public String getQuery() { + return this.query.getDecoded(); + } + + public String getEncodedQuery() { + return this.query.getEncoded(); + } + + public String getFragment() { + return this.fragment.getDecoded(); + } + + public String getEncodedFragment() { + return this.fragment.getEncoded(); + } + + public List<String> getPathSegments() { + return this.path.getPathSegments(); + } + + private volatile String uriString = NOT_CACHED; + + @Override + public String toString() { + @SuppressWarnings("StringEquality") + boolean cached = (uriString != NOT_CACHED); + return cached ? uriString + : (uriString = makeUriString()); + } + + private String makeUriString() { + StringBuilder builder = new StringBuilder(); + + if (scheme != null) { + builder.append(scheme).append(':'); + } + + appendSspTo(builder); + + if (fragment != null && !fragment.isEmpty()) { + builder.append('#').append(fragment.getEncoded()); + } + + return builder.toString(); + } + + public Builder buildUpon() { + return new Builder() + .scheme(scheme) + .authority(authority) + .path(path) + .query(query) + .fragment(fragment); + } + } + + /** + * Helper class for building or manipulating URI references. Not safe for + * concurrent use. + * + * <p>An absolute hierarchical URI reference follows the pattern: + * {@code <scheme>://<authority><absolute path>?<query>#<fragment>} + * + * <p>Relative URI references (which are always hierarchical) follow one + * of two patterns: {@code <relative or absolute path>?<query>#<fragment>} + * or {@code //<authority><absolute path>?<query>#<fragment>} + * + * <p>An opaque URI follows this pattern: + * {@code <scheme>:<opaque part>#<fragment>} + */ + public static final class Builder { + + private String scheme; + private Part opaquePart; + private Part authority; + private PathPart path; + private Part query; + private Part fragment; + + /** + * Constructs a new Builder. + */ + public Builder() {} + + /** + * Sets the scheme. + * + * @param scheme name or {@code null} if this is a relative Uri + */ + public Builder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + Builder opaquePart(Part opaquePart) { + this.opaquePart = opaquePart; + return this; + } + + /** + * Encodes and sets the given opaque scheme-specific-part. + * + * @param opaquePart decoded opaque part + */ + public Builder opaquePart(String opaquePart) { + return opaquePart(Part.fromDecoded(opaquePart)); + } + + /** + * Sets the previously encoded opaque scheme-specific-part. + * + * @param opaquePart encoded opaque part + */ + public Builder encodedOpaquePart(String opaquePart) { + return opaquePart(Part.fromEncoded(opaquePart)); + } + + Builder authority(Part authority) { + // This URI will be hierarchical. + this.opaquePart = null; + + this.authority = authority; + return this; + } + + /** + * Encodes and sets the authority. + */ + public Builder authority(String authority) { + return authority(Part.fromDecoded(authority)); + } + + /** + * Sets the previously encoded authority. + */ + public Builder encodedAuthority(String authority) { + return authority(Part.fromEncoded(authority)); + } + + Builder path(PathPart path) { + // This URI will be hierarchical. + this.opaquePart = null; + + this.path = path; + return this; + } + + /** + * Sets the path. Leaves '/' characters intact but encodes others as + * necessary. + * + * <p>If the path is not null and doesn't start with a '/', and if + * you specify a scheme and/or authority, the builder will prepend the + * given path with a '/'. + */ + public Builder path(String path) { + return path(PathPart.fromDecoded(path)); + } + + /** + * Sets the previously encoded path. + * + * <p>If the path is not null and doesn't start with a '/', and if + * you specify a scheme and/or authority, the builder will prepend the + * given path with a '/'. + */ + public Builder encodedPath(String path) { + return path(PathPart.fromEncoded(path)); + } + + /** + * Encodes the given segment and appends it to the path. + */ + public Builder appendPath(String newSegment) { + return path(PathPart.appendDecodedSegment(path, newSegment)); + } + + /** + * Appends the given segment to the path. + */ + public Builder appendEncodedPath(String newSegment) { + return path(PathPart.appendEncodedSegment(path, newSegment)); + } + + Builder query(Part query) { + // This URI will be hierarchical. + this.opaquePart = null; + + this.query = query; + return this; + } + + /** + * Encodes and sets the query. + */ + public Builder query(String query) { + return query(Part.fromDecoded(query)); + } + + /** + * Sets the previously encoded query. + */ + public Builder encodedQuery(String query) { + return query(Part.fromEncoded(query)); + } + + Builder fragment(Part fragment) { + this.fragment = fragment; + return this; + } + + /** + * Encodes and sets the fragment. + */ + public Builder fragment(String fragment) { + return fragment(Part.fromDecoded(fragment)); + } + + /** + * Sets the previously encoded fragment. + */ + public Builder encodedFragment(String fragment) { + return fragment(Part.fromEncoded(fragment)); + } + + /** + * Encodes the key and value and then appends the parameter to the + * query string. + * + * @param key which will be encoded + * @param value which will be encoded + */ + public Builder appendQueryParameter(String key, String value) { + // This URI will be hierarchical. + this.opaquePart = null; + + String encodedParameter = encode(key, null) + "=" + + encode(value, null); + + if (query == null) { + query = Part.fromEncoded(encodedParameter); + return this; + } + + String oldQuery = query.getEncoded(); + if (oldQuery == null || oldQuery.length() == 0) { + query = Part.fromEncoded(encodedParameter); + } else { + query = Part.fromEncoded(oldQuery + "&" + encodedParameter); + } + + return this; + } + + /** + * Constructs a Uri with the current attributes. + * + * @throws UnsupportedOperationException if the URI is opaque and the + * scheme is null + */ + public Uri build() { + if (opaquePart != null) { + if (this.scheme == null) { + throw new UnsupportedOperationException( + "An opaque URI must have a scheme."); + } + + return new OpaqueUri(scheme, opaquePart, fragment); + } else { + // Hierarchical URIs should not return null for getPath(). + PathPart path = this.path; + if (path == null || path == PathPart.NULL) { + path = PathPart.EMPTY; + } else { + // If we have a scheme and/or authority, the path must + // be absolute. Prepend it with a '/' if necessary. + if (hasSchemeOrAuthority()) { + path = PathPart.makeAbsolute(path); + } + } + + return new HierarchicalUri( + scheme, authority, path, query, fragment); + } + } + + private boolean hasSchemeOrAuthority() { + return scheme != null + || (authority != null && authority != Part.NULL); + + } + + @Override + public String toString() { + return build().toString(); + } + } + + /** + * Searches the query string for parameter values with the given key. + * + * @param key which will be encoded + * + * @throws UnsupportedOperationException if this isn't a hierarchical URI + * @throws NullPointerException if key is null + * + * @return a list of decoded values + */ + public List<String> getQueryParameters(String key) { + if (isOpaque()) { + throw new UnsupportedOperationException(NOT_HIERARCHICAL); + } + + String query = getQuery(); + if (query == null) { + return Collections.emptyList(); + } + + String encodedKey; + try { + encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + + // Prepend query with "&" making the first parameter the same as the + // rest. + query = "&" + query; + + // Parameter prefix. + String prefix = "&" + encodedKey + "="; + + ArrayList<String> values = new ArrayList<String>(); + + int start = 0; + int length = query.length(); + while (start < length) { + start = query.indexOf(prefix, start); + + if (start == -1) { + // No more values. + break; + } + + // Move start to start of value. + start += prefix.length(); + + // Find end of value. + int end = query.indexOf('&', start); + if (end == -1) { + end = query.length(); + } + + String value = query.substring(start, end); + values.add(decode(value)); + + start = end; + } + + return Collections.unmodifiableList(values); + } + + /** + * Searches the query string for the first value with the given key. + * + * @param key which will be encoded + * @throws UnsupportedOperationException if this isn't a hierarchical URI + * @throws NullPointerException if key is null + * + * @return the decoded value or null if no parameter is found + */ + public String getQueryParameter(String key) { + if (isOpaque()) { + throw new UnsupportedOperationException(NOT_HIERARCHICAL); + } + + String query = getQuery(); + + if (query == null) { + return null; + } + + String encodedKey; + try { + encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + + String prefix = encodedKey + "="; + + if (query.length() < prefix.length()) { + return null; + } + + int start; + if (query.startsWith(prefix)) { + // It's the first parameter. + start = prefix.length(); + } else { + // It must be later in the query string. + prefix = "&" + prefix; + start = query.indexOf(prefix); + + if (start == -1) { + // Not found. + return null; + } + + start += prefix.length(); + } + + // Find end of value. + int end = query.indexOf('&', start); + if (end == -1) { + end = query.length(); + } + + String value = query.substring(start, end); + return decode(value); + } + + /** Identifies a null parcelled Uri. */ + private static final int NULL_TYPE_ID = 0; + + /** + * Reads Uris from Parcels. + */ + public static final Parcelable.Creator<Uri> CREATOR + = new Parcelable.Creator<Uri>() { + public Uri createFromParcel(Parcel in) { + int type = in.readInt(); + switch (type) { + case NULL_TYPE_ID: return null; + case StringUri.TYPE_ID: return StringUri.readFrom(in); + case OpaqueUri.TYPE_ID: return OpaqueUri.readFrom(in); + case HierarchicalUri.TYPE_ID: + return HierarchicalUri.readFrom(in); + } + + throw new AssertionError("Unknown URI type: " + type); + } + + public Uri[] newArray(int size) { + return new Uri[size]; + } + }; + + /** + * Writes a Uri to a Parcel. + * + * @param out parcel to write to + * @param uri to write, can be null + */ + public static void writeToParcel(Parcel out, Uri uri) { + if (uri == null) { + out.writeInt(NULL_TYPE_ID); + } else { + uri.writeToParcel(out, 0); + } + } + + private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); + + /** + * Encodes characters in the given string as '%'-escaped octets + * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers + * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes + * all other characters. + * + * @param s string to encode + * @return an encoded version of s suitable for use as a URI component, + * or null if s is null + */ + public static String encode(String s) { + return encode(s, null); + } + + /** + * Encodes characters in the given string as '%'-escaped octets + * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers + * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes + * all other characters with the exception of those specified in the + * allow argument. + * + * @param s string to encode + * @param allow set of additional characters to allow in the encoded form, + * null if no characters should be skipped + * @return an encoded version of s suitable for use as a URI component, + * or null if s is null + */ + public static String encode(String s, String allow) { + if (s == null) { + return null; + } + + // Lazily-initialized buffers. + StringBuilder encoded = null; + + int oldLength = s.length(); + + // This loop alternates between copying over allowed characters and + // encoding in chunks. This results in fewer method calls and + // allocations than encoding one character at a time. + int current = 0; + while (current < oldLength) { + // Start in "copying" mode where we copy over allowed chars. + + // Find the next character which needs to be encoded. + int nextToEncode = current; + while (nextToEncode < oldLength + && isAllowed(s.charAt(nextToEncode), allow)) { + nextToEncode++; + } + + // If there's nothing more to encode... + if (nextToEncode == oldLength) { + if (current == 0) { + // We didn't need to encode anything! + return s; + } else { + // Presumably, we've already done some encoding. + encoded.append(s, current, oldLength); + return encoded.toString(); + } + } + + if (encoded == null) { + encoded = new StringBuilder(); + } + + if (nextToEncode > current) { + // Append allowed characters leading up to this point. + encoded.append(s, current, nextToEncode); + } else { + // assert nextToEncode == current + } + + // Switch to "encoding" mode. + + // Find the next allowed character. + current = nextToEncode; + int nextAllowed = current + 1; + while (nextAllowed < oldLength + && !isAllowed(s.charAt(nextAllowed), allow)) { + nextAllowed++; + } + + // Convert the substring to bytes and encode the bytes as + // '%'-escaped octets. + String toEncode = s.substring(current, nextAllowed); + try { + byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING); + int bytesLength = bytes.length; + for (int i = 0; i < bytesLength; i++) { + encoded.append('%'); + encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]); + encoded.append(HEX_DIGITS[bytes[i] & 0xf]); + } + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + + current = nextAllowed; + } + + // Encoded could still be null at this point if s is empty. + return encoded == null ? s : encoded.toString(); + } + + /** + * Returns true if the given character is allowed. + * + * @param c character to check + * @param allow characters to allow + * @return true if the character is allowed or false if it should be + * encoded + */ + private static boolean isAllowed(char c, String allow) { + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || "_-!.~'()*".indexOf(c) != NOT_FOUND + || (allow != null && allow.indexOf(c) != NOT_FOUND); + } + + /** Unicode replacement character: \\uFFFD. */ + private static final byte[] REPLACEMENT = { (byte) 0xFF, (byte) 0xFD }; + + /** + * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. + * Replaces invalid octets with the unicode replacement character + * ("\\uFFFD"). + * + * @param s encoded string to decode + * @return the given string with escaped octets decoded, or null if + * s is null + */ + public static String decode(String s) { + /* + Compared to java.net.URLEncoderDecoder.decode(), this method decodes a + chunk at a time instead of one character at a time, and it doesn't + throw exceptions. It also only allocates memory when necessary--if + there's nothing to decode, this method won't do much. + */ + + if (s == null) { + return null; + } + + // Lazily-initialized buffers. + StringBuilder decoded = null; + ByteArrayOutputStream out = null; + + int oldLength = s.length(); + + // This loop alternates between copying over normal characters and + // escaping in chunks. This results in fewer method calls and + // allocations than decoding one character at a time. + int current = 0; + while (current < oldLength) { + // Start in "copying" mode where we copy over normal characters. + + // Find the next escape sequence. + int nextEscape = s.indexOf('%', current); + + if (nextEscape == NOT_FOUND) { + if (decoded == null) { + // We didn't actually decode anything. + return s; + } else { + // Append the remainder and return the decoded string. + decoded.append(s, current, oldLength); + return decoded.toString(); + } + } + + // Prepare buffers. + if (decoded == null) { + // Looks like we're going to need the buffers... + // We know the new string will be shorter. Using the old length + // may overshoot a bit, but it will save us from resizing the + // buffer. + decoded = new StringBuilder(oldLength); + out = new ByteArrayOutputStream(4); + } else { + // Clear decoding buffer. + out.reset(); + } + + // Append characters leading up to the escape. + if (nextEscape > current) { + decoded.append(s, current, nextEscape); + + current = nextEscape; + } else { + // assert current == nextEscape + } + + // Switch to "decoding" mode where we decode a string of escape + // sequences. + + // Decode and append escape sequences. Escape sequences look like + // "%ab" where % is literal and a and b are hex digits. + try { + do { + if (current + 2 >= oldLength) { + // Truncated escape sequence. + out.write(REPLACEMENT); + } else { + int a = Character.digit(s.charAt(current + 1), 16); + int b = Character.digit(s.charAt(current + 2), 16); + + if (a == -1 || b == -1) { + // Non hex digits. + out.write(REPLACEMENT); + } else { + // Combine the hex digits into one byte and write. + out.write((a << 4) + b); + } + } + + // Move passed the escape sequence. + current += 3; + } while (current < oldLength && s.charAt(current) == '%'); + + // Decode UTF-8 bytes into a string and append it. + decoded.append(out.toString(DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + // If we don't have a buffer, we didn't have to decode anything. + return decoded == null ? s : decoded.toString(); + } + + /** + * Support for part implementations. + */ + static abstract class AbstractPart { + + /** + * Enum which indicates which representation of a given part we have. + */ + static class Representation { + static final int BOTH = 0; + static final int ENCODED = 1; + static final int DECODED = 2; + } + + volatile String encoded; + volatile String decoded; + + AbstractPart(String encoded, String decoded) { + this.encoded = encoded; + this.decoded = decoded; + } + + abstract String getEncoded(); + + final String getDecoded() { + @SuppressWarnings("StringEquality") + boolean hasDecoded = decoded != NOT_CACHED; + return hasDecoded ? decoded : (decoded = decode(encoded)); + } + + final void writeTo(Parcel parcel) { + @SuppressWarnings("StringEquality") + boolean hasEncoded = encoded != NOT_CACHED; + + @SuppressWarnings("StringEquality") + boolean hasDecoded = decoded != NOT_CACHED; + + if (hasEncoded && hasDecoded) { + parcel.writeInt(Representation.BOTH); + parcel.writeString(encoded); + parcel.writeString(decoded); + } else if (hasEncoded) { + parcel.writeInt(Representation.ENCODED); + parcel.writeString(encoded); + } else if (hasDecoded) { + parcel.writeInt(Representation.DECODED); + parcel.writeString(decoded); + } else { + throw new AssertionError(); + } + } + } + + /** + * Immutable wrapper of encoded and decoded versions of a URI part. Lazily + * creates the encoded or decoded version from the other. + */ + static class Part extends AbstractPart { + + /** A part with null values. */ + static final Part NULL = new EmptyPart(null); + + /** A part with empty strings for values. */ + static final Part EMPTY = new EmptyPart(""); + + private Part(String encoded, String decoded) { + super(encoded, decoded); + } + + boolean isEmpty() { + return false; + } + + String getEncoded() { + @SuppressWarnings("StringEquality") + boolean hasEncoded = encoded != NOT_CACHED; + return hasEncoded ? encoded : (encoded = encode(decoded)); + } + + static Part readFrom(Parcel parcel) { + int representation = parcel.readInt(); + switch (representation) { + case Representation.BOTH: + return from(parcel.readString(), parcel.readString()); + case Representation.ENCODED: + return fromEncoded(parcel.readString()); + case Representation.DECODED: + return fromDecoded(parcel.readString()); + default: + throw new AssertionError(); + } + } + + /** + * Returns given part or {@link #NULL} if the given part is null. + */ + static Part nonNull(Part part) { + return part == null ? NULL : part; + } + + /** + * Creates a part from the encoded string. + * + * @param encoded part string + */ + static Part fromEncoded(String encoded) { + return from(encoded, NOT_CACHED); + } + + /** + * Creates a part from the decoded string. + * + * @param decoded part string + */ + static Part fromDecoded(String decoded) { + return from(NOT_CACHED, decoded); + } + + /** + * Creates a part from the encoded and decoded strings. + * + * @param encoded part string + * @param decoded part string + */ + static Part from(String encoded, String decoded) { + // We have to check both encoded and decoded in case one is + // NOT_CACHED. + + if (encoded == null) { + return NULL; + } + if (encoded.length() == 0) { + return EMPTY; + } + + if (decoded == null) { + return NULL; + } + if (decoded .length() == 0) { + return EMPTY; + } + + return new Part(encoded, decoded); + } + + private static class EmptyPart extends Part { + public EmptyPart(String value) { + super(value, value); + } + + @Override + boolean isEmpty() { + return true; + } + } + } + + /** + * Immutable wrapper of encoded and decoded versions of a path part. Lazily + * creates the encoded or decoded version from the other. + */ + static class PathPart extends AbstractPart { + + /** A part with null values. */ + static final PathPart NULL = new PathPart(null, null); + + /** A part with empty strings for values. */ + static final PathPart EMPTY = new PathPart("", ""); + + private PathPart(String encoded, String decoded) { + super(encoded, decoded); + } + + String getEncoded() { + @SuppressWarnings("StringEquality") + boolean hasEncoded = encoded != NOT_CACHED; + + // Don't encode '/'. + return hasEncoded ? encoded : (encoded = encode(decoded, "/")); + } + + /** + * Cached path segments. This doesn't need to be volatile--we don't + * care if other threads see the result. + */ + private PathSegments pathSegments; + + /** + * Gets the individual path segments. Parses them if necessary. + * + * @return parsed path segments or null if this isn't a hierarchical + * URI + */ + PathSegments getPathSegments() { + if (pathSegments != null) { + return pathSegments; + } + + String path = getEncoded(); + if (path == null) { + return pathSegments = PathSegments.EMPTY; + } + + PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder(); + + int previous = 0; + int current; + while ((current = path.indexOf('/', previous)) > -1) { + // This check keeps us from adding a segment if the path starts + // '/' and an empty segment for "//". + if (previous < current) { + String decodedSegment + = decode(path.substring(previous, current)); + segmentBuilder.add(decodedSegment); + } + previous = current + 1; + } + + // Add in the final path segment. + if (previous < path.length()) { + segmentBuilder.add(decode(path.substring(previous))); + } + + return pathSegments = segmentBuilder.build(); + } + + static PathPart appendEncodedSegment(PathPart oldPart, + String newSegment) { + // If there is no old path, should we make the new path relative + // or absolute? I pick absolute. + + if (oldPart == null) { + // No old path. + return fromEncoded("/" + newSegment); + } + + String oldPath = oldPart.getEncoded(); + + if (oldPath == null) { + oldPath = ""; + } + + int oldPathLength = oldPath.length(); + String newPath; + if (oldPathLength == 0) { + // No old path. + newPath = "/" + newSegment; + } else if (oldPath.charAt(oldPathLength - 1) == '/') { + newPath = oldPath + newSegment; + } else { + newPath = oldPath + "/" + newSegment; + } + + return fromEncoded(newPath); + } + + static PathPart appendDecodedSegment(PathPart oldPart, String decoded) { + String encoded = encode(decoded); + + // TODO: Should we reuse old PathSegments? Probably not. + return appendEncodedSegment(oldPart, encoded); + } + + static PathPart readFrom(Parcel parcel) { + int representation = parcel.readInt(); + switch (representation) { + case Representation.BOTH: + return from(parcel.readString(), parcel.readString()); + case Representation.ENCODED: + return fromEncoded(parcel.readString()); + case Representation.DECODED: + return fromDecoded(parcel.readString()); + default: + throw new AssertionError(); + } + } + + /** + * Creates a path from the encoded string. + * + * @param encoded part string + */ + static PathPart fromEncoded(String encoded) { + return from(encoded, NOT_CACHED); + } + + /** + * Creates a path from the decoded string. + * + * @param decoded part string + */ + static PathPart fromDecoded(String decoded) { + return from(NOT_CACHED, decoded); + } + + /** + * Creates a path from the encoded and decoded strings. + * + * @param encoded part string + * @param decoded part string + */ + static PathPart from(String encoded, String decoded) { + if (encoded == null) { + return NULL; + } + + if (encoded.length() == 0) { + return EMPTY; + } + + return new PathPart(encoded, decoded); + } + + /** + * Prepends path values with "/" if they're present, not empty, and + * they don't already start with "/". + */ + static PathPart makeAbsolute(PathPart oldPart) { + @SuppressWarnings("StringEquality") + boolean encodedCached = oldPart.encoded != NOT_CACHED; + + // We don't care which version we use, and we don't want to force + // unneccessary encoding/decoding. + String oldPath = encodedCached ? oldPart.encoded : oldPart.decoded; + + if (oldPath == null || oldPath.length() == 0 + || oldPath.startsWith("/")) { + return oldPart; + } + + // Prepend encoded string if present. + String newEncoded = encodedCached + ? "/" + oldPart.encoded : NOT_CACHED; + + // Prepend decoded string if present. + @SuppressWarnings("StringEquality") + boolean decodedCached = oldPart.decoded != NOT_CACHED; + String newDecoded = decodedCached + ? "/" + oldPart.decoded + : NOT_CACHED; + + return new PathPart(newEncoded, newDecoded); + } + } + + /** + * Creates a new Uri by encoding and appending a path segment to a base Uri. + * + * @param baseUri Uri to append path segment to + * @param pathSegment to encode and append + * @return a new Uri based on baseUri with the given segment encoded and + * appended to the path + * @throws NullPointerException if baseUri is null + */ + public static Uri withAppendedPath(Uri baseUri, String pathSegment) { + Builder builder = baseUri.buildUpon(); + builder = builder.appendEncodedPath(pathSegment); + return builder.build(); + } +} diff --git a/core/java/android/net/UrlQuerySanitizer.java b/core/java/android/net/UrlQuerySanitizer.java new file mode 100644 index 0000000..70e50b7 --- /dev/null +++ b/core/java/android/net/UrlQuerySanitizer.java @@ -0,0 +1,913 @@ +/* + * 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 android.net; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; + +/** + * + * Sanitizes the Query portion of a URL. Simple example: + * <code> + * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(); + * sanitizer.setAllowUnregisteredParamaters(true); + * sanitizer.parseUrl("http://example.com/?name=Joe+User"); + * String name = sanitizer.getValue("name")); + * // name now contains "Joe_User" + * </code> + * + * Register ValueSanitizers to customize the way individual + * parameters are sanitized: + * <code> + * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(); + * sanitizer.registerParamater("name", UrlQuerySanitizer.createSpaceLegal()); + * sanitizer.parseUrl("http://example.com/?name=Joe+User"); + * String name = sanitizer.getValue("name")); + * // name now contains "Joe User". (The string is first decoded, which + * // converts the '+' to a ' '. Then the string is sanitized, which + * // converts the ' ' to an '_'. (The ' ' is converted because the default + * unregistered parameter sanitizer does not allow any special characters, + * and ' ' is a special character.) + * </code> + * + * There are several ways to create ValueSanitizers. In order of increasing + * sophistication: + * <ol> + * <li>Call one of the UrlQuerySanitizer.createXXX() methods. + * <li>Construct your own instance of + * UrlQuerySanitizer.IllegalCharacterValueSanitizer. + * <li>Subclass UrlQuerySanitizer.ValueSanitizer to define your own value + * sanitizer. + * </ol> + * + */ +public class UrlQuerySanitizer { + + /** + * A simple tuple that holds parameter-value pairs. + * + */ + public class ParameterValuePair { + /** + * Construct a parameter-value tuple. + * @param parameter an unencoded parameter + * @param value an unencoded value + */ + public ParameterValuePair(String parameter, + String value) { + mParameter = parameter; + mValue = value; + } + /** + * The unencoded parameter + */ + public String mParameter; + /** + * The unencoded value + */ + public String mValue; + } + + final private HashMap<String, ValueSanitizer> mSanitizers = + new HashMap<String, ValueSanitizer>(); + final private HashMap<String, String> mEntries = + new HashMap<String, String>(); + final private ArrayList<ParameterValuePair> mEntriesList = + new ArrayList<ParameterValuePair>(); + private boolean mAllowUnregisteredParamaters; + private boolean mPreferFirstRepeatedParameter; + private ValueSanitizer mUnregisteredParameterValueSanitizer = + getAllIllegal(); + + /** + * A functor used to sanitize a single query value. + * + */ + public static interface ValueSanitizer { + /** + * Sanitize an unencoded value. + * @param value + * @return the sanitized unencoded value + */ + public String sanitize(String value); + } + + /** + * Sanitize values based on which characters they contain. Illegal + * characters are replaced with either space or '_', depending upon + * whether space is a legal character or not. + */ + public static class IllegalCharacterValueSanitizer implements + ValueSanitizer { + private int mFlags; + + /** + * Allow space (' ') characters. + */ + public final static int SPACE_OK = 1 << 0; + /** + * Allow whitespace characters other than space. The + * other whitespace characters are + * '\t' '\f' '\n' '\r' and '\0x000b' (vertical tab) + */ + public final static int OTHER_WHITESPACE_OK = 1 << 1; + /** + * Allow characters with character codes 128 to 255. + */ + public final static int NON_7_BIT_ASCII_OK = 1 << 2; + /** + * Allow double quote characters. ('"') + */ + public final static int DQUOTE_OK = 1 << 3; + /** + * Allow single quote characters. ('\'') + */ + public final static int SQUOTE_OK = 1 << 4; + /** + * Allow less-than characters. ('<') + */ + public final static int LT_OK = 1 << 5; + /** + * Allow greater-than characters. ('>') + */ + public final static int GT_OK = 1 << 6; + /** + * Allow ampersand characters ('&') + */ + public final static int AMP_OK = 1 << 7; + /** + * Allow percent-sign characters ('%') + */ + public final static int PCT_OK = 1 << 8; + /** + * Allow nul characters ('\0') + */ + public final static int NUL_OK = 1 << 9; + /** + * Allow text to start with a script URL + * such as "javascript:" or "vbscript:" + */ + public final static int SCRIPT_URL_OK = 1 << 10; + + /** + * Mask with all fields set to OK + */ + public final static int ALL_OK = 0x7ff; + + /** + * Mask with both regular space and other whitespace OK + */ + public final static int ALL_WHITESPACE_OK = + SPACE_OK | OTHER_WHITESPACE_OK; + + + // Common flag combinations: + + /** + * <ul> + * <li>Deny all special characters. + * <li>Deny script URLs. + * </ul> + */ + public final static int ALL_ILLEGAL = + 0; + /** + * <ul> + * <li>Allow all special characters except Nul. ('\0'). + * <li>Allow script URLs. + * </ul> + */ + public final static int ALL_BUT_NUL_LEGAL = + ALL_OK & ~NUL_OK; + /** + * <ul> + * <li>Allow all special characters except for: + * <ul> + * <li>whitespace characters + * <li>Nul ('\0') + * </ul> + * <li>Allow script URLs. + * </ul> + */ + public final static int ALL_BUT_WHITESPACE_LEGAL = + ALL_OK & ~(ALL_WHITESPACE_OK | NUL_OK); + /** + * <ul> + * <li>Allow characters used by encoded URLs. + * <li>Deny script URLs. + * </ul> + */ + public final static int URL_LEGAL = + NON_7_BIT_ASCII_OK | SQUOTE_OK | AMP_OK | PCT_OK; + /** + * <ul> + * <li>Allow characters used by encoded URLs. + * <li>Allow spaces. + * <li>Deny script URLs. + * </ul> + */ + public final static int URL_AND_SPACE_LEGAL = + URL_LEGAL | SPACE_OK; + /** + * <ul> + * <li>Allow ampersand. + * <li>Deny script URLs. + * </ul> + */ + public final static int AMP_LEGAL = + AMP_OK; + /** + * <ul> + * <li>Allow ampersand. + * <li>Allow space. + * <li>Deny script URLs. + * </ul> + */ + public final static int AMP_AND_SPACE_LEGAL = + AMP_OK | SPACE_OK; + /** + * <ul> + * <li>Allow space. + * <li>Deny script URLs. + * </ul> + */ + public final static int SPACE_LEGAL = + SPACE_OK; + /** + * <ul> + * <li>Allow all but. + * <ul> + * <li>Nul ('\0') + * <li>Angle brackets ('<', '>') + * </ul> + * <li>Deny script URLs. + * </ul> + */ + public final static int ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL = + ALL_OK & ~(NUL_OK | LT_OK | GT_OK); + + /** + * Script URL definitions + */ + + private final static String JAVASCRIPT_PREFIX = "javascript:"; + + private final static String VBSCRIPT_PREFIX = "vbscript:"; + + private final static int MIN_SCRIPT_PREFIX_LENGTH = Math.min( + JAVASCRIPT_PREFIX.length(), VBSCRIPT_PREFIX.length()); + + /** + * Construct a sanitizer. The parameters set the behavior of the + * sanitizer. + * @param flags some combination of the XXX_OK flags. + */ + public IllegalCharacterValueSanitizer( + int flags) { + mFlags = flags; + } + /** + * Sanitize a value. + * <ol> + * <li>If script URLs are not OK, the will be removed. + * <li>If neither spaces nor other white space is OK, then + * white space will be trimmed from the beginning and end of + * the URL. (Just the actual white space characters are trimmed, not + * other control codes.) + * <li> Illegal characters will be replaced with + * either ' ' or '_', depending on whether a space is itself a + * legal character. + * </ol> + * @param value + * @return the sanitized value + */ + public String sanitize(String value) { + if (value == null) { + return null; + } + int length = value.length(); + if ((mFlags & SCRIPT_URL_OK) != 0) { + if (length >= MIN_SCRIPT_PREFIX_LENGTH) { + String asLower = value.toLowerCase(); + if (asLower.startsWith(JAVASCRIPT_PREFIX) || + asLower.startsWith(VBSCRIPT_PREFIX)) { + return ""; + } + } + } + + // If whitespace isn't OK, get rid of whitespace at beginning + // and end of value. + if ( (mFlags & ALL_WHITESPACE_OK) == 0) { + value = trimWhitespace(value); + // The length could have changed, so we need to correct + // the length variable. + length = value.length(); + } + + StringBuilder stringBuilder = new StringBuilder(length); + for(int i = 0; i < length; i++) { + char c = value.charAt(i); + if (!characterIsLegal(c)) { + if ((mFlags & SPACE_OK) != 0) { + c = ' '; + } + else { + c = '_'; + } + } + stringBuilder.append(c); + } + return stringBuilder.toString(); + } + + /** + * Trim whitespace from the beginning and end of a string. + * <p> + * Note: can't use {@link String#trim} because {@link String#trim} has a + * different definition of whitespace than we want. + * @param value the string to trim + * @return the trimmed string + */ + private String trimWhitespace(String value) { + int start = 0; + int last = value.length() - 1; + int end = last; + while (start <= end && isWhitespace(value.charAt(start))) { + start++; + } + while (end >= start && isWhitespace(value.charAt(end))) { + end--; + } + if (start == 0 && end == last) { + return value; + } + return value.substring(start, end + 1); + } + + /** + * Check if c is whitespace. + * @param c character to test + * @return true if c is a whitespace character + */ + private boolean isWhitespace(char c) { + switch(c) { + case ' ': + case '\t': + case '\f': + case '\n': + case '\r': + case 11: /* VT */ + return true; + default: + return false; + } + } + + /** + * Check whether an individual character is legal. Uses the + * flag bit-set passed into the constructor. + * @param c + * @return true if c is a legal character + */ + private boolean characterIsLegal(char c) { + switch(c) { + case ' ' : return (mFlags & SPACE_OK) != 0; + case '\t': case '\f': case '\n': case '\r': case 11: /* VT */ + return (mFlags & OTHER_WHITESPACE_OK) != 0; + case '\"': return (mFlags & DQUOTE_OK) != 0; + case '\'': return (mFlags & SQUOTE_OK) != 0; + case '<' : return (mFlags & LT_OK) != 0; + case '>' : return (mFlags & GT_OK) != 0; + case '&' : return (mFlags & AMP_OK) != 0; + case '%' : return (mFlags & PCT_OK) != 0; + case '\0': return (mFlags & NUL_OK) != 0; + default : return (c >= 32 && c < 127) || + (c >= 128 && c <= 255 && ((mFlags & NON_7_BIT_ASCII_OK) != 0)); + } + } + } + + /** + * Get the current value sanitizer used when processing + * unregistered parameter values. + * <p> + * <b>Note:</b> The default unregistered parameter value sanitizer is + * one that doesn't allow any special characters, similar to what + * is returned by calling createAllIllegal. + * + * @return the current ValueSanitizer used to sanitize unregistered + * parameter values. + */ + public ValueSanitizer getUnregisteredParameterValueSanitizer() { + return mUnregisteredParameterValueSanitizer; + } + + /** + * Set the value sanitizer used when processing unregistered + * parameter values. + * @param sanitizer set the ValueSanitizer used to sanitize unregistered + * parameter values. + */ + public void setUnregisteredParameterValueSanitizer( + ValueSanitizer sanitizer) { + mUnregisteredParameterValueSanitizer = sanitizer; + } + + + // Private fields for singleton sanitizers: + + private static final ValueSanitizer sAllIllegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.ALL_ILLEGAL); + + private static final ValueSanitizer sAllButNulLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.ALL_BUT_NUL_LEGAL); + + private static final ValueSanitizer sAllButWhitespaceLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.ALL_BUT_WHITESPACE_LEGAL); + + private static final ValueSanitizer sURLLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.URL_LEGAL); + + private static final ValueSanitizer sUrlAndSpaceLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.URL_AND_SPACE_LEGAL); + + private static final ValueSanitizer sAmpLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.AMP_LEGAL); + + private static final ValueSanitizer sAmpAndSpaceLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.AMP_AND_SPACE_LEGAL); + + private static final ValueSanitizer sSpaceLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.SPACE_LEGAL); + + private static final ValueSanitizer sAllButNulAndAngleBracketsLegal = + new IllegalCharacterValueSanitizer( + IllegalCharacterValueSanitizer.ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL); + + /** + * Return a value sanitizer that does not allow any special characters, + * and also does not allow script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getAllIllegal() { + return sAllIllegal; + } + + /** + * Return a value sanitizer that allows everything except Nul ('\0') + * characters. Script URLs are allowed. + * @return a value sanitizer + */ + public static final ValueSanitizer getAllButNulLegal() { + return sAllButNulLegal; + } + /** + * Return a value sanitizer that allows everything except Nul ('\0') + * characters, space (' '), and other whitespace characters. + * Script URLs are allowed. + * @return a value sanitizer + */ + public static final ValueSanitizer getAllButWhitespaceLegal() { + return sAllButWhitespaceLegal; + } + /** + * Return a value sanitizer that allows all the characters used by + * encoded URLs. Does not allow script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getUrlLegal() { + return sURLLegal; + } + /** + * Return a value sanitizer that allows all the characters used by + * encoded URLs and allows spaces, which are not technically legal + * in encoded URLs, but commonly appear anyway. + * Does not allow script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getUrlAndSpaceLegal() { + return sUrlAndSpaceLegal; + } + /** + * Return a value sanitizer that does not allow any special characters + * except ampersand ('&'). Does not allow script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getAmpLegal() { + return sAmpLegal; + } + /** + * Return a value sanitizer that does not allow any special characters + * except ampersand ('&') and space (' '). Does not allow script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getAmpAndSpaceLegal() { + return sAmpAndSpaceLegal; + } + /** + * Return a value sanitizer that does not allow any special characters + * except space (' '). Does not allow script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getSpaceLegal() { + return sSpaceLegal; + } + /** + * Return a value sanitizer that allows any special characters + * except angle brackets ('<' and '>') and Nul ('\0'). + * Allows script URLs. + * @return a value sanitizer + */ + public static final ValueSanitizer getAllButNulAndAngleBracketsLegal() { + return sAllButNulAndAngleBracketsLegal; + } + + /** + * Constructs a UrlQuerySanitizer. + * <p> + * Defaults: + * <ul> + * <li>unregistered parameters are not allowed. + * <li>the last instance of a repeated parameter is preferred. + * <li>The default value sanitizer is an AllIllegal value sanitizer. + * <ul> + */ + public UrlQuerySanitizer() { + } + + /** + * Constructs a UrlQuerySanitizer and parse a URL. + * This constructor is provided for convenience when the + * default parsing behavior is acceptable. + * <p> + * Because the URL is parsed before the constructor returns, there isn't + * a chance to configure the sanitizer to change the parsing behavior. + * <p> + * <code> + * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(myUrl); + * String name = sanitizer.getValue("name"); + * </code> + * <p> + * Defaults: + * <ul> + * <li>unregistered parameters <em>are</em> allowed. + * <li>the last instance of a repeated parameter is preferred. + * <li>The default value sanitizer is an AllIllegal value sanitizer. + * <ul> + */ + public UrlQuerySanitizer(String url) { + setAllowUnregisteredParamaters(true); + parseUrl(url); + } + + /** + * Parse the query parameters out of an encoded URL. + * Works by extracting the query portion from the URL and then + * calling parseQuery(). If there is no query portion it is + * treated as if the query portion is an empty string. + * @param url the encoded URL to parse. + */ + public void parseUrl(String url) { + int queryIndex = url.indexOf('?'); + String query; + if (queryIndex >= 0) { + query = url.substring(queryIndex + 1); + } + else { + query = ""; + } + parseQuery(query); + } + + /** + * Parse a query. A query string is any number of parameter-value clauses + * separated by any non-zero number of ampersands. A parameter-value clause + * is a parameter followed by an equal sign, followed by a value. If the + * equal sign is missing, the value is assumed to be the empty string. + * @param query the query to parse. + */ + public void parseQuery(String query) { + clear(); + // Split by '&' + StringTokenizer tokenizer = new StringTokenizer(query, "&"); + while(tokenizer.hasMoreElements()) { + String attributeValuePair = tokenizer.nextToken(); + if (attributeValuePair.length() > 0) { + int assignmentIndex = attributeValuePair.indexOf('='); + if (assignmentIndex < 0) { + // No assignment found, treat as if empty value + parseEntry(attributeValuePair, ""); + } + else { + parseEntry(attributeValuePair.substring(0, assignmentIndex), + attributeValuePair.substring(assignmentIndex + 1)); + } + } + } + } + + /** + * Get a set of all of the parameters found in the sanitized query. + * <p> + * Note: Do not modify this set. Treat it as a read-only set. + * @return all the parameters found in the current query. + */ + public Set<String> getParameterSet() { + return mEntries.keySet(); + } + + /** + * An array list of all of the parameter value pairs in the sanitized + * query, in the order they appeared in the query. May contain duplicate + * parameters. + * <p class="note"><b>Note:</b> Do not modify this list. Treat it as a read-only list.</p> + */ + public List<ParameterValuePair> getParameterList() { + return mEntriesList; + } + + /** + * Check if a parameter exists in the current sanitized query. + * @param parameter the unencoded name of a parameter. + * @return true if the paramater exists in the current sanitized queary. + */ + public boolean hasParameter(String parameter) { + return mEntries.containsKey(parameter); + } + + /** + * Get the value for a parameter in the current sanitized query. + * Returns null if the parameter does not + * exit. + * @param parameter the unencoded name of a parameter. + * @return the sanitized unencoded value of the parameter, + * or null if the parameter does not exist. + */ + public String getValue(String parameter) { + return mEntries.get(parameter); + } + + /** + * Register a value sanitizer for a particular parameter. Can also be used + * to replace or remove an already-set value sanitizer. + * <p> + * Registering a non-null value sanitizer for a particular parameter + * makes that parameter a registered parameter. + * @param parameter an unencoded parameter name + * @param valueSanitizer the value sanitizer to use for a particular + * parameter. May be null in order to unregister that parameter. + * @see #getAllowUnregisteredParamaters() + */ + public void registerParameter(String parameter, + ValueSanitizer valueSanitizer) { + if (valueSanitizer == null) { + mSanitizers.remove(parameter); + } + mSanitizers.put(parameter, valueSanitizer); + } + + /** + * Register a value sanitizer for an array of parameters. + * @param parameters An array of unencoded parameter names. + * @param valueSanitizer + * @see #registerParameter + */ + public void registerParameters(String[] parameters, + ValueSanitizer valueSanitizer) { + int length = parameters.length; + for(int i = 0; i < length; i++) { + mSanitizers.put(parameters[i], valueSanitizer); + } + } + + /** + * Set whether or not unregistered parameters are allowed. If they + * are not allowed, then they will be dropped when a query is sanitized. + * <p> + * Defaults to false. + * @param allowUnregisteredParamaters true to allow unregistered parameters. + * @see #getAllowUnregisteredParamaters() + */ + public void setAllowUnregisteredParamaters( + boolean allowUnregisteredParamaters) { + mAllowUnregisteredParamaters = allowUnregisteredParamaters; + } + + /** + * Get whether or not unregistered parameters are allowed. If not + * allowed, they will be dropped when a query is parsed. + * @return true if unregistered parameters are allowed. + * @see #setAllowUnregisteredParamaters(boolean) + */ + public boolean getAllowUnregisteredParamaters() { + return mAllowUnregisteredParamaters; + } + + /** + * Set whether or not the first occurrence of a repeated parameter is + * preferred. True means the first repeated parameter is preferred. + * False means that the last repeated parameter is preferred. + * <p> + * The preferred parameter is the one that is returned when getParameter + * is called. + * <p> + * defaults to false. + * @param preferFirstRepeatedParameter True if the first repeated + * parameter is preferred. + * @see #getPreferFirstRepeatedParameter() + */ + public void setPreferFirstRepeatedParameter( + boolean preferFirstRepeatedParameter) { + mPreferFirstRepeatedParameter = preferFirstRepeatedParameter; + } + + /** + * Get whether or not the first occurrence of a repeated parameter is + * preferred. + * @return true if the first occurrence of a repeated parameter is + * preferred. + * @see #setPreferFirstRepeatedParameter(boolean) + */ + public boolean getPreferFirstRepeatedParameter() { + return mPreferFirstRepeatedParameter; + } + + /** + * Parse an escaped parameter-value pair. The default implementation + * unescapes both the parameter and the value, then looks up the + * effective value sanitizer for the parameter and uses it to sanitize + * the value. If all goes well then addSanitizedValue is called with + * the unescaped parameter and the sanitized unescaped value. + * @param parameter an escaped parameter + * @param value an unsanitzied escaped value + */ + protected void parseEntry(String parameter, String value) { + String unescapedParameter = unescape(parameter); + ValueSanitizer valueSanitizer = + getEffectiveValueSanitizer(unescapedParameter); + + if (valueSanitizer == null) { + return; + } + String unescapedValue = unescape(value); + String sanitizedValue = valueSanitizer.sanitize(unescapedValue); + addSanitizedEntry(unescapedParameter, sanitizedValue); + } + + /** + * Record a sanitized parameter-value pair. Override if you want to + * do additional filtering or validation. + * @param parameter an unescaped parameter + * @param value a sanitized unescaped value + */ + protected void addSanitizedEntry(String parameter, String value) { + mEntriesList.add( + new ParameterValuePair(parameter, value)); + if (mPreferFirstRepeatedParameter) { + if (mEntries.containsKey(parameter)) { + return; + } + } + mEntries.put(parameter, value); + } + + /** + * Get the value sanitizer for a parameter. Returns null if there + * is no value sanitizer registered for the parameter. + * @param parameter the unescaped parameter + * @return the currently registered value sanitizer for this parameter. + * @see #registerParameter(String, android.net.UrlQuerySanitizer.ValueSanitizer) + */ + public ValueSanitizer getValueSanitizer(String parameter) { + return mSanitizers.get(parameter); + } + + /** + * Get the effective value sanitizer for a parameter. Like getValueSanitizer, + * except if there is no value sanitizer registered for a parameter, and + * unregistered paramaters are allowed, then the default value sanitizer is + * returned. + * @param parameter an unescaped parameter + * @return the effective value sanitizer for a parameter. + */ + public ValueSanitizer getEffectiveValueSanitizer(String parameter) { + ValueSanitizer sanitizer = getValueSanitizer(parameter); + if (sanitizer == null && mAllowUnregisteredParamaters) { + sanitizer = getUnregisteredParameterValueSanitizer(); + } + return sanitizer; + } + + /** + * Unescape an escaped string. + * <ul> + * <li>'+' characters are replaced by + * ' ' characters. + * <li>Valid "%xx" escape sequences are replaced by the + * corresponding unescaped character. + * <li>Invalid escape sequences such as %1z", are passed through unchanged. + * <ol> + * @param string the escaped string + * @return the unescaped string. + */ + public String unescape(String string) { + // Early exit if no escaped characters. + int firstEscape = string.indexOf('%'); + if ( firstEscape < 0) { + firstEscape = string.indexOf('+'); + if (firstEscape < 0) { + return string; + } + } + + int length = string.length(); + + StringBuilder stringBuilder = new StringBuilder(length); + stringBuilder.append(string.substring(0, firstEscape)); + for (int i = firstEscape; i < length; i++) { + char c = string.charAt(i); + if (c == '+') { + c = ' '; + } + else if ( c == '%' && i + 2 < length) { + char c1 = string.charAt(i + 1); + char c2 = string.charAt(i + 2); + if (isHexDigit(c1) && isHexDigit(c2)) { + c = (char) (decodeHexDigit(c1) * 16 + decodeHexDigit(c2)); + i += 2; + } + } + stringBuilder.append(c); + } + return stringBuilder.toString(); + } + + /** + * Test if a character is a hexidecimal digit. Both upper case and lower + * case hex digits are allowed. + * @param c the character to test + * @return true if c is a hex digit. + */ + protected boolean isHexDigit(char c) { + return decodeHexDigit(c) >= 0; + } + + /** + * Convert a character that represents a hexidecimal digit into an integer. + * If the character is not a hexidecimal digit, then -1 is returned. + * Both upper case and lower case hex digits are allowed. + * @param c the hexidecimal digit. + * @return the integer value of the hexidecimal digit. + */ + + protected int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + else if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + else { + return -1; + } + } + + /** + * Clear the existing entries. Called to get ready to parse a new + * query string. + */ + protected void clear() { + mEntries.clear(); + mEntriesList.clear(); + } +} + diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java new file mode 100644 index 0000000..f4a2a6a --- /dev/null +++ b/core/java/android/net/WebAddress.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2006 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 android.net; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * {@hide} + * + * Web Address Parser + * + * This is called WebAddress, rather than URL or URI, because it + * attempts to parse the stuff that a user will actually type into a + * browser address widget. + * + * Unlike java.net.uri, this parser will not choke on URIs missing + * schemes. It will only throw a ParseException if the input is + * really hosed. + * + * If given an https scheme but no port, fills in port + * + */ +public class WebAddress { + + private final static String LOGTAG = "http"; + + public String mScheme; + public String mHost; + public int mPort; + public String mPath; + public String mAuthInfo; + + static final int MATCH_GROUP_SCHEME = 1; + static final int MATCH_GROUP_AUTHORITY = 2; + static final int MATCH_GROUP_HOST = 3; + static final int MATCH_GROUP_PORT = 4; + static final int MATCH_GROUP_PATH = 5; + + static Pattern sAddressPattern = Pattern.compile( + /* scheme */ "(?:(http|HTTP|https|HTTPS|file|FILE)\\:\\/\\/)?" + + /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" + + /* host */ "([-A-Za-z0-9%]+(?:\\.[-A-Za-z0-9%]+)*)?" + + /* port */ "(?:\\:([0-9]+))?" + + /* path */ "(\\/?.*)?"); + + /** parses given uriString. */ + public WebAddress(String address) throws ParseException { + if (address == null) { + throw new NullPointerException(); + } + + // android.util.Log.d(LOGTAG, "WebAddress: " + address); + + mScheme = ""; + mHost = ""; + mPort = -1; + mPath = "/"; + mAuthInfo = ""; + + Matcher m = sAddressPattern.matcher(address); + String t; + if (m.matches()) { + t = m.group(MATCH_GROUP_SCHEME); + if (t != null) mScheme = t; + t = m.group(MATCH_GROUP_AUTHORITY); + if (t != null) mAuthInfo = t; + t = m.group(MATCH_GROUP_HOST); + if (t != null) mHost = t; + t = m.group(MATCH_GROUP_PORT); + if (t != null) { + try { + mPort = Integer.parseInt(t); + } catch (NumberFormatException ex) { + throw new ParseException("Bad port"); + } + } + t = m.group(MATCH_GROUP_PATH); + if (t != null && t.length() > 0) { + /* handle busted myspace frontpage redirect with + missing initial "/" */ + if (t.charAt(0) == '/') { + mPath = t; + } else { + mPath = "/" + t; + } + } + + } else { + // nothing found... outa here + throw new ParseException("Bad address"); + } + + /* Get port from scheme or scheme from port, if necessary and + possible */ + if (mPort == 443 && mScheme.equals("")) { + mScheme = "https"; + } else if (mPort == -1) { + if (mScheme.equals("https")) + mPort = 443; + else + mPort = 80; // default + } + if (mScheme.equals("")) mScheme = "http"; + } + + public String toString() { + String port = ""; + if ((mPort != 443 && mScheme.equals("https")) || + (mPort != 80 && mScheme.equals("http"))) { + port = ":" + Integer.toString(mPort); + } + String authInfo = ""; + if (mAuthInfo.length() > 0) { + authInfo = mAuthInfo + "@"; + } + + return mScheme + "://" + authInfo + mHost + port + mPath; + } +} diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java new file mode 100644 index 0000000..01442ae --- /dev/null +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -0,0 +1,452 @@ +/* + * 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 android.net.http; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.client.CookieStore; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.RequestWrapper; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.BasicHttpProcessor; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.BasicHttpContext; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; + +import android.util.Log; +import android.content.ContentResolver; +import android.provider.Settings; +import android.text.TextUtils; + +/** + * Subclass of the Apache {@link DefaultHttpClient} that is configured with + * reasonable default settings and registered schemes for Android, and + * also lets the user add {@link HttpRequestInterceptor} classes. + * Don't create this directly, use the {@link #newInstance} factory method. + * + * <p>This client processes cookies but does not retain them by default. + * To retain cookies, simply add a cookie store to the HttpContext:</p> + * + * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> + * + * {@hide} + */ +public final class AndroidHttpClient implements HttpClient { + + // Gzip of data shorter than this probably won't be worthwhile + public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; + + private static final String TAG = "AndroidHttpClient"; + + + /** Set if HTTP requests are blocked from being executed on this thread */ + private static final ThreadLocal<Boolean> sThreadBlocked = + new ThreadLocal<Boolean>(); + + /** Interceptor throws an exception if the executing thread is blocked */ + private static final HttpRequestInterceptor sThreadCheckInterceptor = + new HttpRequestInterceptor() { + public void process(HttpRequest request, HttpContext context) { + if (sThreadBlocked.get() != null && sThreadBlocked.get()) { + throw new RuntimeException("This thread forbids HTTP requests"); + } + } + }; + + /** + * Create a new HttpClient with reasonable defaults (which you can update). + * @param userAgent to report in your HTTP requests. + * @return AndroidHttpClient for you to use for all your requests. + */ + public static AndroidHttpClient newInstance(String userAgent) { + HttpParams params = new BasicHttpParams(); + + // Turn off stale checking. Our connections break all the time anyway, + // and it's not worth it to pay the penalty of checking every time. + HttpConnectionParams.setStaleCheckingEnabled(params, false); + + // Default connection and socket timeout of 20 seconds. Tweak to taste. + HttpConnectionParams.setConnectionTimeout(params, 20 * 1000); + HttpConnectionParams.setSoTimeout(params, 20 * 1000); + HttpConnectionParams.setSocketBufferSize(params, 8192); + + // Don't handle redirects -- return them to the caller. Our code + // often wants to re-POST after a redirect, which we must do ourselves. + HttpClientParams.setRedirecting(params, false); + + // Set the specified user agent and register standard protocols. + HttpProtocolParams.setUserAgent(params, userAgent); + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", + PlainSocketFactory.getSocketFactory(), 80)); + schemeRegistry.register(new Scheme("https", + SSLSocketFactory.getSocketFactory(), 443)); + ClientConnectionManager manager = + new ThreadSafeClientConnManager(params, schemeRegistry); + + // We use a factory method to modify superclass initialization + // parameters without the funny call-a-static-method dance. + return new AndroidHttpClient(manager, params); + } + + private final HttpClient delegate; + + private RuntimeException mLeakedException = new IllegalStateException( + "AndroidHttpClient created and never closed"); + + private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { + this.delegate = new DefaultHttpClient(ccm, params) { + @Override + protected BasicHttpProcessor createHttpProcessor() { + // Add interceptor to prevent making requests from main thread. + BasicHttpProcessor processor = super.createHttpProcessor(); + processor.addRequestInterceptor(sThreadCheckInterceptor); + processor.addRequestInterceptor(new CurlLogger()); + + return processor; + } + + @Override + protected HttpContext createHttpContext() { + // Same as DefaultHttpClient.createHttpContext() minus the + // cookie store. + HttpContext context = new BasicHttpContext(); + context.setAttribute( + ClientContext.AUTHSCHEME_REGISTRY, + getAuthSchemes()); + context.setAttribute( + ClientContext.COOKIESPEC_REGISTRY, + getCookieSpecs()); + context.setAttribute( + ClientContext.CREDS_PROVIDER, + getCredentialsProvider()); + return context; + } + }; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (mLeakedException != null) { + Log.e(TAG, "Leak found", mLeakedException); + mLeakedException = null; + } + } + + /** + * Block this thread from executing HTTP requests. + * Used to guard against HTTP requests blocking the main application thread. + * @param blocked if HTTP requests run on this thread should be denied + */ + public static void setThreadBlocked(boolean blocked) { + sThreadBlocked.set(blocked); + } + + /** + * Modifies a request to indicate to the server that we would like a + * gzipped response. (Uses the "Accept-Encoding" HTTP header.) + * @param request the request to modify + * @see #getUngzippedContent + */ + public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { + request.addHeader("Accept-Encoding", "gzip"); + } + + /** + * Gets the input stream from a response entity. If the entity is gzipped + * then this will get a stream over the uncompressed data. + * + * @param entity the entity whose content should be read + * @return the input stream to read from + * @throws IOException + */ + public static InputStream getUngzippedContent(HttpEntity entity) + throws IOException { + InputStream responseStream = entity.getContent(); + if (responseStream == null) return responseStream; + Header header = entity.getContentEncoding(); + if (header == null) return responseStream; + String contentEncoding = header.getValue(); + if (contentEncoding == null) return responseStream; + if (contentEncoding.contains("gzip")) responseStream + = new GZIPInputStream(responseStream); + return responseStream; + } + + /** + * Release resources associated with this client. You must call this, + * or significant resources (sockets and memory) may be leaked. + */ + public void close() { + if (mLeakedException != null) { + getConnectionManager().shutdown(); + mLeakedException = null; + } + } + + public HttpParams getParams() { + return delegate.getParams(); + } + + public ClientConnectionManager getConnectionManager() { + return delegate.getConnectionManager(); + } + + public HttpResponse execute(HttpUriRequest request) throws IOException { + return delegate.execute(request); + } + + public HttpResponse execute(HttpUriRequest request, HttpContext context) + throws IOException { + return delegate.execute(request, context); + } + + public HttpResponse execute(HttpHost target, HttpRequest request) + throws IOException { + return delegate.execute(target, request); + } + + public HttpResponse execute(HttpHost target, HttpRequest request, + HttpContext context) throws IOException { + return delegate.execute(target, request, context); + } + + public <T> T execute(HttpUriRequest request, + ResponseHandler<? extends T> responseHandler) + throws IOException, ClientProtocolException { + return delegate.execute(request, responseHandler); + } + + public <T> T execute(HttpUriRequest request, + ResponseHandler<? extends T> responseHandler, HttpContext context) + throws IOException, ClientProtocolException { + return delegate.execute(request, responseHandler, context); + } + + public <T> T execute(HttpHost target, HttpRequest request, + ResponseHandler<? extends T> responseHandler) throws IOException, + ClientProtocolException { + return delegate.execute(target, request, responseHandler); + } + + public <T> T execute(HttpHost target, HttpRequest request, + ResponseHandler<? extends T> responseHandler, HttpContext context) + throws IOException, ClientProtocolException { + return delegate.execute(target, request, responseHandler, context); + } + + /** + * Compress data to send to server. + * Creates a Http Entity holding the gzipped data. + * The data will not be compressed if it is too short. + * @param data The bytes to compress + * @return Entity holding the data + */ + public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver) + throws IOException { + AbstractHttpEntity entity; + if (data.length < getMinGzipSize(resolver)) { + entity = new ByteArrayEntity(data); + } else { + ByteArrayOutputStream arr = new ByteArrayOutputStream(); + OutputStream zipper = new GZIPOutputStream(arr); + zipper.write(data); + zipper.close(); + entity = new ByteArrayEntity(arr.toByteArray()); + entity.setContentEncoding("gzip"); + } + return entity; + } + + /** + * Retrieves the minimum size for compressing data. + * Shorter data will not be compressed. + */ + public static long getMinGzipSize(ContentResolver resolver) { + String sMinGzipBytes = Settings.Gservices.getString(resolver, + Settings.Gservices.SYNC_MIN_GZIP_BYTES); + + if (!TextUtils.isEmpty(sMinGzipBytes)) { + try { + return Long.parseLong(sMinGzipBytes); + } catch (NumberFormatException nfe) { + Log.w(TAG, "Unable to parse " + + Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " + + sMinGzipBytes, nfe); + } + } + return DEFAULT_SYNC_MIN_GZIP_BYTES; + } + + /* cURL logging support. */ + + /** + * Logging tag and level. + */ + private static class LoggingConfiguration { + + private final String tag; + private final int level; + + private LoggingConfiguration(String tag, int level) { + this.tag = tag; + this.level = level; + } + + /** + * Returns true if logging is turned on for this configuration. + */ + private boolean isLoggable() { + return Log.isLoggable(tag, level); + } + + /** + * Prints a message using this configuration. + */ + private void println(String message) { + Log.println(level, tag, message); + } + } + + /** cURL logging configuration. */ + private volatile LoggingConfiguration curlConfiguration; + + /** + * Enables cURL request logging for this client. + * + * @param name to log messages with + * @param level at which to log messages (see {@link android.util.Log}) + */ + public void enableCurlLogging(String name, int level) { + if (name == null) { + throw new NullPointerException("name"); + } + if (level < Log.VERBOSE || level > Log.ASSERT) { + throw new IllegalArgumentException("Level is out of range [" + + Log.VERBOSE + ".." + Log.ASSERT + "]"); + } + + curlConfiguration = new LoggingConfiguration(name, level); + } + + /** + * Disables cURL logging for this client. + */ + public void disableCurlLogging() { + curlConfiguration = null; + } + + /** + * Logs cURL commands equivalent to requests. + */ + private class CurlLogger implements HttpRequestInterceptor { + public void process(HttpRequest request, HttpContext context) + throws HttpException, IOException { + LoggingConfiguration configuration = curlConfiguration; + if (configuration != null + && configuration.isLoggable() + && request instanceof HttpUriRequest) { + configuration.println(toCurl((HttpUriRequest) request)); + } + } + } + + /** + * Generates a cURL command equivalent to the given request. + */ + private static String toCurl(HttpUriRequest request) throws IOException { + StringBuilder builder = new StringBuilder(); + + builder.append("curl "); + + for (Header header: request.getAllHeaders()) { + builder.append("--header \""); + builder.append(header.toString().trim()); + builder.append("\" "); + } + + URI uri = request.getURI(); + + // If this is a wrapped request, use the URI from the original + // request instead. getURI() on the wrapper seems to return a + // relative URI. We want an absolute URI. + if (request instanceof RequestWrapper) { + HttpRequest original = ((RequestWrapper) request).getOriginal(); + if (original instanceof HttpUriRequest) { + uri = ((HttpUriRequest) original).getURI(); + } + } + + builder.append("\""); + builder.append(uri); + builder.append("\""); + + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest entityRequest = + (HttpEntityEnclosingRequest) request; + HttpEntity entity = entityRequest.getEntity(); + if (entity != null && entity.isRepeatable()) { + if (entity.getContentLength() < 1024) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + entity.writeTo(stream); + String entityString = stream.toString(); + + // TODO: Check the content type, too. + builder.append(" --data-ascii \"") + .append(entityString) + .append("\""); + } else { + builder.append(" [TOO MUCH DATA TO INCLUDE]"); + } + } + } + + return builder.toString(); + } +} diff --git a/core/java/android/net/http/AndroidHttpClientConnection.java b/core/java/android/net/http/AndroidHttpClientConnection.java new file mode 100644 index 0000000..eb96679 --- /dev/null +++ b/core/java/android/net/http/AndroidHttpClientConnection.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 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. + * 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 android.net.http; + +import org.apache.http.Header; + +import org.apache.http.HttpConnection; +import org.apache.http.HttpClientConnection; +import org.apache.http.HttpConnectionMetrics; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpInetConnection; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseFactory; +import org.apache.http.NoHttpResponseException; +import org.apache.http.StatusLine; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.ContentLengthStrategy; +import org.apache.http.impl.DefaultHttpResponseFactory; +import org.apache.http.impl.HttpConnectionMetricsImpl; +import org.apache.http.impl.entity.EntitySerializer; +import org.apache.http.impl.entity.StrictContentLengthStrategy; +import org.apache.http.impl.io.ChunkedInputStream; +import org.apache.http.impl.io.ContentLengthInputStream; +import org.apache.http.impl.io.HttpRequestWriter; +import org.apache.http.impl.io.IdentityInputStream; +import org.apache.http.impl.io.SocketInputBuffer; +import org.apache.http.impl.io.SocketOutputBuffer; +import org.apache.http.io.HttpMessageWriter; +import org.apache.http.io.SessionInputBuffer; +import org.apache.http.io.SessionOutputBuffer; +import org.apache.http.message.BasicLineParser; +import org.apache.http.message.ParserCursor; +import org.apache.http.params.CoreConnectionPNames; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.ParseException; +import org.apache.http.util.CharArrayBuffer; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; + +/** + * A alternate class for (@link DefaultHttpClientConnection). + * It has better performance than DefaultHttpClientConnection + * + * {@hide} + */ +public class AndroidHttpClientConnection + implements HttpInetConnection, HttpConnection { + + private SessionInputBuffer inbuffer = null; + private SessionOutputBuffer outbuffer = null; + private int maxHeaderCount; + // store CoreConnectionPNames.MAX_LINE_LENGTH for performance + private int maxLineLength; + + private final EntitySerializer entityserializer; + + private HttpMessageWriter requestWriter = null; + private HttpConnectionMetricsImpl metrics = null; + private volatile boolean open; + private Socket socket = null; + + public AndroidHttpClientConnection() { + this.entityserializer = new EntitySerializer( + new StrictContentLengthStrategy()); + } + + /** + * Bind socket and set HttpParams to AndroidHttpClientConnection + * @param socket outgoing socket + * @param params HttpParams + * @throws IOException + */ + public void bind( + final Socket socket, + final HttpParams params) throws IOException { + if (socket == null) { + throw new IllegalArgumentException("Socket may not be null"); + } + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + assertNotOpen(); + socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params)); + socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params)); + + int linger = HttpConnectionParams.getLinger(params); + if (linger >= 0) { + socket.setSoLinger(linger > 0, linger); + } + this.socket = socket; + + int buffersize = HttpConnectionParams.getSocketBufferSize(params); + this.inbuffer = new SocketInputBuffer(socket, buffersize, params); + this.outbuffer = new SocketOutputBuffer(socket, buffersize, params); + + maxHeaderCount = params.getIntParameter( + CoreConnectionPNames.MAX_HEADER_COUNT, -1); + maxLineLength = params.getIntParameter( + CoreConnectionPNames.MAX_LINE_LENGTH, -1); + + this.requestWriter = new HttpRequestWriter(outbuffer, null, params); + + this.metrics = new HttpConnectionMetricsImpl( + inbuffer.getMetrics(), + outbuffer.getMetrics()); + + this.open = true; + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append(getClass().getSimpleName()).append("["); + if (isOpen()) { + buffer.append(getRemotePort()); + } else { + buffer.append("closed"); + } + buffer.append("]"); + return buffer.toString(); + } + + + private void assertNotOpen() { + if (this.open) { + throw new IllegalStateException("Connection is already open"); + } + } + + private void assertOpen() { + if (!this.open) { + throw new IllegalStateException("Connection is not open"); + } + } + + public boolean isOpen() { + // to make this method useful, we want to check if the socket is connected + return (this.open && this.socket != null && this.socket.isConnected()); + } + + public InetAddress getLocalAddress() { + if (this.socket != null) { + return this.socket.getLocalAddress(); + } else { + return null; + } + } + + public int getLocalPort() { + if (this.socket != null) { + return this.socket.getLocalPort(); + } else { + return -1; + } + } + + public InetAddress getRemoteAddress() { + if (this.socket != null) { + return this.socket.getInetAddress(); + } else { + return null; + } + } + + public int getRemotePort() { + if (this.socket != null) { + return this.socket.getPort(); + } else { + return -1; + } + } + + public void setSocketTimeout(int timeout) { + assertOpen(); + if (this.socket != null) { + try { + this.socket.setSoTimeout(timeout); + } catch (SocketException ignore) { + // It is not quite clear from the original documentation if there are any + // other legitimate cases for a socket exception to be thrown when setting + // SO_TIMEOUT besides the socket being already closed + } + } + } + + public int getSocketTimeout() { + if (this.socket != null) { + try { + return this.socket.getSoTimeout(); + } catch (SocketException ignore) { + return -1; + } + } else { + return -1; + } + } + + public void shutdown() throws IOException { + this.open = false; + Socket tmpsocket = this.socket; + if (tmpsocket != null) { + tmpsocket.close(); + } + } + + public void close() throws IOException { + if (!this.open) { + return; + } + this.open = false; + doFlush(); + try { + try { + this.socket.shutdownOutput(); + } catch (IOException ignore) { + } + try { + this.socket.shutdownInput(); + } catch (IOException ignore) { + } + } catch (UnsupportedOperationException ignore) { + // if one isn't supported, the other one isn't either + } + this.socket.close(); + } + + /** + * Sends the request line and all headers over the connection. + * @param request the request whose headers to send. + * @throws HttpException + * @throws IOException + */ + public void sendRequestHeader(final HttpRequest request) + throws HttpException, IOException { + if (request == null) { + throw new IllegalArgumentException("HTTP request may not be null"); + } + assertOpen(); + this.requestWriter.write(request); + this.metrics.incrementRequestCount(); + } + + /** + * Sends the request entity over the connection. + * @param request the request whose entity to send. + * @throws HttpException + * @throws IOException + */ + public void sendRequestEntity(final HttpEntityEnclosingRequest request) + throws HttpException, IOException { + if (request == null) { + throw new IllegalArgumentException("HTTP request may not be null"); + } + assertOpen(); + if (request.getEntity() == null) { + return; + } + this.entityserializer.serialize( + this.outbuffer, + request, + request.getEntity()); + } + + protected void doFlush() throws IOException { + this.outbuffer.flush(); + } + + public void flush() throws IOException { + assertOpen(); + doFlush(); + } + + /** + * Parses the response headers and adds them to the + * given {@code headers} object, and returns the response StatusLine + * @param headers store parsed header to headers. + * @throws IOException + * @return StatusLine + * @see HttpClientConnection#receiveResponseHeader() + */ + public StatusLine parseResponseHeader(Headers headers) + throws IOException, ParseException { + assertOpen(); + + CharArrayBuffer current = new CharArrayBuffer(64); + + if (inbuffer.readLine(current) == -1) { + throw new NoHttpResponseException("The target server failed to respond"); + } + + // Create the status line from the status string + StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine( + current, new ParserCursor(0, current.length())); + + if (HttpLog.LOGV) HttpLog.v("read: " + statusline); + int statusCode = statusline.getStatusCode(); + + // Parse header body + CharArrayBuffer previous = null; + int headerNumber = 0; + while(true) { + if (current == null) { + current = new CharArrayBuffer(64); + } else { + // This must be he buffer used to parse the status + current.clear(); + } + int l = inbuffer.readLine(current); + if (l == -1 || current.length() < 1) { + break; + } + // Parse the header name and value + // Check for folded headers first + // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 + // discussion on folded headers + char first = current.charAt(0); + if ((first == ' ' || first == '\t') && previous != null) { + // we have continuation folded header + // so append value + int start = 0; + int length = current.length(); + while (start < length) { + char ch = current.charAt(start); + if (ch != ' ' && ch != '\t') { + break; + } + start++; + } + if (maxLineLength > 0 && + previous.length() + 1 + current.length() - start > + maxLineLength) { + throw new IOException("Maximum line length limit exceeded"); + } + previous.append(' '); + previous.append(current, start, current.length() - start); + } else { + if (previous != null) { + headers.parseHeader(previous); + } + headerNumber++; + previous = current; + current = null; + } + if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) { + throw new IOException("Maximum header count exceeded"); + } + } + + if (previous != null) { + headers.parseHeader(previous); + } + + if (statusCode >= 200) { + this.metrics.incrementResponseCount(); + } + return statusline; + } + + /** + * Return the next response entity. + * @param headers contains values for parsing entity + * @see HttpClientConnection#receiveResponseEntity(HttpResponse response) + */ + public HttpEntity receiveResponseEntity(final Headers headers) { + assertOpen(); + BasicHttpEntity entity = new BasicHttpEntity(); + + long len = determineLength(headers); + if (len == ContentLengthStrategy.CHUNKED) { + entity.setChunked(true); + entity.setContentLength(-1); + entity.setContent(new ChunkedInputStream(inbuffer)); + } else if (len == ContentLengthStrategy.IDENTITY) { + entity.setChunked(false); + entity.setContentLength(-1); + entity.setContent(new IdentityInputStream(inbuffer)); + } else { + entity.setChunked(false); + entity.setContentLength(len); + entity.setContent(new ContentLengthInputStream(inbuffer, len)); + } + + String contentTypeHeader = headers.getContentType(); + if (contentTypeHeader != null) { + entity.setContentType(contentTypeHeader); + } + String contentEncodingHeader = headers.getContentEncoding(); + if (contentEncodingHeader != null) { + entity.setContentEncoding(contentEncodingHeader); + } + + return entity; + } + + private long determineLength(final Headers headers) { + long transferEncoding = headers.getTransferEncoding(); + // We use Transfer-Encoding if present and ignore Content-Length. + // RFC2616, 4.4 item number 3 + if (transferEncoding < Headers.NO_TRANSFER_ENCODING) { + return transferEncoding; + } else { + long contentlen = headers.getContentLength(); + if (contentlen > Headers.NO_CONTENT_LENGTH) { + return contentlen; + } else { + return ContentLengthStrategy.IDENTITY; + } + } + } + + /** + * Checks whether this connection has gone down. + * Network connections may get closed during some time of inactivity + * for several reasons. The next time a read is attempted on such a + * connection it will throw an IOException. + * This method tries to alleviate this inconvenience by trying to + * find out if a connection is still usable. Implementations may do + * that by attempting a read with a very small timeout. Thus this + * method may block for a small amount of time before returning a result. + * It is therefore an <i>expensive</i> operation. + * + * @return <code>true</code> if attempts to use this connection are + * likely to succeed, or <code>false</code> if they are likely + * to fail and this connection should be closed + */ + public boolean isStale() { + assertOpen(); + try { + this.inbuffer.isDataAvailable(1); + return false; + } catch (IOException ex) { + return true; + } + } + + /** + * Returns a collection of connection metrcis + * @return HttpConnectionMetrics + */ + public HttpConnectionMetrics getMetrics() { + return this.metrics; + } +} diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java new file mode 100644 index 0000000..b7f7368 --- /dev/null +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -0,0 +1,444 @@ +/* + * Copyright (C) 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. + * 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 android.net.http; + +import android.os.SystemClock; + +import java.io.IOException; + +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +import java.util.Arrays; +import java.util.Date; +import java.util.Enumeration; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.HttpHost; + +import org.bouncycastle.asn1.x509.X509Name; + +/** + * Class responsible for all server certificate validation functionality + * + * {@hide} + */ +class CertificateChainValidator { + + private static long sTotal = 0; + private static long sTotalReused = 0; + + /** + * The singleton instance of the certificate chain validator + */ + private static CertificateChainValidator sInstance; + + /** + * Default trust manager (used to perform CA certificate validation) + */ + private X509TrustManager mDefaultTrustManager; + + /** + * @return The singleton instance of the certificator chain validator + */ + public static CertificateChainValidator getInstance() { + if (sInstance == null) { + sInstance = new CertificateChainValidator(); + } + + return sInstance; + } + + /** + * Creates a new certificate chain validator. This is a pivate constructor. + * If you need a Certificate chain validator, call getInstance(). + */ + private CertificateChainValidator() { + try { + TrustManagerFactory trustManagerFactory + = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init((KeyStore)null); + TrustManager[] trustManagers = + trustManagerFactory.getTrustManagers(); + if (trustManagers != null && trustManagers.length > 0) { + for (TrustManager trustManager : trustManagers) { + if (trustManager instanceof X509TrustManager) { + mDefaultTrustManager = (X509TrustManager)(trustManager); + break; + } + } + } + } catch (Exception exc) { + if (HttpLog.LOGV) { + HttpLog.v("CertificateChainValidator():" + + " failed to initialize the trust manager"); + } + } + } + + /** + * Performs the handshake and server certificates validation + * @param sslSocket The secure connection socket + * @param domain The website domain + * @return An SSL error object if there is an error and null otherwise + */ + public SslError doHandshakeAndValidateServerCertificates( + HttpsConnection connection, SSLSocket sslSocket, String domain) + throws SSLHandshakeException, IOException { + + ++sTotal; + + SSLContext sslContext = HttpsConnection.getContext(); + if (sslContext == null) { + closeSocketThrowException(sslSocket, "SSL context is null"); + } + + X509Certificate[] serverCertificates = null; + + long sessionBeforeHandshakeLastAccessedTime = 0; + byte[] sessionBeforeHandshakeId = null; + + SSLSession sessionAfterHandshake = null; + + synchronized(sslContext) { + // get SSL session before the handshake + SSLSession sessionBeforeHandshake = + getSSLSession(sslContext, connection.getHost()); + if (sessionBeforeHandshake != null) { + sessionBeforeHandshakeLastAccessedTime = + sessionBeforeHandshake.getLastAccessedTime(); + + sessionBeforeHandshakeId = + sessionBeforeHandshake.getId(); + } + + // start handshake, close the socket if we fail + try { + sslSocket.setUseClientMode(true); + sslSocket.startHandshake(); + } catch (IOException e) { + closeSocketThrowException( + sslSocket, e.getMessage(), + "failed to perform SSL handshake"); + } + + // retrieve the chain of the server peer certificates + Certificate[] peerCertificates = + sslSocket.getSession().getPeerCertificates(); + + if (peerCertificates == null || peerCertificates.length <= 0) { + closeSocketThrowException( + sslSocket, "failed to retrieve peer certificates"); + } else { + serverCertificates = + new X509Certificate[peerCertificates.length]; + for (int i = 0; i < peerCertificates.length; ++i) { + serverCertificates[i] = + (X509Certificate)(peerCertificates[i]); + } + + // update the SSL certificate associated with the connection + if (connection != null) { + if (serverCertificates[0] != null) { + connection.setCertificate( + new SslCertificate(serverCertificates[0])); + } + } + } + + // get SSL session after the handshake + sessionAfterHandshake = + getSSLSession(sslContext, connection.getHost()); + } + + if (sessionBeforeHandshakeLastAccessedTime != 0 && + sessionAfterHandshake != null && + Arrays.equals( + sessionBeforeHandshakeId, sessionAfterHandshake.getId()) && + sessionBeforeHandshakeLastAccessedTime < + sessionAfterHandshake.getLastAccessedTime()) { + + if (HttpLog.LOGV) { + HttpLog.v("SSL session was reused: total reused: " + + sTotalReused + + " out of total of: " + sTotal); + + ++sTotalReused; + } + + // no errors!!! + return null; + } + + // check if the first certificate in the chain is for this site + X509Certificate currCertificate = serverCertificates[0]; + if (currCertificate == null) { + closeSocketThrowException( + sslSocket, "certificate for this site is null"); + } else { + if (!DomainNameChecker.match(currCertificate, domain)) { + String errorMessage = "certificate not for this host: " + domain; + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + sslSocket.getSession().invalidate(); + return new SslError( + SslError.SSL_IDMISMATCH, currCertificate); + } + } + + // + // first, we validate the chain using the standard validation + // solution; if we do not find any errors, we are done; if we + // fail the standard validation, we re-validate again below, + // this time trying to retrieve any individual errors we can + // report back to the user. + // + try { + synchronized (mDefaultTrustManager) { + mDefaultTrustManager.checkServerTrusted( + serverCertificates, "RSA"); + + // no errors!!! + return null; + } + } catch (CertificateException e) { + if (HttpLog.LOGV) { + HttpLog.v( + "failed to pre-validate the certificate chain, error: " + + e.getMessage()); + } + } + + sslSocket.getSession().invalidate(); + + SslError error = null; + + // we check the root certificate separately from the rest of the + // chain; this is because we need to know what certificate in + // the chain resulted in an error if any + currCertificate = + serverCertificates[serverCertificates.length - 1]; + if (currCertificate == null) { + closeSocketThrowException( + sslSocket, "root certificate is null"); + } + + // check if the last certificate in the chain (root) is trusted + X509Certificate[] rootCertificateChain = { currCertificate }; + try { + synchronized (mDefaultTrustManager) { + mDefaultTrustManager.checkServerTrusted( + rootCertificateChain, "RSA"); + } + } catch (CertificateExpiredException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "root certificate has expired"; + } + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + error = new SslError( + SslError.SSL_EXPIRED, currCertificate); + } catch (CertificateNotYetValidException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "root certificate not valid yet"; + } + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + error = new SslError( + SslError.SSL_NOTYETVALID, currCertificate); + } catch (CertificateException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "root certificate not trusted"; + } + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + return new SslError( + SslError.SSL_UNTRUSTED, currCertificate); + } + + // Then go through the certificate chain checking that each + // certificate trusts the next and that each certificate is + // within its valid date range. Walk the chain in the order + // from the CA to the end-user + X509Certificate prevCertificate = + serverCertificates[serverCertificates.length - 1]; + + for (int i = serverCertificates.length - 2; i >= 0; --i) { + currCertificate = serverCertificates[i]; + + // if a certificate is null, we cannot verify the chain + if (currCertificate == null) { + closeSocketThrowException( + sslSocket, "null certificate in the chain"); + } + + // verify if trusted by chain + if (!prevCertificate.getSubjectDN().equals( + currCertificate.getIssuerDN())) { + String errorMessage = "not trusted by chain"; + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + return new SslError( + SslError.SSL_UNTRUSTED, currCertificate); + } + + try { + currCertificate.verify(prevCertificate.getPublicKey()); + } catch (GeneralSecurityException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "not trusted by chain"; + } + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + return new SslError( + SslError.SSL_UNTRUSTED, currCertificate); + } + + // verify if the dates are valid + try { + currCertificate.checkValidity(); + } catch (CertificateExpiredException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "certificate expired"; + } + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + if (error == null || + error.getPrimaryError() < SslError.SSL_EXPIRED) { + error = new SslError( + SslError.SSL_EXPIRED, currCertificate); + } + } catch (CertificateNotYetValidException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "certificate not valid yet"; + } + + if (HttpLog.LOGV) { + HttpLog.v(errorMessage); + } + + if (error == null || + error.getPrimaryError() < SslError.SSL_NOTYETVALID) { + error = new SslError( + SslError.SSL_NOTYETVALID, currCertificate); + } + } + + prevCertificate = currCertificate; + } + + // if we do not have an error to report back to the user, throw + // an exception (a generic error will be reported instead) + if (error == null) { + closeSocketThrowException( + sslSocket, + "failed to pre-validate the certificate chain due to a non-standard error"); + } + + return error; + } + + private void closeSocketThrowException( + SSLSocket socket, String errorMessage, String defaultErrorMessage) + throws SSLHandshakeException, IOException { + closeSocketThrowException( + socket, errorMessage != null ? errorMessage : defaultErrorMessage); + } + + private void closeSocketThrowException(SSLSocket socket, String errorMessage) + throws SSLHandshakeException, IOException { + if (HttpLog.LOGV) { + HttpLog.v("validation error: " + errorMessage); + } + + if (socket != null) { + SSLSession session = socket.getSession(); + if (session != null) { + session.invalidate(); + } + + socket.close(); + } + + throw new SSLHandshakeException(errorMessage); + } + + /** + * @param sslContext The SSL context shared accross all the SSL sessions + * @param host The host associated with the session + * @return A suitable SSL session from the SSL context + */ + private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) { + if (sslContext != null && host != null) { + Enumeration en = sslContext.getClientSessionContext().getIds(); + while (en.hasMoreElements()) { + byte[] id = (byte[]) en.nextElement(); + if (id != null) { + SSLSession session = + sslContext.getClientSessionContext().getSession(id); + if (session.isValid() && + host.getHostName().equals(session.getPeerHost()) && + host.getPort() == session.getPeerPort()) { + return session; + } + } + } + } + + return null; + } +} diff --git a/core/java/android/net/http/CertificateValidatorCache.java b/core/java/android/net/http/CertificateValidatorCache.java new file mode 100644 index 0000000..54a1dbe --- /dev/null +++ b/core/java/android/net/http/CertificateValidatorCache.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 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. + * 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 android.net.http; + +import android.os.SystemClock; + +import android.security.Sha1MessageDigest; + +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.CertPath; +import java.security.GeneralSecurityException; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Random; + + +/** + * Validator cache used to speed-up certificate chain validation. The idea is + * to keep each secure domain name associated with a cryptographically secure + * hash of the certificate chain successfully used to validate the domain. If + * we establish connection with the domain more than once and each time receive + * the same list of certificates, we do not have to re-validate. + * + * {@hide} + */ +class CertificateValidatorCache { + + // TODO: debug only! + public static long mSave = 0; + public static long mCost = 0; + // TODO: debug only! + + /** + * The cache-entry lifetime in milliseconds (here, 10 minutes) + */ + private static final long CACHE_ENTRY_LIFETIME = 10 * 60 * 1000; + + /** + * The certificate factory + */ + private static CertificateFactory sCertificateFactory; + + /** + * The certificate validator cache map (domain to a cache entry) + */ + private HashMap<Integer, CacheEntry> mCacheMap; + + /** + * Random salt + */ + private int mBigScrew; + + /** + * @param certificate The array of server certificates to compute a + * secure hash from + * @return The secure hash computed from server certificates + */ + public static byte[] secureHash(Certificate[] certificates) { + byte[] secureHash = null; + + // TODO: debug only! + long beg = SystemClock.uptimeMillis(); + // TODO: debug only! + + if (certificates != null && certificates.length != 0) { + byte[] encodedCertPath = null; + try { + synchronized (CertificateValidatorCache.class) { + if (sCertificateFactory == null) { + try { + sCertificateFactory = + CertificateFactory.getInstance("X.509"); + } catch(GeneralSecurityException e) { + if (HttpLog.LOGV) { + HttpLog.v("CertificateValidatorCache:" + + " failed to create the certificate factory"); + } + } + } + } + + CertPath certPath = + sCertificateFactory.generateCertPath(Arrays.asList(certificates)); + if (certPath != null) { + encodedCertPath = certPath.getEncoded(); + if (encodedCertPath != null) { + Sha1MessageDigest messageDigest = + new Sha1MessageDigest(); + secureHash = messageDigest.digest(encodedCertPath); + } + } + } catch (GeneralSecurityException e) {} + } + + // TODO: debug only! + long end = SystemClock.uptimeMillis(); + mCost += (end - beg); + // TODO: debug only! + + return secureHash; + } + + /** + * Creates a new certificate-validator cache + */ + public CertificateValidatorCache() { + Random random = new Random(); + mBigScrew = random.nextInt(); + + mCacheMap = new HashMap<Integer, CacheEntry>(); + } + + /** + * @param domain The domain to check against + * @param secureHash The secure hash to check against + * @return True iff there is a valid (not expired) cache entry + * associated with the domain and the secure hash + */ + public boolean has(String domain, byte[] secureHash) { + boolean rval = false; + + if (domain != null && domain.length() != 0) { + if (secureHash != null && secureHash.length != 0) { + CacheEntry cacheEntry = (CacheEntry)mCacheMap.get( + new Integer(mBigScrew ^ domain.hashCode())); + if (cacheEntry != null) { + if (!cacheEntry.expired()) { + rval = cacheEntry.has(domain, secureHash); + // TODO: debug only! + if (rval) { + mSave += cacheEntry.mSave; + } + // TODO: debug only! + } else { + mCacheMap.remove(cacheEntry); + } + } + } + } + + return rval; + } + + /** + * Adds the (domain, secureHash) tuple to the cache + * @param domain The domain to be added to the cache + * @param secureHash The secure hash to be added to the cache + * @return True iff succeeds + */ + public boolean put(String domain, byte[] secureHash, long save) { + if (domain != null && domain.length() != 0) { + if (secureHash != null && secureHash.length != 0) { + mCacheMap.put( + new Integer(mBigScrew ^ domain.hashCode()), + new CacheEntry(domain, secureHash, save)); + + return true; + } + } + + return false; + } + + /** + * Certificate-validator cache entry. We have one per domain + */ + private class CacheEntry { + + /** + * The hash associated with this cache entry + */ + private byte[] mHash; + + /** + * The time associated with this cache entry + */ + private long mTime; + + // TODO: debug only! + public long mSave; + // TODO: debug only! + + /** + * The host associated with this cache entry + */ + private String mDomain; + + /** + * Creates a new certificate-validator cache entry + * @param domain The domain to be associated with this cache entry + * @param secureHash The secure hash to be associated with this cache + * entry + */ + public CacheEntry(String domain, byte[] secureHash, long save) { + mDomain = domain; + mHash = secureHash; + // TODO: debug only! + mSave = save; + // TODO: debug only! + mTime = SystemClock.uptimeMillis(); + } + + /** + * @return True iff the cache item has expired + */ + public boolean expired() { + return CACHE_ENTRY_LIFETIME < SystemClock.uptimeMillis() - mTime; + } + + /** + * @param domain The domain to check + * @param secureHash The secure hash to check + * @return True iff the given domain and hash match those associated + * with this entry + */ + public boolean has(String domain, byte[] secureHash) { + if (domain != null && 0 < domain.length()) { + if (!mDomain.equals(domain)) { + return false; + } + } + + int hashLength = secureHash.length; + if (secureHash != null && 0 < hashLength) { + if (hashLength == mHash.length) { + for (int i = 0; i < hashLength; ++i) { + if (secureHash[i] != mHash[i]) { + return false; + } + } + return true; + } + } + + return false; + } + } +}; diff --git a/core/java/android/net/http/CharArrayBuffers.java b/core/java/android/net/http/CharArrayBuffers.java new file mode 100644 index 0000000..77d45f6 --- /dev/null +++ b/core/java/android/net/http/CharArrayBuffers.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 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. + * 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 android.net.http; + +import org.apache.http.util.CharArrayBuffer; +import org.apache.http.protocol.HTTP; + +/** + * Utility methods for working on CharArrayBuffers. + * + * {@hide} + */ +class CharArrayBuffers { + + static final char uppercaseAddon = 'a' - 'A'; + + /** + * Returns true if the buffer contains the given string. Ignores leading + * whitespace and case. + * + * @param buffer to search + * @param beginIndex index at which we should start + * @param str to search for + */ + static boolean containsIgnoreCaseTrimmed(CharArrayBuffer buffer, + int beginIndex, final String str) { + int len = buffer.length(); + char[] chars = buffer.buffer(); + while (beginIndex < len && HTTP.isWhitespace(chars[beginIndex])) { + beginIndex++; + } + int size = str.length(); + boolean ok = len >= beginIndex + size; + for (int j=0; ok && (j<size); j++) { + char a = chars[beginIndex+j]; + char b = str.charAt(j); + if (a != b) { + a = toLower(a); + b = toLower(b); + ok = a == b; + } + } + return ok; + } + + /** + * Returns index of first occurence ch. Lower cases characters leading up + * to first occurrence of ch. + */ + static int setLowercaseIndexOf(CharArrayBuffer buffer, final int ch) { + + int beginIndex = 0; + int endIndex = buffer.length(); + char[] chars = buffer.buffer(); + + for (int i = beginIndex; i < endIndex; i++) { + char current = chars[i]; + if (current == ch) { + return i; + } else if (current >= 'A' && current <= 'Z'){ + // make lower case + current += uppercaseAddon; + chars[i] = current; + } + } + return -1; + } + + private static char toLower(char c) { + if (c >= 'A' && c <= 'Z'){ + c += uppercaseAddon; + } + return c; + } +} diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java new file mode 100644 index 0000000..2c82582 --- /dev/null +++ b/core/java/android/net/http/Connection.java @@ -0,0 +1,523 @@ +/* + * 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 android.net.http; + +import android.content.Context; +import android.os.SystemClock; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.ListIterator; +import java.util.LinkedList; + +import javax.net.ssl.SSLHandshakeException; + +import org.apache.http.ConnectionReuseStrategy; +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpVersion; +import org.apache.http.ParseException; +import org.apache.http.ProtocolVersion; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.BasicHttpContext; + +/** + * {@hide} + */ +abstract class Connection { + + /** + * Allow a TCP connection 60 idle seconds before erroring out + */ + static final int SOCKET_TIMEOUT = 60000; + + private static final int SEND = 0; + private static final int READ = 1; + private static final int DRAIN = 2; + private static final int DONE = 3; + private static final String[] states = {"SEND", "READ", "DRAIN", "DONE"}; + + Context mContext; + + /** The low level connection */ + protected AndroidHttpClientConnection mHttpClientConnection = null; + + /** + * The server SSL certificate associated with this connection + * (null if the connection is not secure) + * It would be nice to store the whole certificate chain, but + * we want to keep things as light-weight as possible + */ + protected SslCertificate mCertificate = null; + + /** + * The host this connection is connected to. If using proxy, + * this is set to the proxy address + */ + HttpHost mHost; + + /** true if the connection can be reused for sending more requests */ + private boolean mCanPersist; + + /** context required by ConnectionReuseStrategy. */ + private HttpContext mHttpContext; + + /** set when cancelled */ + private static int STATE_NORMAL = 0; + private static int STATE_CANCEL_REQUESTED = 1; + private int mActive = STATE_NORMAL; + + /** The number of times to try to re-connect (if connect fails). */ + private final static int RETRY_REQUEST_LIMIT = 2; + + private static final int MIN_PIPE = 2; + private static final int MAX_PIPE = 3; + + /** + * Doesn't seem to exist anymore in the new HTTP client, so copied here. + */ + private static final String HTTP_CONNECTION = "http.connection"; + + RequestQueue.ConnectionManager mConnectionManager; + RequestFeeder mRequestFeeder; + + /** + * Buffer for feeding response blocks to webkit. One block per + * connection reduces memory churn. + */ + private byte[] mBuf; + + protected Connection(Context context, HttpHost host, + RequestQueue.ConnectionManager connectionManager, + RequestFeeder requestFeeder) { + mContext = context; + mHost = host; + mConnectionManager = connectionManager; + mRequestFeeder = requestFeeder; + + mCanPersist = false; + mHttpContext = new BasicHttpContext(null); + } + + HttpHost getHost() { + return mHost; + } + + /** + * connection factory: returns an HTTP or HTTPS connection as + * necessary + */ + static Connection getConnection( + Context context, HttpHost host, + RequestQueue.ConnectionManager connectionManager, + RequestFeeder requestFeeder) { + + if (host.getSchemeName().equals("http")) { + return new HttpConnection(context, host, connectionManager, + requestFeeder); + } + + // Otherwise, default to https + return new HttpsConnection(context, host, connectionManager, + requestFeeder); + } + + /** + * @return The server SSL certificate associated with this + * connection (null if the connection is not secure) + */ + /* package */ SslCertificate getCertificate() { + return mCertificate; + } + + /** + * Close current network connection + * Note: this runs in non-network thread + */ + void cancel() { + mActive = STATE_CANCEL_REQUESTED; + closeConnection(); + if (HttpLog.LOGV) HttpLog.v( + "Connection.cancel(): connection closed " + mHost); + } + + /** + * Process requests in queue + * pipelines requests + */ + void processRequests(Request firstRequest) { + Request req = null; + boolean empty; + int error = EventHandler.OK; + Exception exception = null; + + LinkedList<Request> pipe = new LinkedList<Request>(); + + int minPipe = MIN_PIPE, maxPipe = MAX_PIPE; + int state = SEND; + + while (state != DONE) { + if (HttpLog.LOGV) HttpLog.v( + states[state] + " pipe " + pipe.size()); + + /* If a request was cancelled, give other cancel requests + some time to go through so we don't uselessly restart + connections */ + if (mActive == STATE_CANCEL_REQUESTED) { + try { + Thread.sleep(100); + } catch (InterruptedException x) { /* ignore */ } + mActive = STATE_NORMAL; + } + + switch (state) { + case SEND: { + if (pipe.size() == maxPipe) { + state = READ; + break; + } + /* get a request */ + if (firstRequest == null) { + req = mRequestFeeder.getRequest(mHost); + } else { + req = firstRequest; + firstRequest = null; + } + if (req == null) { + state = DRAIN; + break; + } + req.setConnection(this); + + /* Don't work on cancelled requests. */ + if (req.mCancelled) { + if (HttpLog.LOGV) HttpLog.v( + "processRequests(): skipping cancelled request " + + req); + req.complete(); + break; + } + + if (mHttpClientConnection == null || + !mHttpClientConnection.isOpen()) { + /* If this call fails, the address is bad or + the net is down. Punt for now. + + FIXME: blow out entire queue here on + connection failure if net up? */ + + if (!openHttpConnection(req)) { + state = DONE; + break; + } + } + + try { + /* FIXME: don't increment failure count if old + connection? There should not be a penalty for + attempting to reuse an old connection */ + req.sendRequest(mHttpClientConnection); + } catch (HttpException e) { + exception = e; + error = EventHandler.ERROR; + } catch (IOException e) { + exception = e; + error = EventHandler.ERROR_IO; + } catch (IllegalStateException e) { + exception = e; + error = EventHandler.ERROR_IO; + } + if (exception != null) { + if (httpFailure(req, error, exception) && + !req.mCancelled) { + /* retry request if not permanent failure + or cancelled */ + pipe.addLast(req); + } + exception = null; + state = (clearPipe(pipe) || + !mConnectionManager.isNetworkConnected()) ? + DONE : SEND; + minPipe = maxPipe = 1; + break; + } + + pipe.addLast(req); + if (!mCanPersist) state = READ; + break; + + } + case DRAIN: + case READ: { + empty = !mRequestFeeder.haveRequest(mHost); + int pipeSize = pipe.size(); + if (state != DRAIN && pipeSize < minPipe && + !empty && mCanPersist) { + state = SEND; + break; + } else if (pipeSize == 0) { + /* Done if no other work to do */ + state = empty ? DONE : SEND; + break; + } + + req = (Request)pipe.removeFirst(); + if (HttpLog.LOGV) HttpLog.v( + "processRequests() reading " + req); + + try { + req.readResponse(mHttpClientConnection); + } catch (ParseException e) { + exception = e; + error = EventHandler.ERROR_IO; + } catch (IOException e) { + exception = e; + error = EventHandler.ERROR_IO; + } catch (IllegalStateException e) { + exception = e; + error = EventHandler.ERROR_IO; + } + if (exception != null) { + if (httpFailure(req, error, exception) && + !req.mCancelled) { + /* retry request if not permanent failure + or cancelled */ + req.reset(); + pipe.addFirst(req); + } + exception = null; + mCanPersist = false; + } + if (!mCanPersist) { + if (HttpLog.LOGV) HttpLog.v( + "processRequests(): no persist, closing " + + mHost); + + closeConnection(); + + mHttpContext.removeAttribute(HTTP_CONNECTION); + clearPipe(pipe); + minPipe = maxPipe = 1; + /* If network active continue to service this queue */ + state = mConnectionManager.isNetworkConnected() ? + SEND : DONE; + } + break; + } + } + } + } + + /** + * After a send/receive failure, any pipelined requests must be + * cleared back to the mRequest queue + * @return true if mRequests is empty after pipe cleared + */ + private boolean clearPipe(LinkedList<Request> pipe) { + boolean empty = true; + if (HttpLog.LOGV) HttpLog.v( + "Connection.clearPipe(): clearing pipe " + pipe.size()); + synchronized (mRequestFeeder) { + Request tReq; + while (!pipe.isEmpty()) { + tReq = (Request)pipe.removeLast(); + if (HttpLog.LOGV) HttpLog.v( + "clearPipe() adding back " + mHost + " " + tReq); + mRequestFeeder.requeueRequest(tReq); + empty = false; + } + if (empty) empty = mRequestFeeder.haveRequest(mHost); + } + return empty; + } + + /** + * @return true on success + */ + private boolean openHttpConnection(Request req) { + + long now = SystemClock.uptimeMillis(); + int error = EventHandler.OK; + Exception exception = null; + + try { + // reset the certificate to null before opening a connection + mCertificate = null; + mHttpClientConnection = openConnection(req); + if (mHttpClientConnection != null) { + mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT); + mHttpContext.setAttribute(HTTP_CONNECTION, + mHttpClientConnection); + } else { + // we tried to do SSL tunneling, failed, + // and need to drop the request; + // we have already informed the handler + req.mFailCount = RETRY_REQUEST_LIMIT; + return false; + } + } catch (UnknownHostException e) { + if (HttpLog.LOGV) HttpLog.v("Failed to open connection"); + error = EventHandler.ERROR_LOOKUP; + exception = e; + } catch (SSLConnectionClosedByUserException e) { + // hack: if we have an SSL connection failure, + // we don't want to reconnect + req.mFailCount = RETRY_REQUEST_LIMIT; + // no error message + return false; + } catch (SSLHandshakeException e) { + // hack: if we have an SSL connection failure, + // we don't want to reconnect + req.mFailCount = RETRY_REQUEST_LIMIT; + if (HttpLog.LOGV) HttpLog.v( + "SSL exception performing handshake"); + error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE; + exception = e; + } catch (IOException e) { + error = EventHandler.ERROR_CONNECT; + exception = e; + } + + if (HttpLog.LOGV) { + long now2 = SystemClock.uptimeMillis(); + HttpLog.v("Connection.openHttpConnection() " + + (now2 - now) + " " + mHost); + } + + if (error == EventHandler.OK) { + return true; + } else { + if (mConnectionManager.isNetworkConnected() == false || + req.mFailCount < RETRY_REQUEST_LIMIT) { + // requeue + mRequestFeeder.requeueRequest(req); + req.mFailCount++; + } else { + httpFailure(req, error, exception); + } + return error == EventHandler.OK; + } + } + + /** + * Helper. Calls the mEventHandler's error() method only if + * request failed permanently. Increments mFailcount on failure. + * + * Increments failcount only if the network is believed to be + * connected + * + * @return true if request can be retried (less than + * RETRY_REQUEST_LIMIT failures have occurred). + */ + private boolean httpFailure(Request req, int errorId, Exception e) { + boolean ret = true; + boolean networkConnected = mConnectionManager.isNetworkConnected(); + + // e.printStackTrace(); + if (HttpLog.LOGV) HttpLog.v( + "httpFailure() ******* " + e + " count " + req.mFailCount + + " networkConnected " + networkConnected + " " + mHost + " " + req.getUri()); + + if (networkConnected && ++req.mFailCount >= RETRY_REQUEST_LIMIT) { + ret = false; + String error; + if (errorId < 0) { + error = mContext.getText( + EventHandler.errorStringResources[-errorId]).toString(); + } else { + Throwable cause = e.getCause(); + error = cause != null ? cause.toString() : e.getMessage(); + } + req.mEventHandler.error(errorId, error); + req.complete(); + } + + closeConnection(); + mHttpContext.removeAttribute(HTTP_CONNECTION); + + return ret; + } + + HttpContext getHttpContext() { + return mHttpContext; + } + + /** + * Use same logic as ConnectionReuseStrategy + * @see ConnectionReuseStrategy + */ + private boolean keepAlive(HttpEntity entity, + ProtocolVersion ver, int connType, final HttpContext context) { + org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection) + context.getAttribute(ExecutionContext.HTTP_CONNECTION); + + if (conn != null && !conn.isOpen()) + return false; + // do NOT check for stale connection, that is an expensive operation + + if (entity != null) { + if (entity.getContentLength() < 0) { + if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) { + // if the content length is not known and is not chunk + // encoded, the connection cannot be reused + return false; + } + } + } + // Check for 'Connection' directive + if (connType == Headers.CONN_CLOSE) { + return false; + } else if (connType == Headers.CONN_KEEP_ALIVE) { + return true; + } + // Resorting to protocol version default close connection policy + return !ver.lessEquals(HttpVersion.HTTP_1_0); + } + + void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) { + mCanPersist = keepAlive(entity, ver, connType, mHttpContext); + } + + void setCanPersist(boolean canPersist) { + mCanPersist = canPersist; + } + + boolean getCanPersist() { + return mCanPersist; + } + + /** typically http or https... set by subclass */ + abstract String getScheme(); + abstract void closeConnection(); + abstract AndroidHttpClientConnection openConnection(Request req) throws IOException; + + /** + * Prints request queue to log, for debugging. + * returns request count + */ + public synchronized String toString() { + return mHost.toString(); + } + + byte[] getBuf() { + if (mBuf == null) mBuf = new byte[8192]; + return mBuf; + } + +} diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java new file mode 100644 index 0000000..8e759e2 --- /dev/null +++ b/core/java/android/net/http/ConnectionThread.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 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. + * 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 android.net.http; + +import android.content.Context; +import android.os.SystemClock; + +import org.apache.http.HttpHost; + +import java.lang.Thread; + +/** + * {@hide} + */ +class ConnectionThread extends Thread { + + static final int WAIT_TIMEOUT = 5000; + static final int WAIT_TICK = 1000; + + // Performance probe + long mStartThreadTime; + long mCurrentThreadTime; + + private boolean mWaiting; + private volatile boolean mRunning = true; + private Context mContext; + private RequestQueue.ConnectionManager mConnectionManager; + private RequestFeeder mRequestFeeder; + + private int mId; + Connection mConnection; + + ConnectionThread(Context context, + int id, + RequestQueue.ConnectionManager connectionManager, + RequestFeeder requestFeeder) { + super(); + mContext = context; + setName("http" + id); + mId = id; + mConnectionManager = connectionManager; + mRequestFeeder = requestFeeder; + } + + void requestStop() { + synchronized (mRequestFeeder) { + mRunning = false; + mRequestFeeder.notify(); + } + } + + /** + * Loop until app shutdown. Runs connections in priority + * order. + */ + public void run() { + android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE); + + mStartThreadTime = -1; + mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); + + while (mRunning) { + Request request; + + /* Get a request to process */ + request = mRequestFeeder.getRequest(); + + /* wait for work */ + if (request == null) { + synchronized(mRequestFeeder) { + if (HttpLog.LOGV) HttpLog.v("ConnectionThread: Waiting for work"); + mWaiting = true; + try { + if (mStartThreadTime != -1) { + mCurrentThreadTime = SystemClock + .currentThreadTimeMillis(); + } + mRequestFeeder.wait(); + } catch (InterruptedException e) { + } + mWaiting = false; + } + } else { + if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " + + request.mHost + " " + request ); + + HttpHost proxy = mConnectionManager.getProxyHost(); + + HttpHost host; + if (false) { + // Allow https proxy + host = proxy == null ? request.mHost : proxy; + } else { + // Disallow https proxy -- tmob proxy server + // serves a request loop for https reqs + host = (proxy == null || + request.mHost.getSchemeName().equals("https")) ? + request.mHost : proxy; + } + mConnection = mConnectionManager.getConnection(mContext, host); + mConnection.processRequests(request); + if (mConnection.getCanPersist()) { + if (!mConnectionManager.recycleConnection(host, + mConnection)) { + mConnection.closeConnection(); + } + } else { + mConnection.closeConnection(); + } + mConnection = null; + } + + } + } + + public synchronized String toString() { + String con = mConnection == null ? "" : mConnection.toString(); + String active = mWaiting ? "w" : "a"; + return "cid " + mId + " " + active + " " + con; + } + +} diff --git a/core/java/android/net/http/DomainNameChecker.java b/core/java/android/net/http/DomainNameChecker.java new file mode 100644 index 0000000..e4c8009 --- /dev/null +++ b/core/java/android/net/http/DomainNameChecker.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 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. + * 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 android.net.http; + +import org.bouncycastle.asn1.x509.X509Name; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.cert.X509Certificate; +import java.security.cert.CertificateParsingException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.Vector; + +/** + * Implements basic domain-name validation as specified by RFC2818. + * + * {@hide} + */ +public class DomainNameChecker { + private static Pattern QUICK_IP_PATTERN; + static { + try { + QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$"); + } catch (PatternSyntaxException e) {} + } + + private static final int ALT_DNS_NAME = 2; + private static final int ALT_IPA_NAME = 7; + + /** + * Checks the site certificate against the domain name of the site being visited + * @param certificate The certificate to check + * @param thisDomain The domain name of the site being visited + * @return True iff if there is a domain match as specified by RFC2818 + */ + public static boolean match(X509Certificate certificate, String thisDomain) { + if (certificate == null || thisDomain == null || thisDomain.length() == 0) { + return false; + } + + thisDomain = thisDomain.toLowerCase(); + if (!isIpAddress(thisDomain)) { + return matchDns(certificate, thisDomain); + } else { + return matchIpAddress(certificate, thisDomain); + } + } + + /** + * @return True iff the domain name is specified as an IP address + */ + private static boolean isIpAddress(String domain) { + boolean rval = (domain != null && domain.length() != 0); + if (rval) { + try { + // do a quick-dirty IP match first to avoid DNS lookup + rval = QUICK_IP_PATTERN.matcher(domain).matches(); + if (rval) { + rval = domain.equals( + InetAddress.getByName(domain).getHostAddress()); + } + } catch (UnknownHostException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "unknown host exception"; + } + + if (HttpLog.LOGV) { + HttpLog.v("DomainNameChecker.isIpAddress(): " + errorMessage); + } + + rval = false; + } + } + + return rval; + } + + /** + * Checks the site certificate against the IP domain name of the site being visited + * @param certificate The certificate to check + * @param thisDomain The DNS domain name of the site being visited + * @return True iff if there is a domain match as specified by RFC2818 + */ + private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) { + if (HttpLog.LOGV) { + HttpLog.v("DomainNameChecker.matchIpAddress(): this domain: " + thisDomain); + } + + try { + Collection subjectAltNames = certificate.getSubjectAlternativeNames(); + if (subjectAltNames != null) { + Iterator i = subjectAltNames.iterator(); + while (i.hasNext()) { + List altNameEntry = (List)(i.next()); + if (altNameEntry != null && 2 <= altNameEntry.size()) { + Integer altNameType = (Integer)(altNameEntry.get(0)); + if (altNameType != null) { + if (altNameType.intValue() == ALT_IPA_NAME) { + String altName = (String)(altNameEntry.get(1)); + if (altName != null) { + if (HttpLog.LOGV) { + HttpLog.v("alternative IP: " + altName); + } + if (thisDomain.equalsIgnoreCase(altName)) { + return true; + } + } + } + } + } + } + } + } catch (CertificateParsingException e) {} + + return false; + } + + /** + * Checks the site certificate against the DNS domain name of the site being visited + * @param certificate The certificate to check + * @param thisDomain The DNS domain name of the site being visited + * @return True iff if there is a domain match as specified by RFC2818 + */ + private static boolean matchDns(X509Certificate certificate, String thisDomain) { + boolean hasDns = false; + try { + Collection subjectAltNames = certificate.getSubjectAlternativeNames(); + if (subjectAltNames != null) { + Iterator i = subjectAltNames.iterator(); + while (i.hasNext()) { + List altNameEntry = (List)(i.next()); + if (altNameEntry != null && 2 <= altNameEntry.size()) { + Integer altNameType = (Integer)(altNameEntry.get(0)); + if (altNameType != null) { + if (altNameType.intValue() == ALT_DNS_NAME) { + hasDns = true; + String altName = (String)(altNameEntry.get(1)); + if (altName != null) { + if (matchDns(thisDomain, altName)) { + return true; + } + } + } + } + } + } + } + } catch (CertificateParsingException e) { + // one way we can get here is if an alternative name starts with + // '*' character, which is contrary to one interpretation of the + // spec (a valid DNS name must start with a letter); there is no + // good way around this, and in order to be compatible we proceed + // to check the common name (ie, ignore alternative names) + if (HttpLog.LOGV) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "failed to parse certificate"; + } + + if (HttpLog.LOGV) { + HttpLog.v("DomainNameChecker.matchDns(): " + errorMessage); + } + } + } + + if (!hasDns) { + X509Name xName = new X509Name(certificate.getSubjectDN().getName()); + Vector val = xName.getValues(); + Vector oid = xName.getOIDs(); + for (int i = 0; i < oid.size(); i++) { + if (oid.elementAt(i).equals(X509Name.CN)) { + return matchDns(thisDomain, (String)(val.elementAt(i))); + } + } + } + + return false; + } + + /** + * @param thisDomain The domain name of the site being visited + * @param thatDomain The domain name from the certificate + * @return True iff thisDomain matches thatDomain as specified by RFC2818 + */ + private static boolean matchDns(String thisDomain, String thatDomain) { + if (HttpLog.LOGV) { + HttpLog.v("DomainNameChecker.matchDns():" + + " this domain: " + thisDomain + + " that domain: " + thatDomain); + } + + if (thisDomain == null || thisDomain.length() == 0 || + thatDomain == null || thatDomain.length() == 0) { + return false; + } + + thatDomain = thatDomain.toLowerCase(); + + // (a) domain name strings are equal, ignoring case: X matches X + boolean rval = thisDomain.equals(thatDomain); + if (!rval) { + String[] thisDomainTokens = thisDomain.split("\\."); + String[] thatDomainTokens = thatDomain.split("\\."); + + int thisDomainTokensNum = thisDomainTokens.length; + int thatDomainTokensNum = thatDomainTokens.length; + + // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X + if (thisDomainTokensNum >= thatDomainTokensNum) { + for (int i = thatDomainTokensNum - 1; i >= 0; --i) { + rval = thisDomainTokens[i].equals(thatDomainTokens[i]); + if (!rval) { + // (c) OR we have a special *-match: + // Z.Y.X matches *.Y.X but does not match *.X + rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum); + if (rval) { + rval = thatDomainTokens[0].equals("*"); + if (!rval) { + // (d) OR we have a *-component match: + // f*.com matches foo.com but not bar.com + rval = domainTokenMatch( + thisDomainTokens[0], thatDomainTokens[0]); + } + } + + break; + } + } + } + } + + return rval; + } + + /** + * @param thisDomainToken The domain token from the current domain name + * @param thatDomainToken The domain token from the certificate + * @return True iff thisDomainToken matches thatDomainToken, using the + * wildcard match as specified by RFC2818-3.1. For example, f*.com must + * match foo.com but not bar.com + */ + private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) { + if (thisDomainToken != null && thatDomainToken != null) { + int starIndex = thatDomainToken.indexOf('*'); + if (starIndex >= 0) { + if (thatDomainToken.length() - 1 <= thisDomainToken.length()) { + String prefix = thatDomainToken.substring(0, starIndex); + String suffix = thatDomainToken.substring(starIndex + 1); + + return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix); + } + } + } + + return false; + } +} diff --git a/core/java/android/net/http/EventHandler.java b/core/java/android/net/http/EventHandler.java new file mode 100644 index 0000000..830d1f1 --- /dev/null +++ b/core/java/android/net/http/EventHandler.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2006 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 android.net.http; + + +/** + * Callbacks in this interface are made as an HTTP request is + * processed. The normal order of callbacks is status(), headers(), + * then multiple data() then endData(). handleSslErrorRequest(), if + * there is an SSL certificate error. error() can occur anywhere + * in the transaction. + * + * {@hide} + */ + +public interface EventHandler { + + /** + * Error codes used in the error() callback. Positive error codes + * are reserved for codes sent by http servers. Negative error + * codes are connection/parsing failures, etc. + */ + + /** Success */ + public static final int OK = 0; + /** Generic error */ + public static final int ERROR = -1; + /** Server or proxy hostname lookup failed */ + public static final int ERROR_LOOKUP = -2; + /** Unsupported authentication scheme (ie, not basic or digest) */ + public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3; + /** User authentication failed on server */ + public static final int ERROR_AUTH = -4; + /** User authentication failed on proxy */ + public static final int ERROR_PROXYAUTH = -5; + /** Could not connect to server */ + public static final int ERROR_CONNECT = -6; + /** Failed to write to or read from server */ + public static final int ERROR_IO = -7; + /** Connection timed out */ + public static final int ERROR_TIMEOUT = -8; + /** Too many redirects */ + public static final int ERROR_REDIRECT_LOOP = -9; + /** Unsupported URI scheme (ie, not http, https, etc) */ + public static final int ERROR_UNSUPPORTED_SCHEME = -10; + /** Failed to perform SSL handshake */ + public static final int ERROR_FAILED_SSL_HANDSHAKE = -11; + /** Bad URL */ + public static final int ERROR_BAD_URL = -12; + /** Generic file error for file:/// loads */ + public static final int FILE_ERROR = -13; + /** File not found error for file:/// loads */ + public static final int FILE_NOT_FOUND_ERROR = -14; + /** Too many requests queued */ + public static final int TOO_MANY_REQUESTS_ERROR = -15; + + final static int[] errorStringResources = { + com.android.internal.R.string.httpErrorOk, + com.android.internal.R.string.httpError, + com.android.internal.R.string.httpErrorLookup, + com.android.internal.R.string.httpErrorUnsupportedAuthScheme, + com.android.internal.R.string.httpErrorAuth, + com.android.internal.R.string.httpErrorProxyAuth, + com.android.internal.R.string.httpErrorConnect, + com.android.internal.R.string.httpErrorIO, + com.android.internal.R.string.httpErrorTimeout, + com.android.internal.R.string.httpErrorRedirectLoop, + com.android.internal.R.string.httpErrorUnsupportedScheme, + com.android.internal.R.string.httpErrorFailedSslHandshake, + com.android.internal.R.string.httpErrorBadUrl, + com.android.internal.R.string.httpErrorFile, + com.android.internal.R.string.httpErrorFileNotFound, + com.android.internal.R.string.httpErrorTooManyRequests + }; + + /** + * Called after status line has been sucessfully processed. + * @param major_version HTTP version advertised by server. major + * is the part before the "." + * @param minor_version HTTP version advertised by server. minor + * is the part after the "." + * @param code HTTP Status code. See RFC 2616. + * @param reason_phrase Textual explanation sent by server + */ + public void status(int major_version, + int minor_version, + int code, + String reason_phrase); + + /** + * Called after all headers are successfully processed. + */ + public void headers(Headers headers); + + /** + * An array containing all or part of the http body as read from + * the server. + * @param data A byte array containing the content + * @param len The length of valid content in data + * + * Note: chunked and compressed encodings are handled within + * android.net.http. Decoded data is passed through this + * interface. + */ + public void data(byte[] data, int len); + + /** + * Called when the document is completely read. No more data() + * callbacks will be made after this call + */ + public void endData(); + + /** + * SSL certificate callback called every time a resource is + * loaded via a secure connection + */ + public void certificate(SslCertificate certificate); + + /** + * There was trouble. + * @param id One of the error codes defined below + * @param description of error + */ + public void error(int id, String description); + + /** + * SSL certificate error callback. Handles SSL error(s) on the way + * up to the user. The callback has to make sure that restartConnection() is called, + * otherwise the connection will be suspended indefinitely. + */ + public void handleSslErrorRequest(SslError error); + +} diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java new file mode 100644 index 0000000..5d85ba4 --- /dev/null +++ b/core/java/android/net/http/Headers.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2006 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 android.net.http; + +import android.util.Config; +import android.util.Log; + +import java.util.ArrayList; + +import org.apache.http.HeaderElement; +import org.apache.http.entity.ContentLengthStrategy; +import org.apache.http.message.BasicHeaderValueParser; +import org.apache.http.message.ParserCursor; +import org.apache.http.protocol.HTTP; +import org.apache.http.util.CharArrayBuffer; + +/** + * Manages received headers + * + * {@hide} + */ +public final class Headers { + private static final String LOGTAG = "Http"; + + // header parsing constant + /** + * indicate HTTP 1.0 connection close after the response + */ + public final static int CONN_CLOSE = 1; + /** + * indicate HTTP 1.1 connection keep alive + */ + public final static int CONN_KEEP_ALIVE = 2; + + // initial values. + public final static int NO_CONN_TYPE = 0; + public final static long NO_TRANSFER_ENCODING = 0; + public final static long NO_CONTENT_LENGTH = -1; + + // header string + public final static String TRANSFER_ENCODING = "transfer-encoding"; + public final static String CONTENT_LEN = "content-length"; + public final static String CONTENT_TYPE = "content-type"; + public final static String CONTENT_ENCODING = "content-encoding"; + public final static String CONN_DIRECTIVE = "connection"; + + public final static String LOCATION = "location"; + public final static String PROXY_CONNECTION = "proxy-connection"; + + public final static String WWW_AUTHENTICATE = "www-authenticate"; + public final static String PROXY_AUTHENTICATE = "proxy-authenticate"; + public final static String CONTENT_DISPOSITION = "content-disposition"; + public final static String ACCEPT_RANGES = "accept-ranges"; + public final static String EXPIRES = "expires"; + public final static String CACHE_CONTROL = "cache-control"; + public final static String LAST_MODIFIED = "last-modified"; + public final static String ETAG = "etag"; + public final static String SET_COOKIE = "set-cookie"; + public final static String PRAGMA = "pragma"; + public final static String REFRESH = "refresh"; + + // following hash are generated by String.hashCode() + private final static int HASH_TRANSFER_ENCODING = 1274458357; + private final static int HASH_CONTENT_LEN = -1132779846; + private final static int HASH_CONTENT_TYPE = 785670158; + private final static int HASH_CONTENT_ENCODING = 2095084583; + private final static int HASH_CONN_DIRECTIVE = -775651618; + private final static int HASH_LOCATION = 1901043637; + private final static int HASH_PROXY_CONNECTION = 285929373; + private final static int HASH_WWW_AUTHENTICATE = -243037365; + private final static int HASH_PROXY_AUTHENTICATE = -301767724; + private final static int HASH_CONTENT_DISPOSITION = -1267267485; + private final static int HASH_ACCEPT_RANGES = 1397189435; + private final static int HASH_EXPIRES = -1309235404; + private final static int HASH_CACHE_CONTROL = -208775662; + private final static int HASH_LAST_MODIFIED = 150043680; + private final static int HASH_ETAG = 3123477; + private final static int HASH_SET_COOKIE = 1237214767; + private final static int HASH_PRAGMA = -980228804; + private final static int HASH_REFRESH = 1085444827; + + private long transferEncoding; + private long contentLength; // Content length of the incoming data + private int connectionType; + + private String contentType; + private String contentEncoding; + private String location; + private String wwwAuthenticate; + private String proxyAuthenticate; + private String contentDisposition; + private String acceptRanges; + private String expires; + private String cacheControl; + private String lastModified; + private String etag; + private String pragma; + private String refresh; + private ArrayList<String> cookies = new ArrayList<String>(2); + + public Headers() { + transferEncoding = NO_TRANSFER_ENCODING; + contentLength = NO_CONTENT_LENGTH; + connectionType = NO_CONN_TYPE; + } + + public void parseHeader(CharArrayBuffer buffer) { + int pos = CharArrayBuffers.setLowercaseIndexOf(buffer, ':'); + if (pos == -1) { + return; + } + String name = buffer.substringTrimmed(0, pos); + if (name.length() == 0) { + return; + } + pos++; + + if (HttpLog.LOGV) { + String val = buffer.substringTrimmed(pos, buffer.length()); + HttpLog.v("hdr " + buffer.length() + " " + buffer); + } + + switch (name.hashCode()) { + case HASH_TRANSFER_ENCODING: + if (name.equals(TRANSFER_ENCODING)) { + // headers.transferEncoding = + HeaderElement[] encodings = BasicHeaderValueParser.DEFAULT + .parseElements(buffer, new ParserCursor(pos, + buffer.length())); + // The chunked encoding must be the last one applied RFC2616, + // 14.41 + int len = encodings.length; + if (HTTP.IDENTITY_CODING.equalsIgnoreCase(buffer + .substringTrimmed(pos, buffer.length()))) { + transferEncoding = ContentLengthStrategy.IDENTITY; + } else if ((len > 0) + && (HTTP.CHUNK_CODING + .equalsIgnoreCase(encodings[len - 1].getName()))) { + transferEncoding = ContentLengthStrategy.CHUNKED; + } else { + transferEncoding = ContentLengthStrategy.IDENTITY; + } + } + break; + case HASH_CONTENT_LEN: + if (name.equals(CONTENT_LEN)) { + try { + contentLength = Long.parseLong(buffer.substringTrimmed(pos, + buffer.length())); + } catch (NumberFormatException e) { + if (Config.LOGV) { + Log.v(LOGTAG, "Headers.headers(): error parsing" + + " content length: " + buffer.toString()); + } + } + } + break; + case HASH_CONTENT_TYPE: + if (name.equals(CONTENT_TYPE)) { + contentType = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_CONTENT_ENCODING: + if (name.equals(CONTENT_ENCODING)) { + contentEncoding = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_CONN_DIRECTIVE: + if (name.equals(CONN_DIRECTIVE)) { + setConnectionType(buffer, pos); + } + break; + case HASH_LOCATION: + if (name.equals(LOCATION)) { + location = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_PROXY_CONNECTION: + if (name.equals(PROXY_CONNECTION)) { + setConnectionType(buffer, pos); + } + break; + case HASH_WWW_AUTHENTICATE: + if (name.equals(WWW_AUTHENTICATE)) { + wwwAuthenticate = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_PROXY_AUTHENTICATE: + if (name.equals(PROXY_AUTHENTICATE)) { + proxyAuthenticate = buffer.substringTrimmed(pos, buffer + .length()); + } + break; + case HASH_CONTENT_DISPOSITION: + if (name.equals(CONTENT_DISPOSITION)) { + contentDisposition = buffer.substringTrimmed(pos, buffer + .length()); + } + break; + case HASH_ACCEPT_RANGES: + if (name.equals(ACCEPT_RANGES)) { + acceptRanges = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_EXPIRES: + if (name.equals(EXPIRES)) { + expires = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_CACHE_CONTROL: + if (name.equals(CACHE_CONTROL)) { + cacheControl = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_LAST_MODIFIED: + if (name.equals(LAST_MODIFIED)) { + lastModified = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_ETAG: + if (name.equals(ETAG)) { + etag = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_SET_COOKIE: + if (name.equals(SET_COOKIE)) { + cookies.add(buffer.substringTrimmed(pos, buffer.length())); + } + break; + case HASH_PRAGMA: + if (name.equals(PRAGMA)) { + pragma = buffer.substringTrimmed(pos, buffer.length()); + } + break; + case HASH_REFRESH: + if (name.equals(REFRESH)) { + refresh = buffer.substringTrimmed(pos, buffer.length()); + } + break; + default: + // ignore + } + } + + public long getTransferEncoding() { + return transferEncoding; + } + + public long getContentLength() { + return contentLength; + } + + public int getConnectionType() { + return connectionType; + } + + private void setConnectionType(CharArrayBuffer buffer, int pos) { + if (CharArrayBuffers.containsIgnoreCaseTrimmed( + buffer, pos, HTTP.CONN_CLOSE)) { + connectionType = CONN_CLOSE; + } else if (CharArrayBuffers.containsIgnoreCaseTrimmed( + buffer, pos, HTTP.CONN_KEEP_ALIVE)) { + connectionType = CONN_KEEP_ALIVE; + } + } + + public String getContentType() { + return this.contentType; + } + + public String getContentEncoding() { + return this.contentEncoding; + } + + public String getLocation() { + return this.location; + } + + public String getWwwAuthenticate() { + return this.wwwAuthenticate; + } + + public String getProxyAuthenticate() { + return this.proxyAuthenticate; + } + + public String getContentDisposition() { + return this.contentDisposition; + } + + public String getAcceptRanges() { + return this.acceptRanges; + } + + public String getExpires() { + return this.expires; + } + + public String getCacheControl() { + return this.cacheControl; + } + + public String getLastModified() { + return this.lastModified; + } + + public String getEtag() { + return this.etag; + } + + public ArrayList<String> getSetCookie() { + return this.cookies; + } + + public String getPragma() { + return this.pragma; + } + + public String getRefresh() { + return this.refresh; + } + + public void setContentLength(long value) { + this.contentLength = value; + } + + public void setContentType(String value) { + this.contentType = value; + } + + public void setContentEncoding(String value) { + this.contentEncoding = value; + } + + public void setLocation(String value) { + this.location = value; + } + + public void setWwwAuthenticate(String value) { + this.wwwAuthenticate = value; + } + + public void setProxyAuthenticate(String value) { + this.proxyAuthenticate = value; + } + + public void setContentDisposition(String value) { + this.contentDisposition = value; + } + + public void setAcceptRanges(String value) { + this.acceptRanges = value; + } + + public void setExpires(String value) { + this.expires = value; + } + + public void setCacheControl(String value) { + this.cacheControl = value; + } + + public void setLastModified(String value) { + this.lastModified = value; + } + + public void setEtag(String value) { + this.etag = value; + } +} diff --git a/core/java/android/net/http/HttpAuthHeader.java b/core/java/android/net/http/HttpAuthHeader.java new file mode 100644 index 0000000..d41284c --- /dev/null +++ b/core/java/android/net/http/HttpAuthHeader.java @@ -0,0 +1,422 @@ +/* + * 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 android.net.http; + +/** + * HttpAuthHeader: a class to store HTTP authentication-header parameters. + * For more information, see: RFC 2617: HTTP Authentication. + * + * {@hide} + */ +public class HttpAuthHeader { + /** + * Possible HTTP-authentication header tokens to search for: + */ + public final static String BASIC_TOKEN = "Basic"; + public final static String DIGEST_TOKEN = "Digest"; + + private final static String REALM_TOKEN = "realm"; + private final static String NONCE_TOKEN = "nonce"; + private final static String STALE_TOKEN = "stale"; + private final static String OPAQUE_TOKEN = "opaque"; + private final static String QOP_TOKEN = "qop"; + private final static String ALGORITHM_TOKEN = "algorithm"; + + /** + * An authentication scheme. We currently support two different schemes: + * HttpAuthHeader.BASIC - basic, and + * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth"). + */ + private int mScheme; + + public static final int UNKNOWN = 0; + public static final int BASIC = 1; + public static final int DIGEST = 2; + + /** + * A flag, indicating that the previous request from the client was + * rejected because the nonce value was stale. If stale is TRUE + * (case-insensitive), the client may wish to simply retry the request + * with a new encrypted response, without reprompting the user for a + * new username and password. + */ + private boolean mStale; + + /** + * A string to be displayed to users so they know which username and + * password to use. + */ + private String mRealm; + + /** + * A server-specified data string which should be uniquely generated + * each time a 401 response is made. + */ + private String mNonce; + + /** + * A string of data, specified by the server, which should be returned + * by the client unchanged in the Authorization header of subsequent + * requests with URIs in the same protection space. + */ + private String mOpaque; + + /** + * This directive is optional, but is made so only for backward + * compatibility with RFC 2069 [6]; it SHOULD be used by all + * implementations compliant with this version of the Digest scheme. + * If present, it is a quoted string of one or more tokens indicating + * the "quality of protection" values supported by the server. The + * value "auth" indicates authentication; the value "auth-int" + * indicates authentication with integrity protection. + */ + private String mQop; + + /** + * A string indicating a pair of algorithms used to produce the digest + * and a checksum. If this is not present it is assumed to be "MD5". + */ + private String mAlgorithm; + + /** + * Is this authentication request a proxy authentication request? + */ + private boolean mIsProxy; + + /** + * Username string we get from the user. + */ + private String mUsername; + + /** + * Password string we get from the user. + */ + private String mPassword; + + /** + * Creates a new HTTP-authentication header object from the + * input header string. + * The header string is assumed to contain parameters of at + * most one authentication-scheme (ensured by the caller). + */ + public HttpAuthHeader(String header) { + if (header != null) { + parseHeader(header); + } + } + + /** + * @return True iff this is a proxy authentication header. + */ + public boolean isProxy() { + return mIsProxy; + } + + /** + * Marks this header as a proxy authentication header. + */ + public void setProxy() { + mIsProxy = true; + } + + /** + * @return The username string. + */ + public String getUsername() { + return mUsername; + } + + /** + * Sets the username string. + */ + public void setUsername(String username) { + mUsername = username; + } + + /** + * @return The password string. + */ + public String getPassword() { + return mPassword; + } + + /** + * Sets the password string. + */ + public void setPassword(String password) { + mPassword = password; + } + + /** + * @return True iff this is the BASIC-authentication request. + */ + public boolean isBasic () { + return mScheme == BASIC; + } + + /** + * @return True iff this is the DIGEST-authentication request. + */ + public boolean isDigest() { + return mScheme == DIGEST; + } + + /** + * @return The authentication scheme requested. We currently + * support two schemes: + * HttpAuthHeader.BASIC - basic, and + * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth"). + */ + public int getScheme() { + return mScheme; + } + + /** + * @return True if indicating that the previous request from + * the client was rejected because the nonce value was stale. + */ + public boolean getStale() { + return mStale; + } + + /** + * @return The realm value or null if there is none. + */ + public String getRealm() { + return mRealm; + } + + /** + * @return The nonce value or null if there is none. + */ + public String getNonce() { + return mNonce; + } + + /** + * @return The opaque value or null if there is none. + */ + public String getOpaque() { + return mOpaque; + } + + /** + * @return The QOP ("quality-of_protection") value or null if + * there is none. The QOP value is always lower-case. + */ + public String getQop() { + return mQop; + } + + /** + * @return The name of the algorithm used or null if there is + * none. By default, MD5 is used. + */ + public String getAlgorithm() { + return mAlgorithm; + } + + /** + * @return True iff the authentication scheme requested by the + * server is supported; currently supported schemes: + * BASIC, + * DIGEST (only algorithm="md5", no qop or qop="auth). + */ + public boolean isSupportedScheme() { + // it is a good idea to enforce non-null realms! + if (mRealm != null) { + if (mScheme == BASIC) { + return true; + } else { + if (mScheme == DIGEST) { + return + mAlgorithm.equals("md5") && + (mQop == null || mQop.equals("auth")); + } + } + } + + return false; + } + + /** + * Parses the header scheme name and then scheme parameters if + * the scheme is supported. + */ + private void parseHeader(String header) { + if (HttpLog.LOGV) { + HttpLog.v("HttpAuthHeader.parseHeader(): header: " + header); + } + + if (header != null) { + String parameters = parseScheme(header); + if (parameters != null) { + // if we have a supported scheme + if (mScheme != UNKNOWN) { + parseParameters(parameters); + } + } + } + } + + /** + * Parses the authentication scheme name. If we have a Digest + * scheme, sets the algorithm value to the default of MD5. + * @return The authentication scheme parameters string to be + * parsed later (if the scheme is supported) or null if failed + * to parse the scheme (the header value is null?). + */ + private String parseScheme(String header) { + if (header != null) { + int i = header.indexOf(' '); + if (i >= 0) { + String scheme = header.substring(0, i).trim(); + if (scheme.equalsIgnoreCase(DIGEST_TOKEN)) { + mScheme = DIGEST; + + // md5 is the default algorithm!!! + mAlgorithm = "md5"; + } else { + if (scheme.equalsIgnoreCase(BASIC_TOKEN)) { + mScheme = BASIC; + } + } + + return header.substring(i + 1); + } + } + + return null; + } + + /** + * Parses a comma-separated list of authentification scheme + * parameters. + */ + private void parseParameters(String parameters) { + if (HttpLog.LOGV) { + HttpLog.v("HttpAuthHeader.parseParameters():" + + " parameters: " + parameters); + } + + if (parameters != null) { + int i; + do { + i = parameters.indexOf(','); + if (i < 0) { + // have only one parameter + parseParameter(parameters); + } else { + parseParameter(parameters.substring(0, i)); + parameters = parameters.substring(i + 1); + } + } while (i >= 0); + } + } + + /** + * Parses a single authentication scheme parameter. The parameter + * string is expected to follow the format: PARAMETER=VALUE. + */ + private void parseParameter(String parameter) { + if (parameter != null) { + // here, we are looking for the 1st occurence of '=' only!!! + int i = parameter.indexOf('='); + if (i >= 0) { + String token = parameter.substring(0, i).trim(); + String value = + trimDoubleQuotesIfAny(parameter.substring(i + 1).trim()); + + if (HttpLog.LOGV) { + HttpLog.v("HttpAuthHeader.parseParameter():" + + " token: " + token + + " value: " + value); + } + + if (token.equalsIgnoreCase(REALM_TOKEN)) { + mRealm = value; + } else { + if (mScheme == DIGEST) { + parseParameter(token, value); + } + } + } + } + } + + /** + * If the token is a known parameter name, parses and initializes + * the token value. + */ + private void parseParameter(String token, String value) { + if (token != null && value != null) { + if (token.equalsIgnoreCase(NONCE_TOKEN)) { + mNonce = value; + return; + } + + if (token.equalsIgnoreCase(STALE_TOKEN)) { + parseStale(value); + return; + } + + if (token.equalsIgnoreCase(OPAQUE_TOKEN)) { + mOpaque = value; + return; + } + + if (token.equalsIgnoreCase(QOP_TOKEN)) { + mQop = value.toLowerCase(); + return; + } + + if (token.equalsIgnoreCase(ALGORITHM_TOKEN)) { + mAlgorithm = value.toLowerCase(); + return; + } + } + } + + /** + * Parses and initializes the 'stale' paramer value. Any value + * different from case-insensitive "true" is considered "false". + */ + private void parseStale(String value) { + if (value != null) { + if (value.equalsIgnoreCase("true")) { + mStale = true; + } + } + } + + /** + * Trims double-quotes around a parameter value if there are any. + * @return The string value without the outermost pair of double- + * quotes or null if the original value is null. + */ + static private String trimDoubleQuotesIfAny(String value) { + if (value != null) { + int len = value.length(); + if (len > 2 && + value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') { + return value.substring(1, len - 1); + } + } + + return value; + } +} diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java new file mode 100644 index 0000000..8b12d0b --- /dev/null +++ b/core/java/android/net/http/HttpConnection.java @@ -0,0 +1,96 @@ +/* + * 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 android.net.http; + +import android.content.Context; + +import java.net.Socket; +import java.io.IOException; + +import org.apache.http.HttpClientConnection; +import org.apache.http.HttpHost; +import org.apache.http.impl.DefaultHttpClientConnection; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; + +/** + * A requestConnection connecting to a normal (non secure) http server + * + * {@hide} + */ +class HttpConnection extends Connection { + + HttpConnection(Context context, HttpHost host, + RequestQueue.ConnectionManager connectionManager, + RequestFeeder requestFeeder) { + super(context, host, connectionManager, requestFeeder); + } + + /** + * Opens the connection to a http server + * + * @return the opened low level connection + * @throws IOException if the connection fails for any reason. + */ + @Override + AndroidHttpClientConnection openConnection(Request req) throws IOException { + + // Update the certificate info (connection not secure - set to null) + EventHandler eventHandler = req.getEventHandler(); + mCertificate = null; + eventHandler.certificate(mCertificate); + + AndroidHttpClientConnection conn = new AndroidHttpClientConnection(); + BasicHttpParams params = new BasicHttpParams(); + Socket sock = new Socket(mHost.getHostName(), mHost.getPort()); + params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192); + conn.bind(sock, params); + return conn; + } + + /** + * Closes the low level connection. + * + * If an exception is thrown then it is assumed that the + * connection will have been closed (to the extent possible) + * anyway and the caller does not need to take any further action. + * + */ + void closeConnection() { + try { + if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) { + mHttpClientConnection.close(); + } + } catch (IOException e) { + if (HttpLog.LOGV) HttpLog.v( + "closeConnection(): failed closing connection " + + mHost); + e.printStackTrace(); + } + } + + /** + * Restart a secure connection suspended waiting for user interaction. + */ + void restartConnection(boolean abort) { + // not required for plain http connections + } + + String getScheme() { + return "http"; + } +} diff --git a/core/java/android/net/http/HttpLog.java b/core/java/android/net/http/HttpLog.java new file mode 100644 index 0000000..30bf647 --- /dev/null +++ b/core/java/android/net/http/HttpLog.java @@ -0,0 +1,44 @@ +/* + * 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-level logging flag + */ + +package android.net.http; + +import android.os.SystemClock; + +import android.util.Log; +import android.util.Config; + +/** + * {@hide} + */ +class HttpLog { + private final static String LOGTAG = "http"; + + private static final boolean DEBUG = false; + static final boolean LOGV = DEBUG ? Config.LOGD : Config.LOGV; + + static void v(String logMe) { + Log.v(LOGTAG, SystemClock.uptimeMillis() + " " + Thread.currentThread().getName() + " " + logMe); + } + + static void e(String logMe) { + Log.e(LOGTAG, logMe); + } +} diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java new file mode 100644 index 0000000..fe02d3e --- /dev/null +++ b/core/java/android/net/http/HttpsConnection.java @@ -0,0 +1,427 @@ +/* + * 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 android.net.http; + +import android.content.Context; + +import junit.framework.Assert; + +import java.io.IOException; + +import java.security.cert.X509Certificate; + +import java.net.Socket; +import java.net.InetSocketAddress; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.Header; +import org.apache.http.HttpClientConnection; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ParseException; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.impl.DefaultHttpClientConnection; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpConnectionParams; + +/** + * Simple exception we throw if the SSL connection is closed by the user. + * + * {@hide} + */ +class SSLConnectionClosedByUserException extends SSLException { + + public SSLConnectionClosedByUserException(String reason) { + super(reason); + } +} + +/** + * A Connection connecting to a secure http server or tunneling through + * a http proxy server to a https server. + */ +class HttpsConnection extends Connection { + + /** + * SSL context + */ + private static SSLContext mSslContext = null; + + /** + * SSL socket factory + */ + private static SSLSocketFactory mSslSocketFactory = null; + + static { + // initialize the socket factory + try { + mSslContext = SSLContext.getInstance("TLS"); + if (mSslContext != null) { + // here, trust managers is a single trust-all manager + TrustManager[] trustManagers = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted( + X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted( + X509Certificate[] certs, String authType) { + } + } + }; + + mSslContext.init(null, trustManagers, null); + mSslSocketFactory = mSslContext.getSocketFactory(); + } + } catch (Exception t) { + if (HttpLog.LOGV) { + HttpLog.v("HttpsConnection: failed to initialize the socket factory"); + } + } + } + + /** + * @return The shared SSL context. + */ + /*package*/ static SSLContext getContext() { + return mSslContext; + } + + /** + * Object to wait on when suspending the SSL connection + */ + private Object mSuspendLock = new Object(); + + /** + * True if the connection is suspended pending the result of asking the + * user about an error. + */ + private boolean mSuspended = false; + + /** + * True if the connection attempt should be aborted due to an ssl + * error. + */ + private boolean mAborted = false; + + /** + * Contructor for a https connection. + */ + HttpsConnection(Context context, HttpHost host, + RequestQueue.ConnectionManager connectionManager, + RequestFeeder requestFeeder) { + super(context, host, connectionManager, requestFeeder); + } + + /** + * Sets the server SSL certificate associated with this + * connection. + * @param certificate The SSL certificate + */ + /* package */ void setCertificate(SslCertificate certificate) { + mCertificate = certificate; + } + + /** + * Opens the connection to a http server or proxy. + * + * @return the opened low level connection + * @throws IOException if the connection fails for any reason. + */ + @Override + AndroidHttpClientConnection openConnection(Request req) throws IOException { + SSLSocket sslSock = null; + + HttpHost proxyHost = mConnectionManager.getProxyHost(); + if (proxyHost != null) { + // If we have a proxy set, we first send a CONNECT request + // to the proxy; if the proxy returns 200 OK, we negotiate + // a secure connection to the target server via the proxy. + // If the request fails, we drop it, but provide the event + // handler with the response status and headers. The event + // handler is then responsible for cancelling the load or + // issueing a new request. + AndroidHttpClientConnection proxyConnection = null; + Socket proxySock = null; + try { + proxySock = new Socket + (proxyHost.getHostName(), proxyHost.getPort()); + + proxySock.setSoTimeout(60 * 1000); + + proxyConnection = new AndroidHttpClientConnection(); + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSocketBufferSize(params, 8192); + + proxyConnection.bind(proxySock, params); + } catch(IOException e) { + if (proxyConnection != null) { + proxyConnection.close(); + } + + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = + "failed to establish a connection to the proxy"; + } + + throw new IOException(errorMessage); + } + + StatusLine statusLine = null; + int statusCode = 0; + Headers headers = new Headers(); + try { + BasicHttpRequest proxyReq = new BasicHttpRequest + ("CONNECT", mHost.toHostString()); + + // add all 'proxy' headers from the original request + for (Header h : req.mHttpRequest.getAllHeaders()) { + String headerName = h.getName().toLowerCase(); + if (headerName.startsWith("proxy") || headerName.equals("keep-alive")) { + proxyReq.addHeader(h); + } + } + + proxyConnection.sendRequestHeader(proxyReq); + proxyConnection.flush(); + + // it is possible to receive informational status + // codes prior to receiving actual headers; + // all those status codes are smaller than OK 200 + // a loop is a standard way of dealing with them + do { + statusLine = proxyConnection.parseResponseHeader(headers); + statusCode = statusLine.getStatusCode(); + } while (statusCode < HttpStatus.SC_OK); + } catch (ParseException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = + "failed to send a CONNECT request"; + } + + throw new IOException(errorMessage); + } catch (HttpException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = + "failed to send a CONNECT request"; + } + + throw new IOException(errorMessage); + } catch (IOException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = + "failed to send a CONNECT request"; + } + + throw new IOException(errorMessage); + } + + if (statusCode == HttpStatus.SC_OK) { + try { + synchronized (mSslSocketFactory) { + sslSock = (SSLSocket) mSslSocketFactory.createSocket( + proxySock, mHost.getHostName(), mHost.getPort(), true); + } + } catch(IOException e) { + if (sslSock != null) { + sslSock.close(); + } + + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = + "failed to create an SSL socket"; + } + throw new IOException(errorMessage); + } + } else { + // if the code is not OK, inform the event handler + ProtocolVersion version = statusLine.getProtocolVersion(); + + req.mEventHandler.status(version.getMajor(), + version.getMinor(), + statusCode, + statusLine.getReasonPhrase()); + req.mEventHandler.headers(headers); + req.mEventHandler.endData(); + + proxyConnection.close(); + + // here, we return null to indicate that the original + // request needs to be dropped + return null; + } + } else { + // if we do not have a proxy, we simply connect to the host + try { + synchronized (mSslSocketFactory) { + sslSock = (SSLSocket) mSslSocketFactory.createSocket(); + + sslSock.setSoTimeout(SOCKET_TIMEOUT); + sslSock.connect(new InetSocketAddress(mHost.getHostName(), + mHost.getPort())); + + } + } catch(IOException e) { + if (sslSock != null) { + sslSock.close(); + } + + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "failed to create an SSL socket"; + } + + throw new IOException(errorMessage); + } + } + + // do handshake and validate server certificates + SslError error = CertificateChainValidator.getInstance(). + doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName()); + + EventHandler eventHandler = req.getEventHandler(); + + // Update the certificate info (to be consistent, it is better to do it + // here, before we start handling SSL errors, if any) + eventHandler.certificate(mCertificate); + + // Inform the user if there is a problem + if (error != null) { + // handleSslErrorRequest may immediately unsuspend if it wants to + // allow the certificate anyway. + // So we mark the connection as suspended, call handleSslErrorRequest + // then check if we're still suspended and only wait if we actually + // need to. + synchronized (mSuspendLock) { + mSuspended = true; + } + // don't hold the lock while calling out to the event handler + eventHandler.handleSslErrorRequest(error); + synchronized (mSuspendLock) { + if (mSuspended) { + try { + // Put a limit on how long we are waiting; if the timeout + // expires (which should never happen unless you choose + // to ignore the SSL error dialog for a very long time), + // we wake up the thread and abort the request. This is + // to prevent us from stalling the network if things go + // very bad. + mSuspendLock.wait(10 * 60 * 1000); + if (mSuspended) { + // mSuspended is true if we have not had a chance to + // restart the connection yet (ie, the wait timeout + // has expired) + mSuspended = false; + mAborted = true; + if (HttpLog.LOGV) { + HttpLog.v("HttpsConnection.openConnection():" + + " SSL timeout expired and request was cancelled!!!"); + } + } + } catch (InterruptedException e) { + // ignore + } + } + if (mAborted) { + // The user decided not to use this unverified connection + // so close it immediately. + sslSock.close(); + throw new SSLConnectionClosedByUserException("connection closed by the user"); + } + } + } + + // All went well, we have an open, verified connection. + AndroidHttpClientConnection conn = new AndroidHttpClientConnection(); + BasicHttpParams params = new BasicHttpParams(); + params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192); + conn.bind(sslSock, params); + return conn; + } + + /** + * Closes the low level connection. + * + * If an exception is thrown then it is assumed that the connection will + * have been closed (to the extent possible) anyway and the caller does not + * need to take any further action. + * + */ + @Override + void closeConnection() { + // if the connection has been suspended due to an SSL error + if (mSuspended) { + // wake up the network thread + restartConnection(false); + } + + try { + if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) { + mHttpClientConnection.close(); + } + } catch (IOException e) { + if (HttpLog.LOGV) + HttpLog.v("HttpsConnection.closeConnection():" + + " failed closing connection " + mHost); + e.printStackTrace(); + } + } + + /** + * Restart a secure connection suspended waiting for user interaction. + */ + void restartConnection(boolean proceed) { + if (HttpLog.LOGV) { + HttpLog.v("HttpsConnection.restartConnection():" + + " proceed: " + proceed); + } + + synchronized (mSuspendLock) { + if (mSuspended) { + mSuspended = false; + mAborted = !proceed; + mSuspendLock.notify(); + } + } + } + + @Override + String getScheme() { + return "https"; + } +} diff --git a/core/java/android/net/http/IdleCache.java b/core/java/android/net/http/IdleCache.java new file mode 100644 index 0000000..fda6009 --- /dev/null +++ b/core/java/android/net/http/IdleCache.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 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. + * 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. + */ + +/** + * Hangs onto idle live connections for a little while + */ + +package android.net.http; + +import org.apache.http.HttpHost; + +import android.os.SystemClock; + +/** + * {@hide} + */ +class IdleCache { + + class Entry { + HttpHost mHost; + Connection mConnection; + long mTimeout; + }; + + private final static int IDLE_CACHE_MAX = 8; + + /* Allow five consecutive empty queue checks before shutdown */ + private final static int EMPTY_CHECK_MAX = 5; + + /* six second timeout for connections */ + private final static int TIMEOUT = 6 * 1000; + private final static int CHECK_INTERVAL = 2 * 1000; + private Entry[] mEntries = new Entry[IDLE_CACHE_MAX]; + + private int mCount = 0; + + private IdleReaper mThread = null; + + /* stats */ + private int mCached = 0; + private int mReused = 0; + + IdleCache() { + for (int i = 0; i < IDLE_CACHE_MAX; i++) { + mEntries[i] = new Entry(); + } + } + + /** + * Caches connection, if there is room. + * @return true if connection cached + */ + synchronized boolean cacheConnection( + HttpHost host, Connection connection) { + + boolean ret = false; + + if (HttpLog.LOGV) { + HttpLog.v("IdleCache size " + mCount + " host " + host); + } + + if (mCount < IDLE_CACHE_MAX) { + long time = SystemClock.uptimeMillis(); + for (int i = 0; i < IDLE_CACHE_MAX; i++) { + Entry entry = mEntries[i]; + if (entry.mHost == null) { + entry.mHost = host; + entry.mConnection = connection; + entry.mTimeout = time + TIMEOUT; + mCount++; + if (HttpLog.LOGV) mCached++; + ret = true; + if (mThread == null) { + mThread = new IdleReaper(); + mThread.start(); + } + break; + } + } + } + return ret; + } + + synchronized Connection getConnection(HttpHost host) { + Connection ret = null; + + if (mCount > 0) { + for (int i = 0; i < IDLE_CACHE_MAX; i++) { + Entry entry = mEntries[i]; + HttpHost eHost = entry.mHost; + if (eHost != null && eHost.equals(host)) { + ret = entry.mConnection; + entry.mHost = null; + entry.mConnection = null; + mCount--; + if (HttpLog.LOGV) mReused++; + break; + } + } + } + return ret; + } + + synchronized void clear() { + for (int i = 0; mCount > 0 && i < IDLE_CACHE_MAX; i++) { + Entry entry = mEntries[i]; + if (entry.mHost != null) { + entry.mHost = null; + entry.mConnection.closeConnection(); + entry.mConnection = null; + mCount--; + } + } + } + + private synchronized void clearIdle() { + if (mCount > 0) { + long time = SystemClock.uptimeMillis(); + for (int i = 0; i < IDLE_CACHE_MAX; i++) { + Entry entry = mEntries[i]; + if (entry.mHost != null && time > entry.mTimeout) { + entry.mHost = null; + entry.mConnection.closeConnection(); + entry.mConnection = null; + mCount--; + } + } + } + } + + private class IdleReaper extends Thread { + + public void run() { + int check = 0; + + setName("IdleReaper"); + android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_BACKGROUND); + synchronized (IdleCache.this) { + while (check < EMPTY_CHECK_MAX) { + try { + IdleCache.this.wait(CHECK_INTERVAL); + } catch (InterruptedException ex) { + } + if (mCount == 0) { + check++; + } else { + check = 0; + clearIdle(); + } + } + mThread = null; + } + if (HttpLog.LOGV) { + HttpLog.v("IdleCache IdleReaper shutdown: cached " + mCached + + " reused " + mReused); + mCached = 0; + mReused = 0; + } + } + } +} diff --git a/core/java/android/net/http/LoggingEventHandler.java b/core/java/android/net/http/LoggingEventHandler.java new file mode 100644 index 0000000..1b18651 --- /dev/null +++ b/core/java/android/net/http/LoggingEventHandler.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2006 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. + */ + +/** + * A test EventHandler: Logs everything received + */ + +package android.net.http; + +import android.net.http.Headers; + +/** + * {@hide} + */ +public class LoggingEventHandler implements EventHandler { + + public void requestSent() { + HttpLog.v("LoggingEventHandler:requestSent()"); + } + + public void status(int major_version, + int minor_version, + int code, /* Status-Code value */ + String reason_phrase) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler:status() major: " + major_version + + " minor: " + minor_version + + " code: " + code + + " reason: " + reason_phrase); + } + } + + public void headers(Headers headers) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler:headers()"); + HttpLog.v(headers.toString()); + } + } + + public void locationChanged(String newLocation, boolean permanent) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: locationChanged() " + newLocation + + " permanent " + permanent); + } + } + + public void data(byte[] data, int len) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: data() " + len + " bytes"); + } + // HttpLog.v(new String(data, 0, len)); + } + public void endData() { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: endData() called"); + } + } + + public void certificate(SslCertificate certificate) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: certificate(): " + certificate); + } + } + + public void error(int id, String description) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: error() called Id:" + id + + " description " + description); + } + } + + public void handleSslErrorRequest(SslError error) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: handleSslErrorRequest():" + error); + } + } +} diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java new file mode 100644 index 0000000..bcbecf0 --- /dev/null +++ b/core/java/android/net/http/Request.java @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2006 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 android.net.http; + +import java.io.InputStream; +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.zip.GZIPInputStream; + +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.Header; +import org.apache.http.HttpClientConnection; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ParseException; +import org.apache.http.ProtocolVersion; + +import org.apache.http.StatusLine; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.message.BasicHttpEntityEnclosingRequest; +import org.apache.http.protocol.RequestContent; + +/** + * Represents an HTTP request for a given host. + * + * {@hide} + */ + +class Request { + + /** The eventhandler to call as the request progresses */ + EventHandler mEventHandler; + + private Connection mConnection; + + /** The Apache http request */ + BasicHttpRequest mHttpRequest; + + /** The path component of this request */ + String mPath; + + /** Host serving this request */ + HttpHost mHost; + + /** Set if I'm using a proxy server */ + HttpHost mProxyHost; + + /** True if request is .html, .js, .css */ + boolean mHighPriority; + + /** True if request has been cancelled */ + volatile boolean mCancelled = false; + + int mFailCount = 0; + + private InputStream mBodyProvider; + private int mBodyLength; + + private final static String HOST_HEADER = "Host"; + private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; + private final static String CONTENT_LENGTH_HEADER = "content-length"; + + /* Used to synchronize waitUntilComplete() requests */ + private final Object mClientResource = new Object(); + + /** + * Processor used to set content-length and transfer-encoding + * headers. + */ + private static RequestContent requestContentProcessor = + new RequestContent(); + + /** + * Instantiates a new Request. + * @param method GET/POST/PUT + * @param host The server that will handle this request + * @param path path part of URI + * @param bodyProvider InputStream providing HTTP body, null if none + * @param bodyLength length of body, must be 0 if bodyProvider is null + * @param eventHandler request will make progress callbacks on + * this interface + * @param headers reqeust headers + * @param highPriority true for .html, css, .cs + */ + Request(String method, HttpHost host, HttpHost proxyHost, String path, + InputStream bodyProvider, int bodyLength, + EventHandler eventHandler, + Map<String, String> headers, boolean highPriority) { + mEventHandler = eventHandler; + mHost = host; + mProxyHost = proxyHost; + mPath = path; + mHighPriority = highPriority; + mBodyProvider = bodyProvider; + mBodyLength = bodyLength; + + if (bodyProvider == null) { + mHttpRequest = new BasicHttpRequest(method, getUri()); + } else { + mHttpRequest = new BasicHttpEntityEnclosingRequest( + method, getUri()); + setBodyProvider(bodyProvider, bodyLength); + } + addHeader(HOST_HEADER, getHostPort()); + + /* FIXME: if webcore will make the root document a + high-priority request, we can ask for gzip encoding only on + high priority reqs (saving the trouble for images, etc) */ + addHeader(ACCEPT_ENCODING_HEADER, "gzip"); + addHeaders(headers); + } + + /** + * @param connection Request served by this connection + */ + void setConnection(Connection connection) { + mConnection = connection; + } + + /* package */ EventHandler getEventHandler() { + return mEventHandler; + } + + /** + * Add header represented by given pair to request. Header will + * be formatted in request as "name: value\r\n". + * @param name of header + * @param value of header + */ + void addHeader(String name, String value) { + if (name == null) { + String damage = "Null http header name"; + HttpLog.e(damage); + throw new NullPointerException(damage); + } + if (value == null || value.length() == 0) { + String damage = "Null or empty value for header \"" + name + "\""; + HttpLog.e(damage); + throw new RuntimeException(damage); + } + mHttpRequest.addHeader(name, value); + } + + /** + * Add all headers in given map to this request. This is a helper + * method: it calls addHeader for each pair in the map. + */ + void addHeaders(Map<String, String> headers) { + if (headers == null) { + return; + } + + Entry<String, String> entry; + Iterator<Entry<String, String>> i = headers.entrySet().iterator(); + while (i.hasNext()) { + entry = i.next(); + addHeader(entry.getKey(), entry.getValue()); + } + } + + /** + * Send the request line and headers + */ + void sendRequest(AndroidHttpClientConnection httpClientConnection) + throws HttpException, IOException { + + if (mCancelled) return; // don't send cancelled requests + + if (HttpLog.LOGV) { + HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort()); + // HttpLog.v(mHttpRequest.getRequestLine().toString()); + if (false) { + Iterator i = mHttpRequest.headerIterator(); + while (i.hasNext()) { + Header header = (Header)i.next(); + HttpLog.v(header.getName() + ": " + header.getValue()); + } + } + } + + requestContentProcessor.process(mHttpRequest, + mConnection.getHttpContext()); + httpClientConnection.sendRequestHeader(mHttpRequest); + if (mHttpRequest instanceof HttpEntityEnclosingRequest) { + httpClientConnection.sendRequestEntity( + (HttpEntityEnclosingRequest) mHttpRequest); + } + + if (HttpLog.LOGV) { + HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath); + } + } + + + /** + * Receive a single http response. + * + * @param httpClientConnection the request to receive the response for. + */ + void readResponse(AndroidHttpClientConnection httpClientConnection) + throws IOException, ParseException { + + if (mCancelled) return; // don't send cancelled requests + + StatusLine statusLine = null; + boolean hasBody = false; + boolean reuse = false; + httpClientConnection.flush(); + int statusCode = 0; + + Headers header = new Headers(); + do { + statusLine = httpClientConnection.parseResponseHeader(header); + statusCode = statusLine.getStatusCode(); + } while (statusCode < HttpStatus.SC_OK); + if (HttpLog.LOGV) HttpLog.v( + "Request.readResponseStatus() " + + statusLine.toString().length() + " " + statusLine); + + ProtocolVersion v = statusLine.getProtocolVersion(); + mEventHandler.status(v.getMajor(), v.getMinor(), + statusCode, statusLine.getReasonPhrase()); + mEventHandler.headers(header); + HttpEntity entity = null; + hasBody = canResponseHaveBody(mHttpRequest, statusCode); + + if (hasBody) + entity = httpClientConnection.receiveResponseEntity(header); + + if (entity != null) { + InputStream is = entity.getContent(); + + // process gzip content encoding + Header contentEncoding = entity.getContentEncoding(); + InputStream nis = null; + try { + if (contentEncoding != null && + contentEncoding.getValue().equals("gzip")) { + nis = new GZIPInputStream(is); + } else { + nis = is; + } + + /* accumulate enough data to make it worth pushing it + * up the stack */ + byte[] buf = mConnection.getBuf(); + int len = 0; + int count = 0; + int lowWater = buf.length / 2; + while (len != -1) { + len = nis.read(buf, count, buf.length - count); + if (len != -1) { + count += len; + } + if (len == -1 || count >= lowWater) { + if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count); + mEventHandler.data(buf, count); + count = 0; + } + } + } catch(IOException e) { + // don't throw if we have a non-OK status code + if (statusCode == HttpStatus.SC_OK) { + throw e; + } + } finally { + if (nis != null) { + nis.close(); + } + } + } + mConnection.setCanPersist(entity, statusLine.getProtocolVersion(), + header.getConnectionType()); + mEventHandler.endData(); + complete(); + + if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " + + mHost.getSchemeName() + "://" + getHostPort() + mPath); + } + + /** + * Data will not be sent to or received from server after cancel() + * call. Does not close connection--use close() below for that. + * + * Called by RequestHandle from non-network thread + */ + void cancel() { + if (HttpLog.LOGV) { + HttpLog.v("Request.cancel(): " + getUri()); + } + mCancelled = true; + if (mConnection != null) { + mConnection.cancel(); + } + } + + String getHostPort() { + String myScheme = mHost.getSchemeName(); + int myPort = mHost.getPort(); + + // Only send port when we must... many servers can't deal with it + if (myPort != 80 && myScheme.equals("http") || + myPort != 443 && myScheme.equals("https")) { + return mHost.toHostString(); + } else { + return mHost.getHostName(); + } + } + + String getUri() { + if (mProxyHost == null || + mHost.getSchemeName().equals("https")) { + return mPath; + } + return mHost.getSchemeName() + "://" + getHostPort() + mPath; + } + + /** + * for debugging + */ + public String toString() { + return (mHighPriority ? "P*" : "") + mPath; + } + + + /** + * If this request has been sent once and failed, it must be reset + * before it can be sent again. + */ + void reset() { + /* clear content-length header */ + mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER); + + if (mBodyProvider != null) { + try { + mBodyProvider.reset(); + } catch (IOException ex) { + if (HttpLog.LOGV) HttpLog.v( + "failed to reset body provider " + + getUri()); + } + setBodyProvider(mBodyProvider, mBodyLength); + } + } + + /** + * Pause thread request completes. Used for synchronous requests, + * and testing + */ + void waitUntilComplete() { + synchronized (mClientResource) { + try { + if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()"); + mClientResource.wait(); + if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting"); + } catch (InterruptedException e) { + } + } + } + + void complete() { + synchronized (mClientResource) { + mClientResource.notifyAll(); + } + } + + /** + * Decide whether a response comes with an entity. + * The implementation in this class is based on RFC 2616. + * Unknown methods and response codes are supposed to + * indicate responses with an entity. + * <br/> + * Derived executors can override this method to handle + * methods and response codes not specified in RFC 2616. + * + * @param request the request, to obtain the executed method + * @param response the response, to obtain the status code + */ + + private static boolean canResponseHaveBody(final HttpRequest request, + final int status) { + + if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { + return false; + } + return status >= HttpStatus.SC_OK + && status != HttpStatus.SC_NO_CONTENT + && status != HttpStatus.SC_NOT_MODIFIED + && status != HttpStatus.SC_RESET_CONTENT; + } + + /** + * Supply an InputStream that provides the body of a request. It's + * not great that the caller must also provide the length of the data + * returned by that InputStream, but the client needs to know up + * front, and I'm not sure how to get this out of the InputStream + * itself without a costly readthrough. I'm not sure skip() would + * do what we want. If you know a better way, please let me know. + */ + private void setBodyProvider(InputStream bodyProvider, int bodyLength) { + if (!bodyProvider.markSupported()) { + throw new IllegalArgumentException( + "bodyProvider must support mark()"); + } + // Mark beginning of stream + bodyProvider.mark(Integer.MAX_VALUE); + + ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity( + new InputStreamEntity(bodyProvider, bodyLength)); + } + + + /** + * Handles SSL error(s) on the way down from the user (the user + * has already provided their feedback). + */ + public void handleSslErrorResponse(boolean proceed) { + HttpsConnection connection = (HttpsConnection)(mConnection); + if (connection != null) { + connection.restartConnection(proceed); + } + } + + /** + * Helper: calls error() on eventhandler with appropriate message + * This should not be called before the mConnection is set. + */ + void error(int errorId, int resourceId) { + mEventHandler.error( + errorId, + mConnection.mContext.getText( + resourceId).toString()); + } + +} diff --git a/core/java/android/net/http/RequestFeeder.java b/core/java/android/net/http/RequestFeeder.java new file mode 100644 index 0000000..34ca267 --- /dev/null +++ b/core/java/android/net/http/RequestFeeder.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 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. + * 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. + */ + +/** + * Supplies Requests to a Connection + */ + +package android.net.http; + +import org.apache.http.HttpHost; + +/** + * {@hide} + */ +interface RequestFeeder { + + Request getRequest(); + Request getRequest(HttpHost host); + + /** + * @return true if a request for this host is available + */ + boolean haveRequest(HttpHost host); + + /** + * Put request back on head of queue + */ + void requeueRequest(Request request); +} diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java new file mode 100644 index 0000000..5d81250 --- /dev/null +++ b/core/java/android/net/http/RequestHandle.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2006 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 android.net.http; + +import android.net.ParseException; +import android.net.WebAddress; +import android.security.Md5MessageDigest; +import junit.framework.Assert; +import android.webkit.CookieManager; + +import org.apache.commons.codec.binary.Base64; + +import java.io.InputStream; +import java.lang.Math; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * RequestHandle: handles a request session that may include multiple + * redirects, HTTP authentication requests, etc. + * + * {@hide} + */ +public class RequestHandle { + + private String mUrl; + private WebAddress mUri; + private String mMethod; + private Map<String, String> mHeaders; + + private RequestQueue mRequestQueue; + + private Request mRequest; + + private InputStream mBodyProvider; + private int mBodyLength; + + private int mRedirectCount = 0; + + private final static String AUTHORIZATION_HEADER = "Authorization"; + private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; + + private final static int MAX_REDIRECT_COUNT = 16; + + /** + * Creates a new request session. + */ + public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, + String method, Map<String, String> headers, + InputStream bodyProvider, int bodyLength, Request request) { + + if (headers == null) { + headers = new HashMap<String, String>(); + } + mHeaders = headers; + mBodyProvider = bodyProvider; + mBodyLength = bodyLength; + mMethod = method == null? "GET" : method; + + mUrl = url; + mUri = uri; + + mRequestQueue = requestQueue; + + mRequest = request; + } + + /** + * Cancels this request + */ + public void cancel() { + if (mRequest != null) { + mRequest.cancel(); + } + } + + /** + * Handles SSL error(s) on the way down from the user (the user + * has already provided their feedback). + */ + public void handleSslErrorResponse(boolean proceed) { + if (mRequest != null) { + mRequest.handleSslErrorResponse(proceed); + } + } + + /** + * @return true if we've hit the max redirect count + */ + public boolean isRedirectMax() { + return mRedirectCount >= MAX_REDIRECT_COUNT; + } + + /** + * Create and queue a redirect request. + * + * @param redirectTo URL to redirect to + * @param statusCode HTTP status code returned from original request + * @param cacheHeaders Cache header for redirect URL + * @return true if setup succeeds, false otherwise (redirect loop + * count exceeded) + */ + public boolean setupRedirect(String redirectTo, int statusCode, + Map<String, String> cacheHeaders) { + if (HttpLog.LOGV) { + HttpLog.v("RequestHandle.setupRedirect(): redirectCount " + + mRedirectCount); + } + + // be careful and remove authentication headers, if any + mHeaders.remove(AUTHORIZATION_HEADER); + mHeaders.remove(PROXY_AUTHORIZATION_HEADER); + + if (++mRedirectCount == MAX_REDIRECT_COUNT) { + // Way too many redirects -- fail out + if (HttpLog.LOGV) HttpLog.v( + "RequestHandle.setupRedirect(): too many redirects " + + mRequest); + mRequest.error(EventHandler.ERROR_REDIRECT_LOOP, + com.android.internal.R.string.httpErrorRedirectLoop); + return false; + } + + if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) { + // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3 + if (HttpLog.LOGV) { + HttpLog.v("blowing away the referer on an https -> http redirect"); + } + mHeaders.remove("Referer"); + } + + mUrl = redirectTo; + try { + mUri = new WebAddress(mUrl); + } catch (ParseException e) { + e.printStackTrace(); + } + + // update the "cookie" header based on the redirected url + mHeaders.remove("cookie"); + String cookie = CookieManager.getInstance().getCookie(mUri); + if (cookie != null && cookie.length() > 0) { + mHeaders.put("cookie", cookie); + } + + if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) { + if (HttpLog.LOGV) { + HttpLog.v("replacing POST with GET on redirect to " + redirectTo); + } + mMethod = "GET"; + } + mHeaders.remove("Content-Type"); + mBodyProvider = null; + + // Update the cache headers for this URL + mHeaders.putAll(cacheHeaders); + + createAndQueueNewRequest(); + return true; + } + + /** + * Create and queue an HTTP authentication-response (basic) request. + */ + public void setupBasicAuthResponse(boolean isProxy, String username, String password) { + String response = computeBasicAuthResponse(username, password); + if (HttpLog.LOGV) { + HttpLog.v("setupBasicAuthResponse(): response: " + response); + } + mHeaders.put(authorizationHeader(isProxy), "Basic " + response); + setupAuthResponse(); + } + + /** + * Create and queue an HTTP authentication-response (digest) request. + */ + public void setupDigestAuthResponse(boolean isProxy, + String username, + String password, + String realm, + String nonce, + String QOP, + String algorithm, + String opaque) { + + String response = computeDigestAuthResponse( + username, password, realm, nonce, QOP, algorithm, opaque); + if (HttpLog.LOGV) { + HttpLog.v("setupDigestAuthResponse(): response: " + response); + } + mHeaders.put(authorizationHeader(isProxy), "Digest " + response); + setupAuthResponse(); + } + + private void setupAuthResponse() { + try { + if (mBodyProvider != null) mBodyProvider.reset(); + } catch (java.io.IOException ex) { + if (HttpLog.LOGV) { + HttpLog.v("setupAuthResponse() failed to reset body provider"); + } + } + createAndQueueNewRequest(); + } + + /** + * @return HTTP request method (GET, PUT, etc). + */ + public String getMethod() { + return mMethod; + } + + /** + * @return Basic-scheme authentication response: BASE64(username:password). + */ + public static String computeBasicAuthResponse(String username, String password) { + Assert.assertNotNull(username); + Assert.assertNotNull(password); + + // encode username:password to base64 + return new String(Base64.encodeBase64((username + ':' + password).getBytes())); + } + + public void waitUntilComplete() { + mRequest.waitUntilComplete(); + } + + /** + * @return Digest-scheme authentication response. + */ + private String computeDigestAuthResponse(String username, + String password, + String realm, + String nonce, + String QOP, + String algorithm, + String opaque) { + + Assert.assertNotNull(username); + Assert.assertNotNull(password); + Assert.assertNotNull(realm); + + String A1 = username + ":" + realm + ":" + password; + String A2 = mMethod + ":" + mUrl; + + // because we do not preemptively send authorization headers, nc is always 1 + String nc = "000001"; + String cnonce = computeCnonce(); + String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce); + + String response = ""; + response += "username=" + doubleQuote(username) + ", "; + response += "realm=" + doubleQuote(realm) + ", "; + response += "nonce=" + doubleQuote(nonce) + ", "; + response += "uri=" + doubleQuote(mUrl) + ", "; + response += "response=" + doubleQuote(digest) ; + + if (opaque != null) { + response += ", opaque=" + doubleQuote(opaque); + } + + if (algorithm != null) { + response += ", algorithm=" + algorithm; + } + + if (QOP != null) { + response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce); + } + + return response; + } + + /** + * @return The right authorization header (dependeing on whether it is a proxy or not). + */ + public static String authorizationHeader(boolean isProxy) { + if (!isProxy) { + return AUTHORIZATION_HEADER; + } else { + return PROXY_AUTHORIZATION_HEADER; + } + } + + /** + * @return Double-quoted MD5 digest. + */ + private String computeDigest( + String A1, String A2, String nonce, String QOP, String nc, String cnonce) { + if (HttpLog.LOGV) { + HttpLog.v("computeDigest(): QOP: " + QOP); + } + + if (QOP == null) { + return KD(H(A1), nonce + ":" + H(A2)); + } else { + if (QOP.equalsIgnoreCase("auth")) { + return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2)); + } + } + + return null; + } + + /** + * @return MD5 hash of concat(secret, ":", data). + */ + private String KD(String secret, String data) { + return H(secret + ":" + data); + } + + /** + * @return MD5 hash of param. + */ + private String H(String param) { + if (param != null) { + Md5MessageDigest md5 = new Md5MessageDigest(); + + byte[] d = md5.digest(param.getBytes()); + if (d != null) { + return bufferToHex(d); + } + } + + return null; + } + + /** + * @return HEX buffer representation. + */ + private String bufferToHex(byte[] buffer) { + final char hexChars[] = + { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + + if (buffer != null) { + int length = buffer.length; + if (length > 0) { + StringBuilder hex = new StringBuilder(2 * length); + + for (int i = 0; i < length; ++i) { + byte l = (byte) (buffer[i] & 0x0F); + byte h = (byte)((buffer[i] & 0xF0) >> 4); + + hex.append(hexChars[h]); + hex.append(hexChars[l]); + } + + return hex.toString(); + } else { + return ""; + } + } + + return null; + } + + /** + * Computes a random cnonce value based on the current time. + */ + private String computeCnonce() { + Random rand = new Random(); + int nextInt = rand.nextInt(); + nextInt = (nextInt == Integer.MIN_VALUE) ? + Integer.MAX_VALUE : Math.abs(nextInt); + return Integer.toString(nextInt, 16); + } + + /** + * "Double-quotes" the argument. + */ + private String doubleQuote(String param) { + if (param != null) { + return "\"" + param + "\""; + } + + return null; + } + + /** + * Creates and queues new request. + */ + private void createAndQueueNewRequest() { + mRequest = mRequestQueue.queueRequest( + mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, + mBodyProvider, + mBodyLength, mRequest.mHighPriority).mRequest; + } +} diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java new file mode 100644 index 0000000..d592995 --- /dev/null +++ b/core/java/android/net/http/RequestQueue.java @@ -0,0 +1,647 @@ +/* + * Copyright (C) 2006 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. + */ + +/** + * High level HTTP Interface + * Queues requests as necessary + */ + +package android.net.http; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkConnectivityListener; +import android.net.NetworkInfo; +import android.net.Proxy; +import android.net.WebAddress; +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.ListIterator; +import java.util.Map; + +import org.apache.http.HttpHost; + +/** + * {@hide} + */ +public class RequestQueue implements RequestFeeder { + + private Context mContext; + + /** + * Requests, indexed by HttpHost (scheme, host, port) + */ + private LinkedHashMap<HttpHost, LinkedList<Request>> mPending; + + /* Support for notifying a client when queue is empty */ + private boolean mClientWaiting = false; + + /** true if connected */ + boolean mNetworkConnected = true; + + private HttpHost mProxyHost = null; + private BroadcastReceiver mProxyChangeReceiver; + + private ActivePool mActivePool; + + /* default simultaneous connection count */ + private static final int CONNECTION_COUNT = 4; + + /** + * This intent broadcast when http is paused or unpaused due to + * net availability toggling + */ + public final static String HTTP_NETWORK_STATE_CHANGED_INTENT = + "android.net.http.NETWORK_STATE"; + public final static String HTTP_NETWORK_STATE_UP = "up"; + + /** + * Listen to platform network state. On a change, + * (1) kick stack on or off as appropriate + * (2) send an intent to my host app telling + * it what I've done + */ + private NetworkStateTracker mNetworkStateTracker; + class NetworkStateTracker { + + final static int EVENT_DATA_STATE_CHANGED = 100; + + Context mContext; + NetworkConnectivityListener mConnectivityListener; + NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED; + int mCurrentNetworkType; + + NetworkStateTracker(Context context) { + mContext = context; + } + + /** + * register for updates + */ + protected void enable() { + if (mConnectivityListener == null) { + /* + * Initializing the network type is really unnecessary, + * since as soon as we register with the NCL, we'll + * get a CONNECTED event for the active network, and + * we'll configure the HTTP proxy accordingly. However, + * as a fallback in case that doesn't happen for some + * reason, initializing to type WIFI would mean that + * we'd start out without a proxy. This seems better + * than thinking we have a proxy (which is probably + * private to the carrier network and therefore + * unreachable outside of that network) when we really + * shouldn't. + */ + mCurrentNetworkType = ConnectivityManager.TYPE_WIFI; + mConnectivityListener = new NetworkConnectivityListener(); + mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED); + mConnectivityListener.startListening(mContext); + } + } + + protected void disable() { + if (mConnectivityListener != null) { + mConnectivityListener.unregisterHandler(mHandler); + mConnectivityListener.stopListening(); + mConnectivityListener = null; + } + } + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_DATA_STATE_CHANGED: + networkStateChanged(); + break; + } + } + }; + + int getCurrentNetworkType() { + return mCurrentNetworkType; + } + + void networkStateChanged() { + if (mConnectivityListener == null) + return; + + + NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState(); + NetworkInfo info = mConnectivityListener.getNetworkInfo(); + if (info == null) { + /** + * We've been seeing occasional NPEs here. I believe recent changes + * have made this impossible, but in the interest of being totally + * paranoid, check and log this here. + */ + HttpLog.v("NetworkStateTracker: connectivity broadcast" + + " has null network info - ignoring"); + return; + } + NetworkInfo.State state = info.getState(); + + if (HttpLog.LOGV) { + HttpLog.v("NetworkStateTracker " + info.getTypeName() + + " state= " + state + " last= " + mLastNetworkState + + " connectivityState= " + connectivityState.toString()); + } + + boolean newConnection = + state != mLastNetworkState && state == NetworkInfo.State.CONNECTED; + + if (state == NetworkInfo.State.CONNECTED) { + mCurrentNetworkType = info.getType(); + setProxyConfig(); + } + + mLastNetworkState = state; + if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) { + setNetworkState(false); + broadcastState(false); + } else if (newConnection) { + setNetworkState(true); + broadcastState(true); + } + + } + + void broadcastState(boolean connected) { + Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT); + intent.putExtra(HTTP_NETWORK_STATE_UP, connected); + mContext.sendBroadcast(intent); + } + } + + /** + * This class maintains active connection threads + */ + class ActivePool implements ConnectionManager { + /** Threads used to process requests */ + ConnectionThread[] mThreads; + + IdleCache mIdleCache; + + private int mTotalRequest; + private int mTotalConnection; + private int mConnectionCount; + + ActivePool(int connectionCount) { + mIdleCache = new IdleCache(); + mConnectionCount = connectionCount; + mThreads = new ConnectionThread[mConnectionCount]; + + for (int i = 0; i < mConnectionCount; i++) { + mThreads[i] = new ConnectionThread( + mContext, i, this, RequestQueue.this); + } + } + + void startup() { + for (int i = 0; i < mConnectionCount; i++) { + mThreads[i].start(); + } + } + + void shutdown() { + for (int i = 0; i < mConnectionCount; i++) { + mThreads[i].requestStop(); + } + } + + public boolean isNetworkConnected() { + return mNetworkConnected; + } + + void startConnectionThread() { + synchronized (RequestQueue.this) { + RequestQueue.this.notify(); + } + } + + public void startTiming() { + for (int i = 0; i < mConnectionCount; i++) { + mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime; + } + mTotalRequest = 0; + mTotalConnection = 0; + } + + public void stopTiming() { + int totalTime = 0; + for (int i = 0; i < mConnectionCount; i++) { + ConnectionThread rt = mThreads[i]; + totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime); + rt.mStartThreadTime = -1; + } + Log.d("Http", "Http thread used " + totalTime + " ms " + " for " + + mTotalRequest + " requests and " + mTotalConnection + + " connections"); + } + + void logState() { + StringBuilder dump = new StringBuilder(); + for (int i = 0; i < mConnectionCount; i++) { + dump.append(mThreads[i] + "\n"); + } + HttpLog.v(dump.toString()); + } + + + public HttpHost getProxyHost() { + return mProxyHost; + } + + /** + * Turns off persistence on all live connections + */ + void disablePersistence() { + for (int i = 0; i < mConnectionCount; i++) { + Connection connection = mThreads[i].mConnection; + if (connection != null) connection.setCanPersist(false); + } + mIdleCache.clear(); + } + + /* Linear lookup -- okay for small thread counts. Might use + private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap; + if this turns out to be a hotspot */ + ConnectionThread getThread(HttpHost host) { + synchronized(RequestQueue.this) { + for (int i = 0; i < mThreads.length; i++) { + ConnectionThread ct = mThreads[i]; + Connection connection = ct.mConnection; + if (connection != null && connection.mHost.equals(host)) { + return ct; + } + } + } + return null; + } + + public Connection getConnection(Context context, HttpHost host) { + Connection con = mIdleCache.getConnection(host); + if (con == null) { + mTotalConnection++; + con = Connection.getConnection( + mContext, host, this, RequestQueue.this); + } + return con; + } + public boolean recycleConnection(HttpHost host, Connection connection) { + return mIdleCache.cacheConnection(host, connection); + } + + } + + /** + * A RequestQueue class instance maintains a set of queued + * requests. It orders them, makes the requests against HTTP + * servers, and makes callbacks to supplied eventHandlers as data + * is read. It supports request prioritization, connection reuse + * and pipelining. + * + * @param context application context + */ + public RequestQueue(Context context) { + this(context, CONNECTION_COUNT); + } + + /** + * A RequestQueue class instance maintains a set of queued + * requests. It orders them, makes the requests against HTTP + * servers, and makes callbacks to supplied eventHandlers as data + * is read. It supports request prioritization, connection reuse + * and pipelining. + * + * @param context application context + * @param connectionCount The number of simultaneous connections + */ + public RequestQueue(Context context, int connectionCount) { + mContext = context; + + mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32); + + mActivePool = new ActivePool(connectionCount); + mActivePool.startup(); + } + + /** + * Enables data state and proxy tracking + */ + public synchronized void enablePlatformNotifications() { + if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network"); + + if (mProxyChangeReceiver == null) { + mProxyChangeReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context ctx, Intent intent) { + setProxyConfig(); + } + }; + mContext.registerReceiver(mProxyChangeReceiver, + new IntentFilter(Proxy.PROXY_CHANGE_ACTION)); + } + + /* Network state notification is broken on the simulator + don't register for notifications on SIM */ + String device = SystemProperties.get("ro.product.device"); + boolean simulation = TextUtils.isEmpty(device); + + if (!simulation) { + if (mNetworkStateTracker == null) { + mNetworkStateTracker = new NetworkStateTracker(mContext); + } + mNetworkStateTracker.enable(); + } + } + + /** + * If platform notifications have been enabled, call this method + * to disable before destroying RequestQueue + */ + public synchronized void disablePlatformNotifications() { + if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); + + if (mNetworkStateTracker != null) { + mNetworkStateTracker.disable(); + } + + if (mProxyChangeReceiver != null) { + mContext.unregisterReceiver(mProxyChangeReceiver); + mProxyChangeReceiver = null; + } + } + + /** + * Because our IntentReceiver can run within a different thread, + * synchronize setting the proxy + */ + private synchronized void setProxyConfig() { + if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) { + mProxyHost = null; + } else { + String host = Proxy.getHost(mContext); + if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host); + if (host == null) { + mProxyHost = null; + } else { + mActivePool.disablePersistence(); + mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http"); + } + } + } + + /** + * used by webkit + * @return proxy host if set, null otherwise + */ + public HttpHost getProxyHost() { + return mProxyHost; + } + + /** + * Queues an HTTP request + * @param url The url to load. + * @param method "GET" or "POST." + * @param headers A hashmap of http headers. + * @param eventHandler The event handler for handling returned + * data. Callbacks will be made on the supplied instance. + * @param bodyProvider InputStream providing HTTP body, null if none + * @param bodyLength length of body, must be 0 if bodyProvider is null + * @param highPriority If true, queues before low priority + * requests if possible + */ + public RequestHandle queueRequest( + String url, String method, + Map<String, String> headers, EventHandler eventHandler, + InputStream bodyProvider, int bodyLength, boolean highPriority) { + WebAddress uri = new WebAddress(url); + return queueRequest(url, uri, method, headers, eventHandler, + bodyProvider, bodyLength, highPriority); + } + + /** + * Queues an HTTP request + * @param url The url to load. + * @param uri The uri of the url to load. + * @param method "GET" or "POST." + * @param headers A hashmap of http headers. + * @param eventHandler The event handler for handling returned + * data. Callbacks will be made on the supplied instance. + * @param bodyProvider InputStream providing HTTP body, null if none + * @param bodyLength length of body, must be 0 if bodyProvider is null + * @param highPriority If true, queues before low priority + * requests if possible + */ + public RequestHandle queueRequest( + String url, WebAddress uri, String method, Map<String, String> headers, + EventHandler eventHandler, + InputStream bodyProvider, int bodyLength, + boolean highPriority) { + + if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); + + // Ensure there is an eventHandler set + if (eventHandler == null) { + eventHandler = new LoggingEventHandler(); + } + + /* Create and queue request */ + Request req; + HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); + + // set up request + req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider, + bodyLength, eventHandler, headers, highPriority); + + queueRequest(req, highPriority); + + mActivePool.mTotalRequest++; + + // dump(); + mActivePool.startConnectionThread(); + + return new RequestHandle( + this, url, uri, method, headers, bodyProvider, bodyLength, + req); + } + + /** + * Called by the NetworkStateTracker -- updates when network connectivity + * is lost/restored. + * + * If isNetworkConnected is true, start processing requests + */ + public void setNetworkState(boolean isNetworkConnected) { + if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected); + mNetworkConnected = isNetworkConnected; + if (isNetworkConnected) + mActivePool.startConnectionThread(); + } + + /** + * @return true iff there are any non-active requests pending + */ + synchronized boolean requestsPending() { + return !mPending.isEmpty(); + } + + + /** + * debug tool: prints request queue to log + */ + synchronized void dump() { + HttpLog.v("dump()"); + StringBuilder dump = new StringBuilder(); + int count = 0; + Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter; + + // mActivePool.log(dump); + + if (!mPending.isEmpty()) { + iter = mPending.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); + String hostName = entry.getKey().getHostName(); + StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); + + LinkedList<Request> reqList = entry.getValue(); + ListIterator reqIter = reqList.listIterator(0); + while (iter.hasNext()) { + Request request = (Request)iter.next(); + line.append(request + " "); + } + dump.append(line); + dump.append("\n"); + } + } + HttpLog.v(dump.toString()); + } + + /* + * RequestFeeder implementation + */ + public synchronized Request getRequest() { + Request ret = null; + + if (mNetworkConnected && !mPending.isEmpty()) { + ret = removeFirst(mPending); + } + if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); + return ret; + } + + /** + * @return a request for given host if possible + */ + public synchronized Request getRequest(HttpHost host) { + Request ret = null; + + if (mNetworkConnected && mPending.containsKey(host)) { + LinkedList<Request> reqList = mPending.get(host); + ret = reqList.removeFirst(); + if (reqList.isEmpty()) { + mPending.remove(host); + } + } + if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret); + return ret; + } + + /** + * @return true if a request for this host is available + */ + public synchronized boolean haveRequest(HttpHost host) { + return mPending.containsKey(host); + } + + /** + * Put request back on head of queue + */ + public void requeueRequest(Request request) { + queueRequest(request, true); + } + + /** + * This must be called to cleanly shutdown RequestQueue + */ + public void shutdown() { + mActivePool.shutdown(); + } + + protected synchronized void queueRequest(Request request, boolean head) { + HttpHost host = request.mHost; + LinkedList<Request> reqList; + if (mPending.containsKey(host)) { + reqList = mPending.get(host); + } else { + reqList = new LinkedList<Request>(); + mPending.put(host, reqList); + } + if (head) { + reqList.addFirst(request); + } else { + reqList.add(request); + } + } + + + public void startTiming() { + mActivePool.startTiming(); + } + + public void stopTiming() { + mActivePool.stopTiming(); + } + + /* helper */ + private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) { + Request ret = null; + Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator(); + if (iter.hasNext()) { + Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); + LinkedList<Request> reqList = entry.getValue(); + ret = reqList.removeFirst(); + if (reqList.isEmpty()) { + requestQueue.remove(entry.getKey()); + } + } + return ret; + } + + /** + * This interface is exposed to each connection + */ + interface ConnectionManager { + boolean isNetworkConnected(); + HttpHost getProxyHost(); + Connection getConnection(Context context, HttpHost host); + boolean recycleConnection(HttpHost host, Connection connection); + } +} diff --git a/core/java/android/net/http/SslCertificate.java b/core/java/android/net/http/SslCertificate.java new file mode 100644 index 0000000..46b2bee --- /dev/null +++ b/core/java/android/net/http/SslCertificate.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2006 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 android.net.http; + +import android.os.Bundle; + +import java.text.DateFormat; +import java.util.Vector; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.DERObjectIdentifier; +import org.bouncycastle.asn1.x509.X509Name; + +/** + * SSL certificate info (certificate details) class + */ +public class SslCertificate { + + /** + * Name of the entity this certificate is issued to + */ + private DName mIssuedTo; + + /** + * Name of the entity this certificate is issued by + */ + private DName mIssuedBy; + + /** + * Not-before date from the validity period + */ + private String mValidNotBefore; + + /** + * Not-after date from the validity period + */ + private String mValidNotAfter; + + /** + * Bundle key names + */ + private static final String ISSUED_TO = "issued-to"; + private static final String ISSUED_BY = "issued-by"; + private static final String VALID_NOT_BEFORE = "valid-not-before"; + private static final String VALID_NOT_AFTER = "valid-not-after"; + + /** + * Saves the certificate state to a bundle + * @param certificate The SSL certificate to store + * @return A bundle with the certificate stored in it or null if fails + */ + public static Bundle saveState(SslCertificate certificate) { + Bundle bundle = null; + + if (certificate != null) { + bundle = new Bundle(); + + bundle.putString(ISSUED_TO, certificate.getIssuedTo().getDName()); + bundle.putString(ISSUED_BY, certificate.getIssuedBy().getDName()); + + bundle.putString(VALID_NOT_BEFORE, certificate.getValidNotBefore()); + bundle.putString(VALID_NOT_AFTER, certificate.getValidNotAfter()); + } + + return bundle; + } + + /** + * Restores the certificate stored in the bundle + * @param bundle The bundle with the certificate state stored in it + * @return The SSL certificate stored in the bundle or null if fails + */ + public static SslCertificate restoreState(Bundle bundle) { + if (bundle != null) { + return new SslCertificate( + bundle.getString(ISSUED_TO), + bundle.getString(ISSUED_BY), + bundle.getString(VALID_NOT_BEFORE), + bundle.getString(VALID_NOT_AFTER)); + } + + return null; + } + + /** + * Creates a new SSL certificate object + * @param issuedTo The entity this certificate is issued to + * @param issuedBy The entity that issued this certificate + * @param validNotBefore The not-before date from the certificate validity period + * @param validNotAfter The not-after date from the certificate validity period + */ + public SslCertificate( + String issuedTo, String issuedBy, String validNotBefore, String validNotAfter) { + mIssuedTo = new DName(issuedTo); + mIssuedBy = new DName(issuedBy); + + mValidNotBefore = validNotBefore; + mValidNotAfter = validNotAfter; + } + + /** + * Creates a new SSL certificate object from an X509 certificate + * @param certificate X509 certificate + */ + public SslCertificate(X509Certificate certificate) { + this(certificate.getSubjectDN().getName(), + certificate.getIssuerDN().getName(), + DateFormat.getInstance().format(certificate.getNotBefore()), + DateFormat.getInstance().format(certificate.getNotAfter())); + } + + /** + * @return Not-before date from the certificate validity period or + * "" if none has been set + */ + public String getValidNotBefore() { + return mValidNotBefore != null ? mValidNotBefore : ""; + } + + /** + * @return Not-after date from the certificate validity period or + * "" if none has been set + */ + public String getValidNotAfter() { + return mValidNotAfter != null ? mValidNotAfter : ""; + } + + /** + * @return Issued-to distinguished name or null if none has been set + */ + public DName getIssuedTo() { + return mIssuedTo; + } + + /** + * @return Issued-by distinguished name or null if none has been set + */ + public DName getIssuedBy() { + return mIssuedBy; + } + + /** + * @return A string representation of this certificate for debugging + */ + public String toString() { + return + "Issued to: " + mIssuedTo.getDName() + ";\n" + + "Issued by: " + mIssuedBy.getDName() + ";\n"; + } + + /** + * A distinguished name helper class: a 3-tuple of: + * - common name (CN), + * - organization (O), + * - organizational unit (OU) + */ + public class DName { + /** + * Distinguished name (normally includes CN, O, and OU names) + */ + private String mDName; + + /** + * Common-name (CN) component of the name + */ + private String mCName; + + /** + * Organization (O) component of the name + */ + private String mOName; + + /** + * Organizational Unit (OU) component of the name + */ + private String mUName; + + /** + * Creates a new distinguished name + * @param dName The distinguished name + */ + public DName(String dName) { + if (dName != null) { + X509Name x509Name = new X509Name(mDName = dName); + + Vector val = x509Name.getValues(); + Vector oid = x509Name.getOIDs(); + + for (int i = 0; i < oid.size(); i++) { + if (oid.elementAt(i).equals(X509Name.CN)) { + mCName = (String) val.elementAt(i); + continue; + } + + if (oid.elementAt(i).equals(X509Name.O)) { + mOName = (String) val.elementAt(i); + continue; + } + + if (oid.elementAt(i).equals(X509Name.OU)) { + mUName = (String) val.elementAt(i); + continue; + } + } + } + } + + /** + * @return The distinguished name (normally includes CN, O, and OU names) + */ + public String getDName() { + return mDName != null ? mDName : ""; + } + + /** + * @return The Common-name (CN) component of this name + */ + public String getCName() { + return mCName != null ? mCName : ""; + } + + /** + * @return The Organization (O) component of this name + */ + public String getOName() { + return mOName != null ? mOName : ""; + } + + /** + * @return The Organizational Unit (OU) component of this name + */ + public String getUName() { + return mUName != null ? mUName : ""; + } + } +} diff --git a/core/java/android/net/http/SslError.java b/core/java/android/net/http/SslError.java new file mode 100644 index 0000000..2788cb1 --- /dev/null +++ b/core/java/android/net/http/SslError.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2006 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 android.net.http; + +import java.security.cert.X509Certificate; + +/** + * One or more individual SSL errors and the associated SSL certificate + * + * {@hide} + */ +public class SslError { + + /** + * Individual SSL errors (in the order from the least to the most severe): + */ + + /** + * The certificate is not yet valid + */ + public static final int SSL_NOTYETVALID = 0; + /** + * The certificate has expired + */ + public static final int SSL_EXPIRED = 1; + /** + * Hostname mismatch + */ + public static final int SSL_IDMISMATCH = 2; + /** + * The certificate authority is not trusted + */ + public static final int SSL_UNTRUSTED = 3; + + + /** + * The number of different SSL errors (update if you add a new SSL error!!!) + */ + public static final int SSL_MAX_ERROR = 4; + + /** + * The SSL error set bitfield (each individual error is an bit index; + * multiple individual errors can be OR-ed) + */ + int mErrors; + + /** + * The SSL certificate associated with the error set + */ + SslCertificate mCertificate; + + /** + * Creates a new SSL error set object + * @param error The SSL error + * @param certificate The associated SSL certificate + */ + public SslError(int error, SslCertificate certificate) { + addError(error); + mCertificate = certificate; + } + + /** + * Creates a new SSL error set object + * @param error The SSL error + * @param certificate The associated SSL certificate + */ + public SslError(int error, X509Certificate certificate) { + addError(error); + mCertificate = new SslCertificate(certificate); + } + + /** + * @return The SSL certificate associated with the error set + */ + public SslCertificate getCertificate() { + return mCertificate; + } + + /** + * Adds the SSL error to the error set + * @param error The SSL error to add + * @return True iff the error being added is a known SSL error + */ + public boolean addError(int error) { + boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR); + if (rval) { + mErrors |= (0x1 << error); + } + + return rval; + } + + /** + * @param error The SSL error to check + * @return True iff the set includes the error + */ + public boolean hasError(int error) { + boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR); + if (rval) { + rval = ((mErrors & (0x1 << error)) != 0); + } + + return rval; + } + + /** + * @return The primary, most severe, SSL error in the set + */ + public int getPrimaryError() { + if (mErrors != 0) { + // go from the most to the least severe errors + for (int error = SslError.SSL_MAX_ERROR - 1; error >= 0; --error) { + if ((mErrors & (0x1 << error)) != 0) { + return error; + } + } + } + + return 0; + } + + /** + * @return A String representation of this SSL error object + * (used mostly for debugging). + */ + public String toString() { + return "primary error: " + getPrimaryError() + + " certificate: " + getCertificate(); + } +} diff --git a/core/java/android/net/http/Timer.java b/core/java/android/net/http/Timer.java new file mode 100644 index 0000000..cc15a30 --- /dev/null +++ b/core/java/android/net/http/Timer.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2006 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 android.net.http; + +import android.os.SystemClock; + +/** + * {@hide} + * Debugging tool + */ +class Timer { + + private long mStart; + private long mLast; + + public Timer() { + mStart = mLast = SystemClock.uptimeMillis(); + } + + public void mark(String message) { + long now = SystemClock.uptimeMillis(); + if (HttpLog.LOGV) { + HttpLog.v(message + " " + (now - mLast) + " total " + (now - mStart)); + } + mLast = now; + } +} diff --git a/core/java/android/net/http/package.html b/core/java/android/net/http/package.html new file mode 100755 index 0000000..a81cbce --- /dev/null +++ b/core/java/android/net/http/package.html @@ -0,0 +1,2 @@ +<body> +</body> diff --git a/core/java/android/net/package.html b/core/java/android/net/package.html new file mode 100755 index 0000000..47c57e6 --- /dev/null +++ b/core/java/android/net/package.html @@ -0,0 +1,5 @@ +<body> + +Classes that help with network access, beyond the normal java.net.* APIs. + +</body> |