diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/net/http | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_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')
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> |