summaryrefslogtreecommitdiffstats
path: root/core/java/android/net/http
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/net/http
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/net/http')
-rw-r--r--core/java/android/net/http/AndroidHttpClient.java505
-rw-r--r--core/java/android/net/http/AndroidHttpClientConnection.java464
-rw-r--r--core/java/android/net/http/CertificateChainValidator.java354
-rw-r--r--core/java/android/net/http/CertificateValidatorCache.java254
-rw-r--r--core/java/android/net/http/CharArrayBuffers.java89
-rw-r--r--core/java/android/net/http/Connection.java528
-rw-r--r--core/java/android/net/http/ConnectionThread.java137
-rw-r--r--core/java/android/net/http/DomainNameChecker.java277
-rw-r--r--core/java/android/net/http/EventHandler.java147
-rw-r--r--core/java/android/net/http/Headers.java447
-rw-r--r--core/java/android/net/http/HttpAuthHeader.java422
-rw-r--r--core/java/android/net/http/HttpConnection.java96
-rw-r--r--core/java/android/net/http/HttpLog.java44
-rw-r--r--core/java/android/net/http/HttpsConnection.java427
-rw-r--r--core/java/android/net/http/IdleCache.java175
-rw-r--r--core/java/android/net/http/LoggingEventHandler.java90
-rw-r--r--core/java/android/net/http/Request.java462
-rw-r--r--core/java/android/net/http/RequestFeeder.java42
-rw-r--r--core/java/android/net/http/RequestHandle.java424
-rw-r--r--core/java/android/net/http/RequestQueue.java647
-rw-r--r--core/java/android/net/http/SslCertificate.java251
-rw-r--r--core/java/android/net/http/SslError.java144
-rw-r--r--core/java/android/net/http/Timer.java41
-rwxr-xr-xcore/java/android/net/http/package.html2
24 files changed, 6469 insertions, 0 deletions
diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java
new file mode 100644
index 0000000..0c4fcda
--- /dev/null
+++ b/core/java/android/net/http/AndroidHttpClient.java
@@ -0,0 +1,505 @@
+/*
+ * 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.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 org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
+import org.apache.harmony.xnet.provider.jsse.SSLContextImpl;
+
+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.security.KeyManagementException;
+
+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.
+ * @param sessionCache persistent session cache
+ * @return AndroidHttpClient for you to use for all your requests.
+ */
+ public static AndroidHttpClient newInstance(String userAgent,
+ SSLClientSessionCache sessionCache) {
+ 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",
+ socketFactoryWithCache(sessionCache), 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);
+ }
+
+ /**
+ * Returns a socket factory backed by the given persistent session cache.
+ *
+ * @param sessionCache to retrieve sessions from, null for no cache
+ */
+ private static SSLSocketFactory socketFactoryWithCache(
+ SSLClientSessionCache sessionCache) {
+ if (sessionCache == null) {
+ // Use the default factory which doesn't support persistent
+ // caching.
+ return SSLSocketFactory.getSocketFactory();
+ }
+
+ // Create a new SSL context backed by the cache.
+ // TODO: Keep a weak *identity* hash map of caches to engines. In the
+ // mean time, if we have two engines for the same cache, they'll still
+ // share sessions but will have to do so through the persistent cache.
+ SSLContextImpl sslContext = new SSLContextImpl();
+ try {
+ sslContext.engineInit(null, null, null, sessionCache, null);
+ } catch (KeyManagementException e) {
+ throw new AssertionError(e);
+ }
+ return new SSLSocketFactory(sslContext.engineGetSocketFactory());
+ }
+
+ /**
+ * 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) {
+ return newInstance(userAgent, null /* session cache */);
+ }
+
+ 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);
+ }
+
+ /**
+ * Returns true if auth logging is turned on for this configuration.
+ */
+ private boolean isAuthLoggable() {
+ return Log.isLoggable(tag + "-auth", 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,
+ configuration.isAuthLoggable()));
+ }
+ }
+ }
+
+ /**
+ * Generates a cURL command equivalent to the given request.
+ */
+ private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("curl ");
+
+ for (Header header: request.getAllHeaders()) {
+ if (!logAuthToken
+ && (header.getName().equals("Authorization") ||
+ header.getName().equals("Cookie"))) {
+ continue;
+ }
+ 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..0edbe5b
--- /dev/null
+++ b/core/java/android/net/http/CertificateChainValidator.java
@@ -0,0 +1,354 @@
+/*
+ * 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 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 javax.net.ssl.SSLHandshakeException;
+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;
+
+/**
+ * Class responsible for all server certificate validation functionality
+ *
+ * {@hide}
+ */
+class CertificateChainValidator {
+
+ /**
+ * 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 IOException {
+ X509Certificate[] serverCertificates = null;
+
+ // 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]));
+ }
+ }
+ }
+
+ // 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 IOException {
+ closeSocketThrowException(
+ socket, errorMessage != null ? errorMessage : defaultErrorMessage);
+ }
+
+ private void closeSocketThrowException(SSLSocket socket,
+ String errorMessage) throws 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);
+ }
+}
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..563634f
--- /dev/null
+++ b/core/java/android/net/http/Connection.java
@@ -0,0 +1,528 @@
+/*
+ * 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 (IllegalArgumentException e) {
+ if (HttpLog.LOGV) HttpLog.v("Illegal argument exception");
+ error = EventHandler.ERROR_CONNECT;
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ 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..b0923d1
--- /dev/null
+++ b/core/java/android/net/http/Headers.java
@@ -0,0 +1,447 @@
+/*
+ * 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 strings
+ 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;
+
+ // keep any headers that require direct access in a presized
+ // string array
+ private final static int IDX_TRANSFER_ENCODING = 0;
+ private final static int IDX_CONTENT_LEN = 1;
+ private final static int IDX_CONTENT_TYPE = 2;
+ private final static int IDX_CONTENT_ENCODING = 3;
+ private final static int IDX_CONN_DIRECTIVE = 4;
+ private final static int IDX_LOCATION = 5;
+ private final static int IDX_PROXY_CONNECTION = 6;
+ private final static int IDX_WWW_AUTHENTICATE = 7;
+ private final static int IDX_PROXY_AUTHENTICATE = 8;
+ private final static int IDX_CONTENT_DISPOSITION = 9;
+ private final static int IDX_ACCEPT_RANGES = 10;
+ private final static int IDX_EXPIRES = 11;
+ private final static int IDX_CACHE_CONTROL = 12;
+ private final static int IDX_LAST_MODIFIED = 13;
+ private final static int IDX_ETAG = 14;
+ private final static int IDX_SET_COOKIE = 15;
+ private final static int IDX_PRAGMA = 16;
+ private final static int IDX_REFRESH = 17;
+
+ private final static int HEADER_COUNT = 18;
+
+ /* parsed values */
+ private long transferEncoding;
+ private long contentLength; // Content length of the incoming data
+ private int connectionType;
+ private ArrayList<String> cookies = new ArrayList<String>(2);
+
+ private String[] mHeaders = new String[HEADER_COUNT];
+ private final static String[] sHeaderNames = {
+ TRANSFER_ENCODING,
+ CONTENT_LEN,
+ CONTENT_TYPE,
+ CONTENT_ENCODING,
+ CONN_DIRECTIVE,
+ LOCATION,
+ PROXY_CONNECTION,
+ WWW_AUTHENTICATE,
+ PROXY_AUTHENTICATE,
+ CONTENT_DISPOSITION,
+ ACCEPT_RANGES,
+ EXPIRES,
+ CACHE_CONTROL,
+ LAST_MODIFIED,
+ ETAG,
+ SET_COOKIE,
+ PRAGMA,
+ REFRESH
+ };
+
+ // Catch-all for headers not explicitly handled
+ private ArrayList<String> mExtraHeaderNames = new ArrayList<String>(4);
+ private ArrayList<String> mExtraHeaderValues = new ArrayList<String>(4);
+
+ 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++;
+
+ String val = buffer.substringTrimmed(pos, buffer.length());
+ if (HttpLog.LOGV) {
+ HttpLog.v("hdr " + buffer.length() + " " + buffer);
+ }
+
+ switch (name.hashCode()) {
+ case HASH_TRANSFER_ENCODING:
+ if (name.equals(TRANSFER_ENCODING)) {
+ mHeaders[IDX_TRANSFER_ENCODING] = val;
+ 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(val)) {
+ 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)) {
+ mHeaders[IDX_CONTENT_LEN] = val;
+ try {
+ contentLength = Long.parseLong(val);
+ } 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)) {
+ mHeaders[IDX_CONTENT_TYPE] = val;
+ }
+ break;
+ case HASH_CONTENT_ENCODING:
+ if (name.equals(CONTENT_ENCODING)) {
+ mHeaders[IDX_CONTENT_ENCODING] = val;
+ }
+ break;
+ case HASH_CONN_DIRECTIVE:
+ if (name.equals(CONN_DIRECTIVE)) {
+ mHeaders[IDX_CONN_DIRECTIVE] = val;
+ setConnectionType(buffer, pos);
+ }
+ break;
+ case HASH_LOCATION:
+ if (name.equals(LOCATION)) {
+ mHeaders[IDX_LOCATION] = val;
+ }
+ break;
+ case HASH_PROXY_CONNECTION:
+ if (name.equals(PROXY_CONNECTION)) {
+ mHeaders[IDX_PROXY_CONNECTION] = val;
+ setConnectionType(buffer, pos);
+ }
+ break;
+ case HASH_WWW_AUTHENTICATE:
+ if (name.equals(WWW_AUTHENTICATE)) {
+ mHeaders[IDX_WWW_AUTHENTICATE] = val;
+ }
+ break;
+ case HASH_PROXY_AUTHENTICATE:
+ if (name.equals(PROXY_AUTHENTICATE)) {
+ mHeaders[IDX_PROXY_AUTHENTICATE] = val;
+ }
+ break;
+ case HASH_CONTENT_DISPOSITION:
+ if (name.equals(CONTENT_DISPOSITION)) {
+ mHeaders[IDX_CONTENT_DISPOSITION] = val;
+ }
+ break;
+ case HASH_ACCEPT_RANGES:
+ if (name.equals(ACCEPT_RANGES)) {
+ mHeaders[IDX_ACCEPT_RANGES] = val;
+ }
+ break;
+ case HASH_EXPIRES:
+ if (name.equals(EXPIRES)) {
+ mHeaders[IDX_EXPIRES] = val;
+ }
+ break;
+ case HASH_CACHE_CONTROL:
+ if (name.equals(CACHE_CONTROL)) {
+ mHeaders[IDX_CACHE_CONTROL] = val;
+ }
+ break;
+ case HASH_LAST_MODIFIED:
+ if (name.equals(LAST_MODIFIED)) {
+ mHeaders[IDX_LAST_MODIFIED] = val;
+ }
+ break;
+ case HASH_ETAG:
+ if (name.equals(ETAG)) {
+ mHeaders[IDX_ETAG] = val;
+ }
+ break;
+ case HASH_SET_COOKIE:
+ if (name.equals(SET_COOKIE)) {
+ mHeaders[IDX_SET_COOKIE] = val;
+ cookies.add(val);
+ }
+ break;
+ case HASH_PRAGMA:
+ if (name.equals(PRAGMA)) {
+ mHeaders[IDX_PRAGMA] = val;
+ }
+ break;
+ case HASH_REFRESH:
+ if (name.equals(REFRESH)) {
+ mHeaders[IDX_REFRESH] = val;
+ }
+ break;
+ default:
+ mExtraHeaderNames.add(name);
+ mExtraHeaderValues.add(val);
+ }
+ }
+
+ public long getTransferEncoding() {
+ return transferEncoding;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public int getConnectionType() {
+ return connectionType;
+ }
+
+ public String getContentType() {
+ return mHeaders[IDX_CONTENT_TYPE];
+ }
+
+ public String getContentEncoding() {
+ return mHeaders[IDX_CONTENT_ENCODING];
+ }
+
+ public String getLocation() {
+ return mHeaders[IDX_LOCATION];
+ }
+
+ public String getWwwAuthenticate() {
+ return mHeaders[IDX_WWW_AUTHENTICATE];
+ }
+
+ public String getProxyAuthenticate() {
+ return mHeaders[IDX_PROXY_AUTHENTICATE];
+ }
+
+ public String getContentDisposition() {
+ return mHeaders[IDX_CONTENT_DISPOSITION];
+ }
+
+ public String getAcceptRanges() {
+ return mHeaders[IDX_ACCEPT_RANGES];
+ }
+
+ public String getExpires() {
+ return mHeaders[IDX_EXPIRES];
+ }
+
+ public String getCacheControl() {
+ return mHeaders[IDX_CACHE_CONTROL];
+ }
+
+ public String getLastModified() {
+ return mHeaders[IDX_LAST_MODIFIED];
+ }
+
+ public String getEtag() {
+ return mHeaders[IDX_ETAG];
+ }
+
+ public ArrayList<String> getSetCookie() {
+ return this.cookies;
+ }
+
+ public String getPragma() {
+ return mHeaders[IDX_PRAGMA];
+ }
+
+ public String getRefresh() {
+ return mHeaders[IDX_REFRESH];
+ }
+
+ public void setContentLength(long value) {
+ this.contentLength = value;
+ }
+
+ public void setContentType(String value) {
+ mHeaders[IDX_CONTENT_TYPE] = value;
+ }
+
+ public void setContentEncoding(String value) {
+ mHeaders[IDX_CONTENT_ENCODING] = value;
+ }
+
+ public void setLocation(String value) {
+ mHeaders[IDX_LOCATION] = value;
+ }
+
+ public void setWwwAuthenticate(String value) {
+ mHeaders[IDX_WWW_AUTHENTICATE] = value;
+ }
+
+ public void setProxyAuthenticate(String value) {
+ mHeaders[IDX_PROXY_AUTHENTICATE] = value;
+ }
+
+ public void setContentDisposition(String value) {
+ mHeaders[IDX_CONTENT_DISPOSITION] = value;
+ }
+
+ public void setAcceptRanges(String value) {
+ mHeaders[IDX_ACCEPT_RANGES] = value;
+ }
+
+ public void setExpires(String value) {
+ mHeaders[IDX_EXPIRES] = value;
+ }
+
+ public void setCacheControl(String value) {
+ mHeaders[IDX_CACHE_CONTROL] = value;
+ }
+
+ public void setLastModified(String value) {
+ mHeaders[IDX_LAST_MODIFIED] = value;
+ }
+
+ public void setEtag(String value) {
+ mHeaders[IDX_ETAG] = value;
+ }
+
+ public interface HeaderCallback {
+ public void header(String name, String value);
+ }
+
+ /**
+ * Reports all non-null headers to the callback
+ */
+ public void getHeaders(HeaderCallback hcb) {
+ for (int i = 0; i < HEADER_COUNT; i++) {
+ String h = mHeaders[i];
+ if (h != null) {
+ hcb.header(sHeaderNames[i], h);
+ }
+ }
+ int extraLen = mExtraHeaderNames.size();
+ for (int i = 0; i < extraLen; i++) {
+ if (Config.LOGV) {
+ HttpLog.v("Headers.getHeaders() extra: " + i + " " +
+ mExtraHeaderNames.get(i) + " " + mExtraHeaderValues.get(i));
+ }
+ hcb.header(mExtraHeaderNames.get(i),
+ mExtraHeaderValues.get(i));
+ }
+
+ }
+
+ 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;
+ }
+ }
+}
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..df4fff0
--- /dev/null
+++ b/core/java/android/net/http/Request.java
@@ -0,0 +1,462 @@
+/*
+ * 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.EOFException;
+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 (EOFException e) {
+ /* InflaterInputStream throws an EOFException when the
+ server truncates gzipped content. Handle this case
+ as we do truncated non-gzipped content: no error */
+ if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
+ } 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..c4ee5b0
--- /dev/null
+++ b/core/java/android/net/http/RequestHandle.java
@@ -0,0 +1,424 @@
+/*
+ * 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";
+
+ public 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;
+ }
+
+ public int getRedirectCount() {
+ return mRedirectCount;
+ }
+
+ public void setRedirectCount(int count) {
+ mRedirectCount = 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, body provider unable to rewind on 307 redirect)
+ */
+ 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";
+ }
+ /* Only repost content on a 307. If 307, reset the body
+ provider so we can replay the body */
+ if (statusCode == 307) {
+ try {
+ if (mBodyProvider != null) mBodyProvider.reset();
+ } catch (java.io.IOException ex) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupAuthResponse() failed to reset body provider");
+ }
+ return false;
+ }
+
+ } else {
+ 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..66d5722
--- /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.mProxyHost == null ? request.mHost : request.mProxyHost;
+ 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>