diff options
author | Narayan Kamath <narayan@google.com> | 2014-11-27 18:15:37 +0000 |
---|---|---|
committer | Narayan Kamath <narayan@google.com> | 2015-02-04 13:13:58 +0000 |
commit | a8b46a3d3b6ed1488df10740653829283572903b (patch) | |
tree | f2e7c14a60fbcdac9ac45dcfb2548975de03b554 | |
parent | 9ba0e2c70ba567fb802bf23f94b9b173a524431a (diff) | |
download | android_external_apache-http-a8b46a3d3b6ed1488df10740653829283572903b.tar.gz android_external_apache-http-a8b46a3d3b6ed1488df10740653829283572903b.tar.bz2 android_external_apache-http-a8b46a3d3b6ed1488df10740653829283572903b.zip |
Move apache specific portions of android.net.http to apache-http.
.. and move some parts of apache-http into the framework. The parts that
have been moved need to be in the bootclasspath because we have public API
that returns org.apache.http.conn.ssl.SSLSocketFactory :( .
This change also removes the placeholder library shim.
bug: 18027885
Change-Id: I37aa7562bcd5e05191b83676fae4533e03b86d1d
41 files changed, 7116 insertions, 1920 deletions
@@ -17,8 +17,10 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := org.apache.http.legacy LOCAL_MODULE_TAGS := optional +LOCAL_JAVA_LIBRARIES := conscrypt LOCAL_SRC_FILES := \ - $(call all-java-files-under,placeholder) + $(call all-java-files-under,src) \ + $(call all-java-files-under,android) LOCAL_MODULE_TAGS := optional include $(BUILD_JAVA_LIBRARY) diff --git a/android/src/android/net/http/AndroidHttpClient.java b/android/src/android/net/http/AndroidHttpClient.java new file mode 100644 index 0000000..04f3974 --- /dev/null +++ b/android/src/android/net/http/AndroidHttpClient.java @@ -0,0 +1,509 @@ +/* + * 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 com.android.internal.http.HttpDateTime; + +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.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.client.protocol.ClientContext; +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.entity.AbstractHttpEntity; +import org.apache.http.entity.ByteArrayEntity; +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.BasicHttpContext; +import org.apache.http.protocol.BasicHttpProcessor; +import org.apache.http.protocol.HttpContext; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.SSLCertificateSocketFactory; +import android.net.SSLSessionCache; +import android.os.Looper; +import android.util.Base64; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * Implementation of the Apache {@link DefaultHttpClient} that is configured with + * reasonable default settings and registered schemes for Android. + * 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> + */ +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; + + // Default connection and socket timeout of 60 seconds. Tweak to taste. + private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000; + + private static final String TAG = "AndroidHttpClient"; + + private static String[] textContentTypes = new String[] { + "text/", + "application/xml", + "application/json" + }; + + /** Interceptor throws an exception if the executing thread is blocked */ + private static final HttpRequestInterceptor sThreadCheckInterceptor = + new HttpRequestInterceptor() { + public void process(HttpRequest request, HttpContext context) { + // Prevent the HttpRequest from being sent on the main thread + if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) { + 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 context to use for caching SSL sessions (may be null for no caching) + * @return AndroidHttpClient for you to use for all your requests. + */ + public static AndroidHttpClient newInstance(String userAgent, Context context) { + 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); + + HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT); + HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT); + 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); + + // Use a session cache for SSL sockets + SSLSessionCache sessionCache = context == null ? null : new SSLSessionCache(context); + + // 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", + SSLCertificateSocketFactory.getHttpSocketFactory( + SOCKET_OPERATION_TIMEOUT, 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); + } + + /** + * 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; + } + } + + /** + * 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) { + return DEFAULT_SYNC_MIN_GZIP_BYTES; // For now, this is just a constant. + } + + /* cURL logging support. */ + + /** + * Logging tag and level. + */ + private static class LoggingConfiguration { + + private final String tag; + private final int level; + + private LoggingConfiguration(String tag, int level) { + this.tag = tag; + this.level = level; + } + + /** + * Returns true if logging is turned on for this configuration. + */ + private boolean isLoggable() { + return Log.isLoggable(tag, level); + } + + /** + * Prints a message using this configuration. + */ + private void println(String message) { + Log.println(level, tag, message); + } + } + + /** cURL logging configuration. */ + private volatile LoggingConfiguration curlConfiguration; + + /** + * Enables cURL request logging for this client. + * + * @param name to log messages with + * @param level at which to log messages (see {@link android.util.Log}) + */ + public void enableCurlLogging(String name, int level) { + if (name == null) { + throw new NullPointerException("name"); + } + if (level < Log.VERBOSE || level > Log.ASSERT) { + throw new IllegalArgumentException("Level is out of range [" + + Log.VERBOSE + ".." + Log.ASSERT + "]"); + } + + curlConfiguration = new LoggingConfiguration(name, level); + } + + /** + * Disables cURL logging for this client. + */ + public void disableCurlLogging() { + curlConfiguration = null; + } + + /** + * Logs cURL commands equivalent to requests. + */ + private class CurlLogger implements HttpRequestInterceptor { + public void process(HttpRequest request, HttpContext context) + throws HttpException, IOException { + LoggingConfiguration configuration = curlConfiguration; + if (configuration != null + && configuration.isLoggable() + && request instanceof HttpUriRequest) { + // Never print auth token -- we used to check ro.secure=0 to + // enable that, but can't do that in unbundled code. + configuration.println(toCurl((HttpUriRequest) request, false)); + } + } + } + + /** + * 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 "); + + // add in the method + builder.append("-X "); + builder.append(request.getMethod()); + builder.append(" "); + + 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); + + if (isBinaryContent(request)) { + String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP); + builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; "); + builder.append(" --data-binary @/tmp/$$.bin"); + } else { + String entityString = stream.toString(); + builder.append(" --data-ascii \"") + .append(entityString) + .append("\""); + } + } else { + builder.append(" [TOO MUCH DATA TO INCLUDE]"); + } + } + } + + return builder.toString(); + } + + private static boolean isBinaryContent(HttpUriRequest request) { + Header[] headers; + headers = request.getHeaders(Headers.CONTENT_ENCODING); + if (headers != null) { + for (Header header : headers) { + if ("gzip".equalsIgnoreCase(header.getValue())) { + return true; + } + } + } + + headers = request.getHeaders(Headers.CONTENT_TYPE); + if (headers != null) { + for (Header header : headers) { + for (String contentType : textContentTypes) { + if (header.getValue().startsWith(contentType)) { + return false; + } + } + } + } + return true; + } + + /** + * Returns the date of the given HTTP date string. This method can identify + * and parse the date formats emitted by common HTTP servers, such as + * <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>, + * <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>, + * <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>, + * <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and + * <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI + * C's asctime()</a>. + * + * @return the number of milliseconds since Jan. 1, 1970, midnight GMT. + * @throws IllegalArgumentException if {@code dateString} is not a date or + * of an unsupported format. + */ + public static long parseDate(String dateString) { + return HttpDateTime.parse(dateString); + } +} diff --git a/android/src/android/net/http/AndroidHttpClientConnection.java b/android/src/android/net/http/AndroidHttpClientConnection.java new file mode 100644 index 0000000..6d48fce --- /dev/null +++ b/android/src/android/net/http/AndroidHttpClientConnection.java @@ -0,0 +1,460 @@ +/* + * 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.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.NoHttpResponseException; +import org.apache.http.StatusLine; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.ContentLengthStrategy; +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/android/src/android/net/http/CertificateChainValidator.java b/android/src/android/net/http/CertificateChainValidator.java new file mode 100644 index 0000000..bf3fe02 --- /dev/null +++ b/android/src/android/net/http/CertificateChainValidator.java @@ -0,0 +1,279 @@ +/* + * 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 com.android.org.conscrypt.SSLParametersImpl; +import com.android.org.conscrypt.TrustManagerImpl; + +import android.util.Slog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +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} + */ +public class CertificateChainValidator { + private static final String TAG = "CertificateChainValidator"; + + private static class NoPreloadHolder { + /** + * The singleton instance of the certificate chain validator. + */ + private static final CertificateChainValidator sInstance = new CertificateChainValidator(); + + /** + * The singleton instance of the hostname verifier. + */ + private static final HostnameVerifier sVerifier = HttpsURLConnection + .getDefaultHostnameVerifier(); + } + + private X509TrustManager mTrustManager; + + /** + * @return The singleton instance of the certificates chain validator + */ + public static CertificateChainValidator getInstance() { + return NoPreloadHolder.sInstance; + } + + /** + * Creates a new certificate chain validator. This is a private constructor. + * If you need a Certificate chain validator, call getInstance(). + */ + private CertificateChainValidator() { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X.509"); + tmf.init((KeyStore) null); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + mTrustManager = (X509TrustManager) tm; + } + } + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("X.509 TrustManagerFactory must be available", e); + } catch (KeyStoreException e) { + throw new RuntimeException("X.509 TrustManagerFactory cannot be initialized", e); + } + + if (mTrustManager == null) { + throw new RuntimeException( + "None of the X.509 TrustManagers are X509TrustManager"); + } + } + + /** + * Performs the handshake and server certificates validation + * Notice a new chain will be rebuilt by tracing the issuer and subject + * before calling checkServerTrusted(). + * And if the last traced certificate is self issued and it is expired, it + * will be dropped. + * @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 { + // get a valid SSLSession, close the socket if we fail + SSLSession sslSession = sslSocket.getSession(); + if (!sslSession.isValid()) { + closeSocketThrowException(sslSocket, "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 { + // update the SSL certificate associated with the connection + if (connection != null) { + if (peerCertificates[0] != null) { + connection.setCertificate( + new SslCertificate((X509Certificate)peerCertificates[0])); + } + } + } + + return verifyServerDomainAndCertificates((X509Certificate[]) peerCertificates, domain, "RSA"); + } + + /** + * Similar to doHandshakeAndValidateServerCertificates but exposed to JNI for use + * by Chromium HTTPS stack to validate the cert chain. + * @param certChain The bytes for certificates in ASN.1 DER encoded certificates format. + * @param domain The full website hostname and domain + * @param authType The authentication type for the cert chain + * @return An SSL error object if there is an error and null otherwise + */ + public static SslError verifyServerCertificates( + byte[][] certChain, String domain, String authType) + throws IOException { + + if (certChain == null || certChain.length == 0) { + throw new IllegalArgumentException("bad certificate chain"); + } + + X509Certificate[] serverCertificates = new X509Certificate[certChain.length]; + + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + for (int i = 0; i < certChain.length; ++i) { + serverCertificates[i] = (X509Certificate) cf.generateCertificate( + new ByteArrayInputStream(certChain[i])); + } + } catch (CertificateException e) { + throw new IOException("can't read certificate", e); + } + + return verifyServerDomainAndCertificates(serverCertificates, domain, authType); + } + + /** + * Handles updates to credential storage. + */ + public static void handleTrustStorageUpdate() { + TrustManagerFactory tmf; + try { + tmf = TrustManagerFactory.getInstance("X.509"); + tmf.init((KeyStore) null); + } catch (NoSuchAlgorithmException e) { + Slog.w(TAG, "Couldn't find default X.509 TrustManagerFactory"); + return; + } catch (KeyStoreException e) { + Slog.w(TAG, "Couldn't initialize default X.509 TrustManagerFactory", e); + return; + } + + TrustManager[] tms = tmf.getTrustManagers(); + boolean sentUpdate = false; + for (TrustManager tm : tms) { + try { + Method updateMethod = tm.getClass().getDeclaredMethod("handleTrustStorageUpdate"); + updateMethod.setAccessible(true); + updateMethod.invoke(tm); + sentUpdate = true; + } catch (Exception e) { + } + } + if (!sentUpdate) { + Slog.w(TAG, "Didn't find a TrustManager to handle CA list update"); + } + } + + /** + * Common code of doHandshakeAndValidateServerCertificates and verifyServerCertificates. + * Calls DomainNamevalidator to verify the domain, and TrustManager to verify the certs. + * @param chain the cert chain in X509 cert format. + * @param domain The full website hostname and domain + * @param authType The authentication type for the cert chain + * @return An SSL error object if there is an error and null otherwise + */ + private static SslError verifyServerDomainAndCertificates( + X509Certificate[] chain, String domain, String authType) + throws IOException { + // check if the first certificate in the chain is for this site + X509Certificate currCertificate = chain[0]; + if (currCertificate == null) { + throw new IllegalArgumentException("certificate for this site is null"); + } + + boolean valid = domain != null + && !domain.isEmpty() + && NoPreloadHolder.sVerifier.verify(domain, + new DelegatingSSLSession.CertificateWrap(currCertificate)); + if (!valid) { + if (HttpLog.LOGV) { + HttpLog.v("certificate not for this host: " + domain); + } + return new SslError(SslError.SSL_IDMISMATCH, currCertificate); + } + + try { + X509TrustManager x509TrustManager = SSLParametersImpl.getDefaultX509TrustManager(); + if (x509TrustManager instanceof TrustManagerImpl) { + TrustManagerImpl trustManager = (TrustManagerImpl) x509TrustManager; + trustManager.checkServerTrusted(chain, authType, domain); + } else { + x509TrustManager.checkServerTrusted(chain, authType); + } + return null; // No errors. + } catch (GeneralSecurityException e) { + if (HttpLog.LOGV) { + HttpLog.v("failed to validate the certificate chain, error: " + + e.getMessage()); + } + return new SslError(SslError.SSL_UNTRUSTED, currCertificate); + } + } + + /** + * Returns the platform default {@link X509TrustManager}. + */ + private X509TrustManager getTrustManager() { + return mTrustManager; + } + + 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/android/src/android/net/http/Connection.java b/android/src/android/net/http/Connection.java new file mode 100644 index 0000000..831bd0e --- /dev/null +++ b/android/src/android/net/http/Connection.java @@ -0,0 +1,575 @@ +/* + * 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.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"; + + 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, + RequestFeeder requestFeeder) { + mContext = context; + mHost = host; + 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, HttpHost proxy, + RequestFeeder requestFeeder) { + + if (host.getSchemeName().equals("http")) { + return new HttpConnection(context, host, requestFeeder); + } + + // Otherwise, default to https + return new HttpsConnection(context, host, proxy, 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; + } + } + + /* we have a connection, let the event handler + * know of any associated certificate, + * potentially none. + */ + req.mEventHandler.certificate(mCertificate); + + 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) ? 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; + state = SEND; + } + 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 (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; + + // e.printStackTrace(); + if (HttpLog.LOGV) HttpLog.v( + "httpFailure() ******* " + e + " count " + req.mFailCount + + " " + mHost + " " + req.getUri()); + + if (++req.mFailCount >= RETRY_REQUEST_LIMIT) { + ret = false; + String error; + if (errorId < 0) { + error = getEventHandlerErrorString(errorId); + } 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; + } + + private static String getEventHandlerErrorString(int errorId) { + switch (errorId) { + case EventHandler.OK: + return "OK"; + + case EventHandler.ERROR: + return "ERROR"; + + case EventHandler.ERROR_LOOKUP: + return "ERROR_LOOKUP"; + + case EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME: + return "ERROR_UNSUPPORTED_AUTH_SCHEME"; + + case EventHandler.ERROR_AUTH: + return "ERROR_AUTH"; + + case EventHandler.ERROR_PROXYAUTH: + return "ERROR_PROXYAUTH"; + + case EventHandler.ERROR_CONNECT: + return "ERROR_CONNECT"; + + case EventHandler.ERROR_IO: + return "ERROR_IO"; + + case EventHandler.ERROR_TIMEOUT: + return "ERROR_TIMEOUT"; + + case EventHandler.ERROR_REDIRECT_LOOP: + return "ERROR_REDIRECT_LOOP"; + + case EventHandler.ERROR_UNSUPPORTED_SCHEME: + return "ERROR_UNSUPPORTED_SCHEME"; + + case EventHandler.ERROR_FAILED_SSL_HANDSHAKE: + return "ERROR_FAILED_SSL_HANDSHAKE"; + + case EventHandler.ERROR_BAD_URL: + return "ERROR_BAD_URL"; + + case EventHandler.FILE_ERROR: + return "FILE_ERROR"; + + case EventHandler.FILE_NOT_FOUND_ERROR: + return "FILE_NOT_FOUND_ERROR"; + + case EventHandler.TOO_MANY_REQUESTS_ERROR: + return "TOO_MANY_REQUESTS_ERROR"; + + default: + return "UNKNOWN_ERROR"; + } + } + + 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/android/src/android/net/http/ConnectionThread.java b/android/src/android/net/http/ConnectionThread.java new file mode 100644 index 0000000..d825530 --- /dev/null +++ b/android/src/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 java.lang.Thread; + +/** + * {@hide} + */ +class ConnectionThread extends Thread { + + static final int WAIT_TIMEOUT = 5000; + static final int WAIT_TICK = 1000; + + // Performance probe + long mCurrentThreadTime; + long mTotalThreadTime; + + 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_DEFAULT + + android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE); + + // these are used to get performance data. When it is not in the timing, + // mCurrentThreadTime is 0. When it starts timing, mCurrentThreadTime is + // first set to -1, it will be set to the current thread time when the + // next request starts. + mCurrentThreadTime = 0; + mTotalThreadTime = 0; + + while (mRunning) { + if (mCurrentThreadTime == -1) { + mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); + } + + 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 { + mRequestFeeder.wait(); + } catch (InterruptedException e) { + } + mWaiting = false; + if (mCurrentThreadTime != 0) { + mCurrentThreadTime = SystemClock + .currentThreadTimeMillis(); + } + } + } else { + if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " + + request.mHost + " " + request ); + + mConnection = mConnectionManager.getConnection(mContext, + request.mHost); + mConnection.processRequests(request); + if (mConnection.getCanPersist()) { + if (!mConnectionManager.recycleConnection(mConnection)) { + mConnection.closeConnection(); + } + } else { + mConnection.closeConnection(); + } + mConnection = null; + + if (mCurrentThreadTime > 0) { + long start = mCurrentThreadTime; + mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); + mTotalThreadTime += mCurrentThreadTime - start; + } + } + + } + } + + public synchronized String toString() { + String con = mConnection == null ? "" : mConnection.toString(); + String active = mWaiting ? "w" : "a"; + return "cid " + mId + " " + active + " " + con; + } + +} diff --git a/android/src/android/net/http/DelegatingSSLSession.java b/android/src/android/net/http/DelegatingSSLSession.java new file mode 100644 index 0000000..98fbe21 --- /dev/null +++ b/android/src/android/net/http/DelegatingSSLSession.java @@ -0,0 +1,158 @@ +/* + * Copyright 2014 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.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509TrustManager; + +/** + * This is only used when a {@code certificate} is available but usage + * requires a {@link SSLSession}. + * + * @hide + */ +public class DelegatingSSLSession implements SSLSession { + protected DelegatingSSLSession() { + } + + public static class CertificateWrap extends DelegatingSSLSession { + private final Certificate mCertificate; + + public CertificateWrap(Certificate certificate) { + mCertificate = certificate; + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + return new Certificate[] { mCertificate }; + } + } + + + @Override + public int getApplicationBufferSize() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCipherSuite() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCreationTime() { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getId() { + throw new UnsupportedOperationException(); + } + + @Override + public long getLastAccessedTime() { + throw new UnsupportedOperationException(); + } + + @Override + public Certificate[] getLocalCertificates() { + throw new UnsupportedOperationException(); + } + + @Override + public Principal getLocalPrincipal() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPacketBufferSize() { + throw new UnsupportedOperationException(); + } + + @Override + public javax.security.cert.X509Certificate[] getPeerCertificateChain() + throws SSLPeerUnverifiedException { + throw new UnsupportedOperationException(); + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + throw new UnsupportedOperationException(); + } + + @Override + public String getPeerHost() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPeerPort() { + throw new UnsupportedOperationException(); + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + throw new UnsupportedOperationException(); + } + + @Override + public String getProtocol() { + throw new UnsupportedOperationException(); + } + + @Override + public SSLSessionContext getSessionContext() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getValueNames() { + throw new UnsupportedOperationException(); + } + + @Override + public void invalidate() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isValid() { + throw new UnsupportedOperationException(); + } + + @Override + public void putValue(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeValue(String name) { + throw new UnsupportedOperationException(); + } +} diff --git a/android/src/android/net/http/EventHandler.java b/android/src/android/net/http/EventHandler.java new file mode 100644 index 0000000..3fd471d --- /dev/null +++ b/android/src/android/net/http/EventHandler.java @@ -0,0 +1,131 @@ +/* + * 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; + + /** + * 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 before resource request is + * made, which will be null for insecure 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. + * @return True if the callback can handle the error, which means it will + * call restartConnection() to unblock the thread later, + * otherwise return false. + */ + public boolean handleSslErrorRequest(SslError error); + +} diff --git a/android/src/android/net/http/Headers.java b/android/src/android/net/http/Headers.java new file mode 100644 index 0000000..0f8b105 --- /dev/null +++ b/android/src/android/net/http/Headers.java @@ -0,0 +1,521 @@ +/* + * 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.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"; + public final static String X_PERMITTED_CROSS_DOMAIN_POLICIES = "x-permitted-cross-domain-policies"; + + // following hash are generated by String.hashCode() + private final static int HASH_TRANSFER_ENCODING = 1274458357; + private final static int HASH_CONTENT_LEN = -1132779846; + private final static int HASH_CONTENT_TYPE = 785670158; + private final static int HASH_CONTENT_ENCODING = 2095084583; + private final static int HASH_CONN_DIRECTIVE = -775651618; + private final static int HASH_LOCATION = 1901043637; + private final static int HASH_PROXY_CONNECTION = 285929373; + private final static int HASH_WWW_AUTHENTICATE = -243037365; + private final static int HASH_PROXY_AUTHENTICATE = -301767724; + private final static int HASH_CONTENT_DISPOSITION = -1267267485; + private final static int HASH_ACCEPT_RANGES = 1397189435; + private final static int HASH_EXPIRES = -1309235404; + private final static int HASH_CACHE_CONTROL = -208775662; + private final static int HASH_LAST_MODIFIED = 150043680; + private final static int HASH_ETAG = 3123477; + private final static int HASH_SET_COOKIE = 1237214767; + private final static int HASH_PRAGMA = -980228804; + private final static int HASH_REFRESH = 1085444827; + private final static int HASH_X_PERMITTED_CROSS_DOMAIN_POLICIES = -1345594014; + + // 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 IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES = 18; + + private final static int HEADER_COUNT = 19; + + /* 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, + X_PERMITTED_CROSS_DOMAIN_POLICIES + }; + + // 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 = 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 (false) { + 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)) { + // In case where we receive more than one header, create a ',' separated list. + // This should be ok, according to RFC 2616 chapter 4.2 + if (mHeaders[IDX_CACHE_CONTROL] != null && + mHeaders[IDX_CACHE_CONTROL].length() > 0) { + mHeaders[IDX_CACHE_CONTROL] += (',' + val); + } else { + 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; + case HASH_X_PERMITTED_CROSS_DOMAIN_POLICIES: + if (name.equals(X_PERMITTED_CROSS_DOMAIN_POLICIES)) { + mHeaders[IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES] = 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 String getXPermittedCrossDomainPolicies() { + return mHeaders[IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES]; + } + + 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 void setXPermittedCrossDomainPolicies(String value) { + mHeaders[IDX_X_PERMITTED_CROSS_DOMAIN_POLICIES] = 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 (false) { + 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 (containsIgnoreCaseTrimmed(buffer, pos, HTTP.CONN_CLOSE)) { + connectionType = CONN_CLOSE; + } else if (containsIgnoreCaseTrimmed( + buffer, pos, HTTP.CONN_KEEP_ALIVE)) { + connectionType = CONN_KEEP_ALIVE; + } + } + + + /** + * 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 = Character.toLowerCase(a); + b = Character.toLowerCase(b); + ok = a == b; + } + } + + return true; + } + + /** + * 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 { + chars[i] = Character.toLowerCase(current); + } + } + return -1; + } +} diff --git a/android/src/android/net/http/HttpAuthHeader.java b/android/src/android/net/http/HttpAuthHeader.java new file mode 100644 index 0000000..3abac23 --- /dev/null +++ b/android/src/android/net/http/HttpAuthHeader.java @@ -0,0 +1,424 @@ +/* + * 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 java.util.Locale; + +/** + * 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(Locale.ROOT); + return; + } + + if (token.equalsIgnoreCase(ALGORITHM_TOKEN)) { + mAlgorithm = value.toLowerCase(Locale.ROOT); + 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/android/src/android/net/http/HttpConnection.java b/android/src/android/net/http/HttpConnection.java new file mode 100644 index 0000000..edf8fed --- /dev/null +++ b/android/src/android/net/http/HttpConnection.java @@ -0,0 +1,93 @@ +/* + * 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.HttpHost; +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, + RequestFeeder requestFeeder) { + super(context, host, 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/android/src/android/net/http/HttpLog.java b/android/src/android/net/http/HttpLog.java new file mode 100644 index 0000000..0934664 --- /dev/null +++ b/android/src/android/net/http/HttpLog.java @@ -0,0 +1,43 @@ +/* + * 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; + +/** + * {@hide} + */ +class HttpLog { + private final static String LOGTAG = "http"; + + private static final boolean DEBUG = false; + static final boolean LOGV = false; + + 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/android/src/android/net/http/HttpsConnection.java b/android/src/android/net/http/HttpsConnection.java new file mode 100644 index 0000000..a8674de --- /dev/null +++ b/android/src/android/net/http/HttpsConnection.java @@ -0,0 +1,433 @@ +/* + * 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.util.Log; +import com.android.org.conscrypt.FileClientSessionCache; +import com.android.org.conscrypt.OpenSSLContextImpl; +import com.android.org.conscrypt.SSLClientSessionCache; +import org.apache.http.Header; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +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.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +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 java.io.File; +import java.io.IOException; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.cert.X509Certificate; +import java.util.Locale; + +/** + * A Connection connecting to a secure http server or tunneling through + * a http proxy server to a https server. + * + * @hide + */ +public class HttpsConnection extends Connection { + + /** + * SSL socket factory + */ + private static SSLSocketFactory mSslSocketFactory = null; + + static { + // This initialization happens in the zygote. It triggers some + // lazy initialization that can will benefit later invocations of + // initializeEngine(). + initializeEngine(null); + } + + /** + * @hide + * + * @param sessionDir directory to cache SSL sessions + */ + public static void initializeEngine(File sessionDir) { + try { + SSLClientSessionCache cache = null; + if (sessionDir != null) { + Log.d("HttpsConnection", "Caching SSL sessions in " + + sessionDir + "."); + cache = FileClientSessionCache.usingDirectory(sessionDir); + } + + OpenSSLContextImpl sslContext = OpenSSLContextImpl.getPreferred(); + + // 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) { + } + } + }; + + sslContext.engineInit(null, trustManagers, null); + sslContext.engineGetClientSessionContext().setPersistentCache(cache); + + synchronized (HttpsConnection.class) { + mSslSocketFactory = sslContext.engineGetSocketFactory(); + } + } catch (KeyManagementException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private synchronized static SSLSocketFactory getSocketFactory() { + return mSslSocketFactory; + } + + /** + * 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; + + // Used when connecting through a proxy. + private HttpHost mProxyHost; + + /** + * Contructor for a https connection. + */ + HttpsConnection(Context context, HttpHost host, HttpHost proxy, + RequestFeeder requestFeeder) { + super(context, host, requestFeeder); + mProxyHost = proxy; + } + + /** + * 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; + + if (mProxyHost != 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 + (mProxyHost.getHostName(), mProxyHost.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, we also need + // to add 'host' header unless we want proxy to answer us with a + // 400 Bad Request + for (Header h : req.mHttpRequest.getAllHeaders()) { + String headerName = h.getName().toLowerCase(Locale.ROOT); + if (headerName.startsWith("proxy") || headerName.equals("keep-alive") + || headerName.equals("host")) { + 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 { + sslSock = (SSLSocket) getSocketFactory().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 { + sslSock = (SSLSocket) getSocketFactory().createSocket( + mHost.getHostName(), mHost.getPort()); + sslSock.setSoTimeout(SOCKET_TIMEOUT); + } 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()); + + // 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 + boolean canHandle = req.getEventHandler().handleSslErrorRequest(error); + if(!canHandle) { + throw new IOException("failed to handle "+ 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"; + } +} + +/** + * Simple exception we throw if the SSL connection is closed by the user. + * + * {@hide} + */ +class SSLConnectionClosedByUserException extends SSLException { + + public SSLConnectionClosedByUserException(String reason) { + super(reason); + } +} diff --git a/android/src/android/net/http/IdleCache.java b/android/src/android/net/http/IdleCache.java new file mode 100644 index 0000000..fda6009 --- /dev/null +++ b/android/src/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/android/src/android/net/http/LoggingEventHandler.java b/android/src/android/net/http/LoggingEventHandler.java new file mode 100644 index 0000000..bdafa0b --- /dev/null +++ b/android/src/android/net/http/LoggingEventHandler.java @@ -0,0 +1,92 @@ +/* + * 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 boolean handleSslErrorRequest(SslError error) { + if (HttpLog.LOGV) { + HttpLog.v("LoggingEventHandler: handleSslErrorRequest():" + error); + } + // return false so that the caller thread won't wait forever + return false; + } +} diff --git a/android/src/android/net/http/Request.java b/android/src/android/net/http/Request.java new file mode 100644 index 0000000..76d7bb9 --- /dev/null +++ b/android/src/android/net/http/Request.java @@ -0,0 +1,526 @@ +/* + * 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.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.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 has been cancelled */ + volatile boolean mCancelled = false; + + int mFailCount = 0; + + // This will be used to set the Range field if we retry a connection. This + // is http/1.1 feature. + private int mReceivedBytes = 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(); + + /** True if loading should be paused **/ + private boolean mLoadingPaused = false; + + /** + * 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 + */ + Request(String method, HttpHost host, HttpHost proxyHost, String path, + InputStream bodyProvider, int bodyLength, + EventHandler eventHandler, + Map<String, String> headers) { + mEventHandler = eventHandler; + mHost = host; + mProxyHost = proxyHost; + mPath = path; + mBodyProvider = bodyProvider; + mBodyLength = bodyLength; + + if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) { + mHttpRequest = new BasicHttpRequest(method, getUri()); + } else { + mHttpRequest = new BasicHttpEntityEnclosingRequest( + method, getUri()); + // it is ok to have null entity for BasicHttpEntityEnclosingRequest. + // By using BasicHttpEntityEnclosingRequest, it will set up the + // correct content-length, content-type and content-encoding. + if (bodyProvider != null) { + 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 pause True if the load should be paused. + */ + synchronized void setLoadingPaused(boolean pause) { + mLoadingPaused = pause; + + // Wake up the paused thread if we're unpausing the load. + if (!mLoadingPaused) { + notify(); + } + } + + /** + * @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; + 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); + + // restrict the range request to the servers claiming that they are + // accepting ranges in bytes + boolean supportPartialContent = "bytes".equalsIgnoreCase(header + .getAcceptRanges()); + + if (entity != null) { + InputStream is = entity.getContent(); + + // process gzip content encoding + Header contentEncoding = entity.getContentEncoding(); + InputStream nis = null; + byte[] buf = null; + int count = 0; + 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 */ + buf = mConnection.getBuf(); + int len = 0; + int lowWater = buf.length / 2; + while (len != -1) { + synchronized(this) { + while (mLoadingPaused) { + // Put this (network loading) thread to sleep if WebCore + // has asked us to. This can happen with plugins for + // example, if we are streaming data but the plugin has + // filled its internal buffers. + try { + wait(); + } catch (InterruptedException e) { + HttpLog.e("Interrupted exception whilst " + + "network thread paused at WebCore's request." + + " " + e.getMessage()); + } + } + } + + len = nis.read(buf, count, buf.length - count); + + if (len != -1) { + count += len; + if (supportPartialContent) mReceivedBytes += 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 (count > 0) { + // if there is uncommited content, we should commit them + mEventHandler.data(buf, count); + } + 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 + || statusCode == HttpStatus.SC_PARTIAL_CONTENT) { + if (supportPartialContent && count > 0) { + // if there is uncommited content, we should commit them + // as we will continue the request + mEventHandler.data(buf, count); + } + 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 + */ + synchronized void cancel() { + if (HttpLog.LOGV) { + HttpLog.v("Request.cancel(): " + getUri()); + } + + // Ensure that the network thread is not blocked by a hanging request from WebCore to + // pause the load. + mLoadingPaused = false; + notify(); + + 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 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); + } + + if (mReceivedBytes > 0) { + // reset the fail count as we continue the request + mFailCount = 0; + // set the "Range" header to indicate that the retry will continue + // instead of restarting the request + HttpLog.v("*** Request.reset() to range:" + mReceivedBytes); + mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-"); + } + } + + /** + * 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; + } + + /** + * 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/placeholder/org/apache/http/PlaceHolder.java b/android/src/android/net/http/RequestFeeder.java index 20ebfc0..34ca267 100644 --- a/placeholder/org/apache/http/PlaceHolder.java +++ b/android/src/android/net/http/RequestFeeder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 The Android Open Source Project + * 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. @@ -14,7 +14,29 @@ * limitations under the License. */ -package org.apache.http; +/** + * 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); -public class PlaceHolder { + /** + * Put request back on head of queue + */ + void requeueRequest(Request request); } diff --git a/android/src/android/net/http/RequestHandle.java b/android/src/android/net/http/RequestHandle.java new file mode 100644 index 0000000..46c3869 --- /dev/null +++ b/android/src/android/net/http/RequestHandle.java @@ -0,0 +1,478 @@ +/* + * 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.webkit.CookieManager; + +import org.apache.commons.codec.binary.Base64; + +import java.io.InputStream; +import java.lang.Math; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +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; + // Used only with synchronous requests. + private Connection mConnection; + + 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; + } + + /** + * Creates a new request session with a given Connection. This connection + * is used during a synchronous load to handle this request. + */ + public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, + String method, Map<String, String> headers, + InputStream bodyProvider, int bodyLength, Request request, + Connection conn) { + this(requestQueue, url, uri, method, headers, bodyProvider, bodyLength, + request); + mConnection = conn; + } + + /** + * Cancels this request + */ + public void cancel() { + if (mRequest != null) { + mRequest.cancel(); + } + } + + /** + * Pauses the loading of this request. For example, called from the WebCore thread + * when the plugin can take no more data. + */ + public void pauseRequest(boolean pause) { + if (mRequest != null) { + mRequest.setLoadingPaused(pause); + } + } + + /** + * 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("setupRedirect() 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) { + if (username == null) { + throw new NullPointerException("username == null"); + } + + if (password == null) { + throw new NullPointerException("password == null"); + } + + // encode username:password to base64 + return new String(Base64.encodeBase64((username + ':' + password).getBytes())); + } + + public void waitUntilComplete() { + mRequest.waitUntilComplete(); + } + + public void processRequest() { + if (mConnection != null) { + mConnection.processRequests(mRequest); + } + } + + /** + * @return Digest-scheme authentication response. + */ + private String computeDigestAuthResponse(String username, + String password, + String realm, + String nonce, + String QOP, + String algorithm, + String opaque) { + + if (username == null) { + throw new NullPointerException("username == null"); + } + + if (password == null) { + throw new NullPointerException("password == null"); + } + + if (realm == null) { + throw new NullPointerException("realm == null"); + } + + String A1 = username + ":" + realm + ":" + password; + String A2 = mMethod + ":" + mUrl; + + // because we do not preemptively send authorization headers, nc is always 1 + String nc = "00000001"; + 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) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + + byte[] d = md5.digest(param.getBytes()); + if (d != null) { + return bufferToHex(d); + } + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + 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() { + // mConnection is non-null if and only if the requests are synchronous. + if (mConnection != null) { + RequestHandle newHandle = mRequestQueue.queueSynchronousRequest( + mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, + mBodyProvider, mBodyLength); + mRequest = newHandle.mRequest; + mConnection = newHandle.mConnection; + newHandle.processRequest(); + return; + } + mRequest = mRequestQueue.queueRequest( + mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, + mBodyProvider, + mBodyLength).mRequest; + } +} diff --git a/android/src/android/net/http/RequestQueue.java b/android/src/android/net/http/RequestQueue.java new file mode 100644 index 0000000..7d2da1b --- /dev/null +++ b/android/src/android/net/http/RequestQueue.java @@ -0,0 +1,542 @@ +/* + * 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.NetworkInfo; +import android.net.Proxy; +import android.net.WebAddress; +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 { + + + /** + * Requests, indexed by HttpHost (scheme, host, port) + */ + private final LinkedHashMap<HttpHost, LinkedList<Request>> mPending; + private final Context mContext; + private final ActivePool mActivePool; + private final ConnectivityManager mConnectivityManager; + + private HttpHost mProxyHost = null; + private BroadcastReceiver mProxyChangeReceiver; + + /* default simultaneous connection count */ + private static final int CONNECTION_COUNT = 4; + + /** + * 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(); + } + } + + void startConnectionThread() { + synchronized (RequestQueue.this) { + RequestQueue.this.notify(); + } + } + + public void startTiming() { + for (int i = 0; i < mConnectionCount; i++) { + ConnectionThread rt = mThreads[i]; + rt.mCurrentThreadTime = -1; + rt.mTotalThreadTime = 0; + } + mTotalRequest = 0; + mTotalConnection = 0; + } + + public void stopTiming() { + int totalTime = 0; + for (int i = 0; i < mConnectionCount; i++) { + ConnectionThread rt = mThreads[i]; + if (rt.mCurrentThreadTime != -1) { + totalTime += rt.mTotalThreadTime; + } + rt.mCurrentThreadTime = 0; + } + Log.d("Http", "Http thread used " + totalTime + " ms " + " for " + + mTotalRequest + " requests and " + mTotalConnection + + " new 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) { + host = RequestQueue.this.determineHost(host); + Connection con = mIdleCache.getConnection(host); + if (con == null) { + mTotalConnection++; + con = Connection.getConnection(mContext, host, mProxyHost, + RequestQueue.this); + } + return con; + } + public boolean recycleConnection(Connection connection) { + return mIdleCache.cacheConnection(connection.getHost(), 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(); + + mConnectivityManager = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + /** + * 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)); + } + // we need to resample the current proxy setup + setProxyConfig(); + } + + /** + * 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 (mProxyChangeReceiver != null) { + mContext.unregisterReceiver(mProxyChangeReceiver); + mProxyChangeReceiver = null; + } + } + + /** + * Because our IntentReceiver can run within a different thread, + * synchronize setting the proxy + */ + private synchronized void setProxyConfig() { + NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); + if (info != null && info.getType() == 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 + */ + public RequestHandle queueRequest( + String url, String method, + Map<String, String> headers, EventHandler eventHandler, + InputStream bodyProvider, int bodyLength) { + WebAddress uri = new WebAddress(url); + return queueRequest(url, uri, method, headers, eventHandler, + bodyProvider, bodyLength); + } + + /** + * 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 + */ + public RequestHandle queueRequest( + String url, WebAddress uri, String method, Map<String, String> headers, + EventHandler eventHandler, + InputStream bodyProvider, int bodyLength) { + + 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.getHost(), uri.getPort(), uri.getScheme()); + + // set up request + req = new Request(method, httpHost, mProxyHost, uri.getPath(), bodyProvider, + bodyLength, eventHandler, headers); + + queueRequest(req, false); + + mActivePool.mTotalRequest++; + + // dump(); + mActivePool.startConnectionThread(); + + return new RequestHandle( + this, url, uri, method, headers, bodyProvider, bodyLength, + req); + } + + private static class SyncFeeder implements RequestFeeder { + // This is used in the case where the request fails and needs to be + // requeued into the RequestFeeder. + private Request mRequest; + SyncFeeder() { + } + public Request getRequest() { + Request r = mRequest; + mRequest = null; + return r; + } + public Request getRequest(HttpHost host) { + return getRequest(); + } + public boolean haveRequest(HttpHost host) { + return mRequest != null; + } + public void requeueRequest(Request r) { + mRequest = r; + } + } + + public RequestHandle queueSynchronousRequest(String url, WebAddress uri, + String method, Map<String, String> headers, + EventHandler eventHandler, InputStream bodyProvider, + int bodyLength) { + if (HttpLog.LOGV) { + HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri); + } + + HttpHost host = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); + + Request req = new Request(method, host, mProxyHost, uri.getPath(), + bodyProvider, bodyLength, eventHandler, headers); + + // Open a new connection that uses our special RequestFeeder + // implementation. + host = determineHost(host); + Connection conn = Connection.getConnection(mContext, host, mProxyHost, + new SyncFeeder()); + + // TODO: I would like to process the request here but LoadListener + // needs a RequestHandle to process some messages. + return new RequestHandle(this, url, uri, method, headers, bodyProvider, + bodyLength, req, conn); + + } + + // Chooses between the proxy and the request's host. + private HttpHost determineHost(HttpHost host) { + // There used to be a comment in ConnectionThread about t-mob's proxy + // being really bad about https. But, HttpsConnection actually looks + // for a proxy and connects through it anyway. I think that this check + // is still valid because if a site is https, we will use + // HttpsConnection rather than HttpConnection if the proxy address is + // not secure. + return (mProxyHost == null || "https".equals(host.getSchemeName())) + ? host : mProxyHost; + } + + /** + * @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 (!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 (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 { + HttpHost getProxyHost(); + Connection getConnection(Context context, HttpHost host); + boolean recycleConnection(Connection connection); + } +} diff --git a/android/src/com/android/internal/http/multipart/ByteArrayPartSource.java b/android/src/com/android/internal/http/multipart/ByteArrayPartSource.java new file mode 100644 index 0000000..faaac7f --- /dev/null +++ b/android/src/com/android/internal/http/multipart/ByteArrayPartSource.java @@ -0,0 +1,86 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/ByteArrayPartSource.java,v 1.7 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * A PartSource that reads from a byte array. This class should be used when + * the data to post is already loaded into memory. + * + * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> + * + * @since 2.0 + */ +public class ByteArrayPartSource implements PartSource { + + /** Name of the source file. */ + private String fileName; + + /** Byte array of the source file. */ + private byte[] bytes; + + /** + * Constructor for ByteArrayPartSource. + * + * @param fileName the name of the file these bytes represent + * @param bytes the content of this part + */ + public ByteArrayPartSource(String fileName, byte[] bytes) { + + this.fileName = fileName; + this.bytes = bytes; + + } + + /** + * @see PartSource#getLength() + */ + public long getLength() { + return bytes.length; + } + + /** + * @see PartSource#getFileName() + */ + public String getFileName() { + return fileName; + } + + /** + * @see PartSource#createInputStream() + */ + public InputStream createInputStream() { + return new ByteArrayInputStream(bytes); + } + +} diff --git a/android/src/com/android/internal/http/multipart/FilePart.java b/android/src/com/android/internal/http/multipart/FilePart.java new file mode 100644 index 0000000..bfcda00 --- /dev/null +++ b/android/src/com/android/internal/http/multipart/FilePart.java @@ -0,0 +1,254 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/FilePart.java,v 1.19 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.http.util.EncodingUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class implements a part of a Multipart post object that + * consists of a file. + * + * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a> + * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> + * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a> + * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> + * @author <a href="mailto:mdiggory@latte.harvard.edu">Mark Diggory</a> + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> + * + * @since 2.0 + * + */ +public class FilePart extends PartBase { + + /** Default content encoding of file attachments. */ + public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; + + /** Default charset of file attachments. */ + public static final String DEFAULT_CHARSET = "ISO-8859-1"; + + /** Default transfer encoding of file attachments. */ + public static final String DEFAULT_TRANSFER_ENCODING = "binary"; + + /** Log object for this class. */ + private static final Log LOG = LogFactory.getLog(FilePart.class); + + /** Attachment's file name */ + protected static final String FILE_NAME = "; filename="; + + /** Attachment's file name as a byte array */ + private static final byte[] FILE_NAME_BYTES = + EncodingUtils.getAsciiBytes(FILE_NAME); + + /** Source of the file part. */ + private PartSource source; + + /** + * FilePart Constructor. + * + * @param name the name for this part + * @param partSource the source for this part + * @param contentType the content type for this part, if <code>null</code> the + * {@link #DEFAULT_CONTENT_TYPE default} is used + * @param charset the charset encoding for this part, if <code>null</code> the + * {@link #DEFAULT_CHARSET default} is used + */ + public FilePart(String name, PartSource partSource, String contentType, String charset) { + + super( + name, + contentType == null ? DEFAULT_CONTENT_TYPE : contentType, + charset == null ? "ISO-8859-1" : charset, + DEFAULT_TRANSFER_ENCODING + ); + + if (partSource == null) { + throw new IllegalArgumentException("Source may not be null"); + } + this.source = partSource; + } + + /** + * FilePart Constructor. + * + * @param name the name for this part + * @param partSource the source for this part + */ + public FilePart(String name, PartSource partSource) { + this(name, partSource, null, null); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param file the file to post + * + * @throws FileNotFoundException if the <i>file</i> is not a normal + * file or if it is not readable. + */ + public FilePart(String name, File file) + throws FileNotFoundException { + this(name, new FilePartSource(file), null, null); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param file the file to post + * @param contentType the content type for this part, if <code>null</code> the + * {@link #DEFAULT_CONTENT_TYPE default} is used + * @param charset the charset encoding for this part, if <code>null</code> the + * {@link #DEFAULT_CHARSET default} is used + * + * @throws FileNotFoundException if the <i>file</i> is not a normal + * file or if it is not readable. + */ + public FilePart(String name, File file, String contentType, String charset) + throws FileNotFoundException { + this(name, new FilePartSource(file), contentType, charset); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param fileName the file name + * @param file the file to post + * + * @throws FileNotFoundException if the <i>file</i> is not a normal + * file or if it is not readable. + */ + public FilePart(String name, String fileName, File file) + throws FileNotFoundException { + this(name, new FilePartSource(fileName, file), null, null); + } + + /** + * FilePart Constructor. + * + * @param name the name of the file part + * @param fileName the file name + * @param file the file to post + * @param contentType the content type for this part, if <code>null</code> the + * {@link #DEFAULT_CONTENT_TYPE default} is used + * @param charset the charset encoding for this part, if <code>null</code> the + * {@link #DEFAULT_CHARSET default} is used + * + * @throws FileNotFoundException if the <i>file</i> is not a normal + * file or if it is not readable. + */ + public FilePart(String name, String fileName, File file, String contentType, String charset) + throws FileNotFoundException { + this(name, new FilePartSource(fileName, file), contentType, charset); + } + + /** + * Write the disposition header to the output stream + * @param out The output stream + * @throws IOException If an IO problem occurs + * @see Part#sendDispositionHeader(OutputStream) + */ + @Override + protected void sendDispositionHeader(OutputStream out) + throws IOException { + LOG.trace("enter sendDispositionHeader(OutputStream out)"); + super.sendDispositionHeader(out); + String filename = this.source.getFileName(); + if (filename != null) { + out.write(FILE_NAME_BYTES); + out.write(QUOTE_BYTES); + out.write(EncodingUtils.getAsciiBytes(filename)); + out.write(QUOTE_BYTES); + } + } + + /** + * Write the data in "source" to the specified stream. + * @param out The output stream. + * @throws IOException if an IO problem occurs. + * @see Part#sendData(OutputStream) + */ + @Override + protected void sendData(OutputStream out) throws IOException { + LOG.trace("enter sendData(OutputStream out)"); + if (lengthOfData() == 0) { + + // this file contains no data, so there is nothing to send. + // we don't want to create a zero length buffer as this will + // cause an infinite loop when reading. + LOG.debug("No data to send."); + return; + } + + byte[] tmp = new byte[4096]; + InputStream instream = source.createInputStream(); + try { + int len; + while ((len = instream.read(tmp)) >= 0) { + out.write(tmp, 0, len); + } + } finally { + // we're done with the stream, close it + instream.close(); + } + } + + /** + * Returns the source of the file part. + * + * @return The source. + */ + protected PartSource getSource() { + LOG.trace("enter getSource()"); + return this.source; + } + + /** + * Return the length of the data. + * @return The length. + * @see Part#lengthOfData() + */ + @Override + protected long lengthOfData() { + LOG.trace("enter lengthOfData()"); + return source.getLength(); + } + +} diff --git a/android/src/com/android/internal/http/multipart/FilePartSource.java b/android/src/com/android/internal/http/multipart/FilePartSource.java new file mode 100644 index 0000000..eb5cc0f --- /dev/null +++ b/android/src/com/android/internal/http/multipart/FilePartSource.java @@ -0,0 +1,131 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/FilePartSource.java,v 1.10 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * A PartSource that reads from a File. + * + * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> + * @author <a href="mailto:mdiggory@latte.harvard.edu">Mark Diggory</a> + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * + * @since 2.0 + */ +public class FilePartSource implements PartSource { + + /** File part file. */ + private File file = null; + + /** File part file name. */ + private String fileName = null; + + /** + * Constructor for FilePartSource. + * + * @param file the FilePart source File. + * + * @throws FileNotFoundException if the file does not exist or + * cannot be read + */ + public FilePartSource(File file) throws FileNotFoundException { + this.file = file; + if (file != null) { + if (!file.isFile()) { + throw new FileNotFoundException("File is not a normal file."); + } + if (!file.canRead()) { + throw new FileNotFoundException("File is not readable."); + } + this.fileName = file.getName(); + } + } + + /** + * Constructor for FilePartSource. + * + * @param fileName the file name of the FilePart + * @param file the source File for the FilePart + * + * @throws FileNotFoundException if the file does not exist or + * cannot be read + */ + public FilePartSource(String fileName, File file) + throws FileNotFoundException { + this(file); + if (fileName != null) { + this.fileName = fileName; + } + } + + /** + * Return the length of the file + * @return the length of the file. + * @see PartSource#getLength() + */ + public long getLength() { + if (this.file != null) { + return this.file.length(); + } else { + return 0; + } + } + + /** + * Return the current filename + * @return the filename. + * @see PartSource#getFileName() + */ + public String getFileName() { + return (fileName == null) ? "noname" : fileName; + } + + /** + * Return a new {@link FileInputStream} for the current filename. + * @return the new input stream. + * @throws IOException If an IO problem occurs. + * @see PartSource#createInputStream() + */ + public InputStream createInputStream() throws IOException { + if (this.file != null) { + return new FileInputStream(this.file); + } else { + return new ByteArrayInputStream(new byte[] {}); + } + } + +} diff --git a/android/src/com/android/internal/http/multipart/MultipartEntity.java b/android/src/com/android/internal/http/multipart/MultipartEntity.java new file mode 100644 index 0000000..2c5e7f6 --- /dev/null +++ b/android/src/com/android/internal/http/multipart/MultipartEntity.java @@ -0,0 +1,230 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/MultipartRequestEntity.java,v 1.1 2004/10/06 03:39:59 mbecke Exp $ + * $Revision: 502647 $ + * $Date: 2007-02-02 17:22:54 +0100 (Fri, 02 Feb 2007) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Random; + +import org.apache.http.Header; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.message.BasicHeader; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HTTP; +import org.apache.http.util.EncodingUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implements a request entity suitable for an HTTP multipart POST method. + * <p> + * The HTTP multipart POST method is defined in section 3.3 of + * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC1867</a>: + * <blockquote> + * The media-type multipart/form-data follows the rules of all multipart + * MIME data streams as outlined in RFC 1521. The multipart/form-data contains + * a series of parts. Each part is expected to contain a content-disposition + * header where the value is "form-data" and a name attribute specifies + * the field name within the form, e.g., 'content-disposition: form-data; + * name="xxxxx"', where xxxxx is the field name corresponding to that field. + * Field names originally in non-ASCII character sets may be encoded using + * the method outlined in RFC 1522. + * </blockquote> + * </p> + * <p>This entity is designed to be used in conjunction with the + * {@link org.apache.http.HttpRequest} to provide + * multipart posts. Example usage:</p> + * <pre> + * File f = new File("/path/fileToUpload.txt"); + * HttpRequest request = new HttpRequest("http://host/some_path"); + * Part[] parts = { + * new StringPart("param_name", "value"), + * new FilePart(f.getName(), f) + * }; + * filePost.setEntity( + * new MultipartRequestEntity(parts, filePost.getParams()) + * ); + * HttpClient client = new HttpClient(); + * int status = client.executeMethod(filePost); + * </pre> + * + * @since 3.0 + */ +public class MultipartEntity extends AbstractHttpEntity { + + private static final Log log = LogFactory.getLog(MultipartEntity.class); + + /** The Content-Type for multipart/form-data. */ + private static final String MULTIPART_FORM_CONTENT_TYPE = "multipart/form-data"; + + /** + * Sets the value to use as the multipart boundary. + * <p> + * This parameter expects a value if type {@link String}. + * </p> + */ + public static final String MULTIPART_BOUNDARY = "http.method.multipart.boundary"; + + /** + * The pool of ASCII chars to be used for generating a multipart boundary. + */ + private static byte[] MULTIPART_CHARS = EncodingUtils.getAsciiBytes( + "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); + + /** + * Generates a random multipart boundary string. + */ + private static byte[] generateMultipartBoundary() { + Random rand = new Random(); + byte[] bytes = new byte[rand.nextInt(11) + 30]; // a random size from 30 to 40 + for (int i = 0; i < bytes.length; i++) { + bytes[i] = MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]; + } + return bytes; + } + + /** The MIME parts as set by the constructor */ + protected Part[] parts; + + private byte[] multipartBoundary; + + private HttpParams params; + + private boolean contentConsumed = false; + + /** + * Creates a new multipart entity containing the given parts. + * @param parts The parts to include. + * @param params The params of the HttpMethod using this entity. + */ + public MultipartEntity(Part[] parts, HttpParams params) { + if (parts == null) { + throw new IllegalArgumentException("parts cannot be null"); + } + if (params == null) { + throw new IllegalArgumentException("params cannot be null"); + } + this.parts = parts; + this.params = params; + } + + public MultipartEntity(Part[] parts) { + setContentType(MULTIPART_FORM_CONTENT_TYPE); + if (parts == null) { + throw new IllegalArgumentException("parts cannot be null"); + } + this.parts = parts; + this.params = null; + } + + /** + * Returns the MIME boundary string that is used to demarcate boundaries of + * this part. The first call to this method will implicitly create a new + * boundary string. To create a boundary string first the + * HttpMethodParams.MULTIPART_BOUNDARY parameter is considered. Otherwise + * a random one is generated. + * + * @return The boundary string of this entity in ASCII encoding. + */ + protected byte[] getMultipartBoundary() { + if (multipartBoundary == null) { + String temp = null; + if (params != null) { + temp = (String) params.getParameter(MULTIPART_BOUNDARY); + } + if (temp != null) { + multipartBoundary = EncodingUtils.getAsciiBytes(temp); + } else { + multipartBoundary = generateMultipartBoundary(); + } + } + return multipartBoundary; + } + + /** + * Returns <code>true</code> if all parts are repeatable, <code>false</code> otherwise. + */ + public boolean isRepeatable() { + for (int i = 0; i < parts.length; i++) { + if (!parts[i].isRepeatable()) { + return false; + } + } + return true; + } + + /* (non-Javadoc) + */ + public void writeTo(OutputStream out) throws IOException { + Part.sendParts(out, parts, getMultipartBoundary()); + } + /* (non-Javadoc) + * @see org.apache.commons.http.AbstractHttpEntity.#getContentType() + */ + @Override + public Header getContentType() { + StringBuffer buffer = new StringBuffer(MULTIPART_FORM_CONTENT_TYPE); + buffer.append("; boundary="); + buffer.append(EncodingUtils.getAsciiString(getMultipartBoundary())); + return new BasicHeader(HTTP.CONTENT_TYPE, buffer.toString()); + + } + + /* (non-Javadoc) + */ + public long getContentLength() { + try { + return Part.getLengthOfParts(parts, getMultipartBoundary()); + } catch (Exception e) { + log.error("An exception occurred while getting the length of the parts", e); + return 0; + } + } + + public InputStream getContent() throws IOException, IllegalStateException { + if(!isRepeatable() && this.contentConsumed ) { + throw new IllegalStateException("Content has been consumed"); + } + this.contentConsumed = true; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Part.sendParts(baos, this.parts, this.multipartBoundary); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + return bais; + } + + public boolean isStreaming() { + return false; + } +} diff --git a/android/src/com/android/internal/http/multipart/Part.java b/android/src/com/android/internal/http/multipart/Part.java new file mode 100644 index 0000000..cb1b546 --- /dev/null +++ b/android/src/com/android/internal/http/multipart/Part.java @@ -0,0 +1,439 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/Part.java,v 1.16 2005/01/14 21:16:40 olegk Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.http.util.EncodingUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstract class for one Part of a multipart post object. + * + * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a> + * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> + * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a> + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> + * + * @since 2.0 + */ +public abstract class Part { + + /** Log object for this class. */ + private static final Log LOG = LogFactory.getLog(Part.class); + + /** + * The boundary + * @deprecated use {@link org.apache.http.client.methods.multipart#MULTIPART_BOUNDARY} + */ + protected static final String BOUNDARY = "----------------314159265358979323846"; + + /** + * The boundary as a byte array. + * @deprecated + */ + protected static final byte[] BOUNDARY_BYTES = EncodingUtils.getAsciiBytes(BOUNDARY); + + /** + * The default boundary to be used if {@link #setPartBoundary(byte[])} has not + * been called. + */ + private static final byte[] DEFAULT_BOUNDARY_BYTES = BOUNDARY_BYTES; + + /** Carriage return/linefeed */ + protected static final String CRLF = "\r\n"; + + /** Carriage return/linefeed as a byte array */ + protected static final byte[] CRLF_BYTES = EncodingUtils.getAsciiBytes(CRLF); + + /** Content dispostion characters */ + protected static final String QUOTE = "\""; + + /** Content dispostion as a byte array */ + protected static final byte[] QUOTE_BYTES = + EncodingUtils.getAsciiBytes(QUOTE); + + /** Extra characters */ + protected static final String EXTRA = "--"; + + /** Extra characters as a byte array */ + protected static final byte[] EXTRA_BYTES = + EncodingUtils.getAsciiBytes(EXTRA); + + /** Content dispostion characters */ + protected static final String CONTENT_DISPOSITION = "Content-Disposition: form-data; name="; + + /** Content dispostion as a byte array */ + protected static final byte[] CONTENT_DISPOSITION_BYTES = + EncodingUtils.getAsciiBytes(CONTENT_DISPOSITION); + + /** Content type header */ + protected static final String CONTENT_TYPE = "Content-Type: "; + + /** Content type header as a byte array */ + protected static final byte[] CONTENT_TYPE_BYTES = + EncodingUtils.getAsciiBytes(CONTENT_TYPE); + + /** Content charset */ + protected static final String CHARSET = "; charset="; + + /** Content charset as a byte array */ + protected static final byte[] CHARSET_BYTES = + EncodingUtils.getAsciiBytes(CHARSET); + + /** Content type header */ + protected static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding: "; + + /** Content type header as a byte array */ + protected static final byte[] CONTENT_TRANSFER_ENCODING_BYTES = + EncodingUtils.getAsciiBytes(CONTENT_TRANSFER_ENCODING); + + /** + * Return the boundary string. + * @return the boundary string + * @deprecated uses a constant string. Rather use {@link #getPartBoundary} + */ + public static String getBoundary() { + return BOUNDARY; + } + + /** + * The ASCII bytes to use as the multipart boundary. + */ + private byte[] boundaryBytes; + + /** + * Return the name of this part. + * @return The name. + */ + public abstract String getName(); + + /** + * Returns the content type of this part. + * @return the content type, or <code>null</code> to exclude the content type header + */ + public abstract String getContentType(); + + /** + * Return the character encoding of this part. + * @return the character encoding, or <code>null</code> to exclude the character + * encoding header + */ + public abstract String getCharSet(); + + /** + * Return the transfer encoding of this part. + * @return the transfer encoding, or <code>null</code> to exclude the transfer encoding header + */ + public abstract String getTransferEncoding(); + + /** + * Gets the part boundary to be used. + * @return the part boundary as an array of bytes. + * + * @since 3.0 + */ + protected byte[] getPartBoundary() { + if (boundaryBytes == null) { + // custom boundary bytes have not been set, use the default. + return DEFAULT_BOUNDARY_BYTES; + } else { + return boundaryBytes; + } + } + + /** + * Sets the part boundary. Only meant to be used by + * {@link Part#sendParts(OutputStream, Part[], byte[])} + * and {@link Part#getLengthOfParts(Part[], byte[])} + * @param boundaryBytes An array of ASCII bytes. + * @since 3.0 + */ + void setPartBoundary(byte[] boundaryBytes) { + this.boundaryBytes = boundaryBytes; + } + + /** + * Tests if this part can be sent more than once. + * @return <code>true</code> if {@link #sendData(OutputStream)} can be successfully called + * more than once. + * @since 3.0 + */ + public boolean isRepeatable() { + return true; + } + + /** + * Write the start to the specified output stream + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected void sendStart(OutputStream out) throws IOException { + LOG.trace("enter sendStart(OutputStream out)"); + out.write(EXTRA_BYTES); + out.write(getPartBoundary()); + out.write(CRLF_BYTES); + } + + /** + * Write the content disposition header to the specified output stream + * + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected void sendDispositionHeader(OutputStream out) throws IOException { + LOG.trace("enter sendDispositionHeader(OutputStream out)"); + out.write(CONTENT_DISPOSITION_BYTES); + out.write(QUOTE_BYTES); + out.write(EncodingUtils.getAsciiBytes(getName())); + out.write(QUOTE_BYTES); + } + + /** + * Write the content type header to the specified output stream + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected void sendContentTypeHeader(OutputStream out) throws IOException { + LOG.trace("enter sendContentTypeHeader(OutputStream out)"); + String contentType = getContentType(); + if (contentType != null) { + out.write(CRLF_BYTES); + out.write(CONTENT_TYPE_BYTES); + out.write(EncodingUtils.getAsciiBytes(contentType)); + String charSet = getCharSet(); + if (charSet != null) { + out.write(CHARSET_BYTES); + out.write(EncodingUtils.getAsciiBytes(charSet)); + } + } + } + + /** + * Write the content transfer encoding header to the specified + * output stream + * + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected void sendTransferEncodingHeader(OutputStream out) throws IOException { + LOG.trace("enter sendTransferEncodingHeader(OutputStream out)"); + String transferEncoding = getTransferEncoding(); + if (transferEncoding != null) { + out.write(CRLF_BYTES); + out.write(CONTENT_TRANSFER_ENCODING_BYTES); + out.write(EncodingUtils.getAsciiBytes(transferEncoding)); + } + } + + /** + * Write the end of the header to the output stream + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected void sendEndOfHeader(OutputStream out) throws IOException { + LOG.trace("enter sendEndOfHeader(OutputStream out)"); + out.write(CRLF_BYTES); + out.write(CRLF_BYTES); + } + + /** + * Write the data to the specified output stream + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected abstract void sendData(OutputStream out) throws IOException; + + /** + * Return the length of the main content + * + * @return long The length. + * @throws IOException If an IO problem occurs + */ + protected abstract long lengthOfData() throws IOException; + + /** + * Write the end data to the output stream. + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + protected void sendEnd(OutputStream out) throws IOException { + LOG.trace("enter sendEnd(OutputStream out)"); + out.write(CRLF_BYTES); + } + + /** + * Write all the data to the output stream. + * If you override this method make sure to override + * #length() as well + * + * @param out The output stream + * @throws IOException If an IO problem occurs. + */ + public void send(OutputStream out) throws IOException { + LOG.trace("enter send(OutputStream out)"); + sendStart(out); + sendDispositionHeader(out); + sendContentTypeHeader(out); + sendTransferEncodingHeader(out); + sendEndOfHeader(out); + sendData(out); + sendEnd(out); + } + + + /** + * Return the full length of all the data. + * If you override this method make sure to override + * #send(OutputStream) as well + * + * @return long The length. + * @throws IOException If an IO problem occurs + */ + public long length() throws IOException { + LOG.trace("enter length()"); + if (lengthOfData() < 0) { + return -1; + } + ByteArrayOutputStream overhead = new ByteArrayOutputStream(); + sendStart(overhead); + sendDispositionHeader(overhead); + sendContentTypeHeader(overhead); + sendTransferEncodingHeader(overhead); + sendEndOfHeader(overhead); + sendEnd(overhead); + return overhead.size() + lengthOfData(); + } + + /** + * Return a string representation of this object. + * @return A string representation of this object. + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return this.getName(); + } + + /** + * Write all parts and the last boundary to the specified output stream. + * + * @param out The stream to write to. + * @param parts The parts to write. + * + * @throws IOException If an I/O error occurs while writing the parts. + */ + public static void sendParts(OutputStream out, final Part[] parts) + throws IOException { + sendParts(out, parts, DEFAULT_BOUNDARY_BYTES); + } + + /** + * Write all parts and the last boundary to the specified output stream. + * + * @param out The stream to write to. + * @param parts The parts to write. + * @param partBoundary The ASCII bytes to use as the part boundary. + * + * @throws IOException If an I/O error occurs while writing the parts. + * + * @since 3.0 + */ + public static void sendParts(OutputStream out, Part[] parts, byte[] partBoundary) + throws IOException { + + if (parts == null) { + throw new IllegalArgumentException("Parts may not be null"); + } + if (partBoundary == null || partBoundary.length == 0) { + throw new IllegalArgumentException("partBoundary may not be empty"); + } + for (int i = 0; i < parts.length; i++) { + // set the part boundary before the part is sent + parts[i].setPartBoundary(partBoundary); + parts[i].send(out); + } + out.write(EXTRA_BYTES); + out.write(partBoundary); + out.write(EXTRA_BYTES); + out.write(CRLF_BYTES); + } + + /** + * Return the total sum of all parts and that of the last boundary + * + * @param parts The parts. + * @return The total length + * + * @throws IOException If an I/O error occurs while writing the parts. + */ + public static long getLengthOfParts(Part[] parts) + throws IOException { + return getLengthOfParts(parts, DEFAULT_BOUNDARY_BYTES); + } + + /** + * Gets the length of the multipart message including the given parts. + * + * @param parts The parts. + * @param partBoundary The ASCII bytes to use as the part boundary. + * @return The total length + * + * @throws IOException If an I/O error occurs while writing the parts. + * + * @since 3.0 + */ + public static long getLengthOfParts(Part[] parts, byte[] partBoundary) throws IOException { + LOG.trace("getLengthOfParts(Parts[])"); + if (parts == null) { + throw new IllegalArgumentException("Parts may not be null"); + } + long total = 0; + for (int i = 0; i < parts.length; i++) { + // set the part boundary before we calculate the part's length + parts[i].setPartBoundary(partBoundary); + long l = parts[i].length(); + if (l < 0) { + return -1; + } + total += l; + } + total += EXTRA_BYTES.length; + total += partBoundary.length; + total += EXTRA_BYTES.length; + total += CRLF_BYTES.length; + return total; + } +} diff --git a/android/src/com/android/internal/http/multipart/PartBase.java b/android/src/com/android/internal/http/multipart/PartBase.java new file mode 100644 index 0000000..876d15d --- /dev/null +++ b/android/src/com/android/internal/http/multipart/PartBase.java @@ -0,0 +1,150 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/PartBase.java,v 1.5 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + + +/** + * Provides setters and getters for the basic Part properties. + * + * @author Michael Becke + */ +public abstract class PartBase extends Part { + + /** Name of the file part. */ + private String name; + + /** Content type of the file part. */ + private String contentType; + + /** Content encoding of the file part. */ + private String charSet; + + /** The transfer encoding. */ + private String transferEncoding; + + /** + * Constructor. + * + * @param name The name of the part + * @param contentType The content type, or <code>null</code> + * @param charSet The character encoding, or <code>null</code> + * @param transferEncoding The transfer encoding, or <code>null</code> + */ + public PartBase(String name, String contentType, String charSet, String transferEncoding) { + + if (name == null) { + throw new IllegalArgumentException("Name must not be null"); + } + this.name = name; + this.contentType = contentType; + this.charSet = charSet; + this.transferEncoding = transferEncoding; + } + + /** + * Returns the name. + * @return The name. + * @see Part#getName() + */ + @Override + public String getName() { + return this.name; + } + + /** + * Returns the content type of this part. + * @return String The name. + */ + @Override + public String getContentType() { + return this.contentType; + } + + /** + * Return the character encoding of this part. + * @return String The name. + */ + @Override + public String getCharSet() { + return this.charSet; + } + + /** + * Returns the transfer encoding of this part. + * @return String The name. + */ + @Override + public String getTransferEncoding() { + return transferEncoding; + } + + /** + * Sets the character encoding. + * + * @param charSet the character encoding, or <code>null</code> to exclude the character + * encoding header + */ + public void setCharSet(String charSet) { + this.charSet = charSet; + } + + /** + * Sets the content type. + * + * @param contentType the content type, or <code>null</code> to exclude the content type header + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Sets the part name. + * + * @param name + */ + public void setName(String name) { + if (name == null) { + throw new IllegalArgumentException("Name must not be null"); + } + this.name = name; + } + + /** + * Sets the transfer encoding. + * + * @param transferEncoding the transfer encoding, or <code>null</code> to exclude the + * transfer encoding header + */ + public void setTransferEncoding(String transferEncoding) { + this.transferEncoding = transferEncoding; + } + +} diff --git a/android/src/com/android/internal/http/multipart/PartSource.java b/android/src/com/android/internal/http/multipart/PartSource.java new file mode 100644 index 0000000..3740696 --- /dev/null +++ b/android/src/com/android/internal/http/multipart/PartSource.java @@ -0,0 +1,72 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/PartSource.java,v 1.6 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.IOException; +import java.io.InputStream; + +/** + * An interface for providing access to data when posting MultiPart messages. + * + * @see FilePart + * + * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a> + * + * @since 2.0 + */ +public interface PartSource { + + /** + * Gets the number of bytes contained in this source. + * + * @return a value >= 0 + */ + long getLength(); + + /** + * Gets the name of the file this source represents. + * + * @return the fileName used for posting a MultiPart file part + */ + String getFileName(); + + /** + * Gets a new InputStream for reading this source. This method can be + * called more than once and should therefore return a new stream every + * time. + * + * @return a new InputStream + * + * @throws IOException if an error occurs when creating the InputStream + */ + InputStream createInputStream() throws IOException; + +} diff --git a/android/src/com/android/internal/http/multipart/StringPart.java b/android/src/com/android/internal/http/multipart/StringPart.java new file mode 100644 index 0000000..c98257e --- /dev/null +++ b/android/src/com/android/internal/http/multipart/StringPart.java @@ -0,0 +1,150 @@ +/* + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/StringPart.java,v 1.11 2004/04/18 23:51:37 jsdever Exp $ + * $Revision: 480424 $ + * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package com.android.internal.http.multipart; + +import java.io.OutputStream; +import java.io.IOException; + +import org.apache.http.util.EncodingUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Simple string parameter for a multipart post + * + * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a> + * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> + * + * @since 2.0 + */ +public class StringPart extends PartBase { + + /** Log object for this class. */ + private static final Log LOG = LogFactory.getLog(StringPart.class); + + /** Default content encoding of string parameters. */ + public static final String DEFAULT_CONTENT_TYPE = "text/plain"; + + /** Default charset of string parameters*/ + public static final String DEFAULT_CHARSET = "US-ASCII"; + + /** Default transfer encoding of string parameters*/ + public static final String DEFAULT_TRANSFER_ENCODING = "8bit"; + + /** Contents of this StringPart. */ + private byte[] content; + + /** The String value of this part. */ + private String value; + + /** + * Constructor. + * + * @param name The name of the part + * @param value the string to post + * @param charset the charset to be used to encode the string, if <code>null</code> + * the {@link #DEFAULT_CHARSET default} is used + */ + public StringPart(String name, String value, String charset) { + + super( + name, + DEFAULT_CONTENT_TYPE, + charset == null ? DEFAULT_CHARSET : charset, + DEFAULT_TRANSFER_ENCODING + ); + if (value == null) { + throw new IllegalArgumentException("Value may not be null"); + } + if (value.indexOf(0) != -1) { + // See RFC 2048, 2.8. "8bit Data" + throw new IllegalArgumentException("NULs may not be present in string parts"); + } + this.value = value; + } + + /** + * Constructor. + * + * @param name The name of the part + * @param value the string to post + */ + public StringPart(String name, String value) { + this(name, value, null); + } + + /** + * Gets the content in bytes. Bytes are lazily created to allow the charset to be changed + * after the part is created. + * + * @return the content in bytes + */ + private byte[] getContent() { + if (content == null) { + content = EncodingUtils.getBytes(value, getCharSet()); + } + return content; + } + + /** + * Writes the data to the given OutputStream. + * @param out the OutputStream to write to + * @throws IOException if there is a write error + */ + @Override + protected void sendData(OutputStream out) throws IOException { + LOG.trace("enter sendData(OutputStream)"); + out.write(getContent()); + } + + /** + * Return the length of the data. + * @return The length of the data. + * @see Part#lengthOfData() + */ + @Override + protected long lengthOfData() { + LOG.trace("enter lengthOfData()"); + return getContent().length; + } + + /* (non-Javadoc) + * @see org.apache.commons.httpclient.methods.multipart.BasePart#setCharSet(java.lang.String) + */ + @Override + public void setCharSet(String charSet) { + super.setCharSet(charSet); + this.content = null; + } + +} diff --git a/src/org/apache/http/conn/ConnectTimeoutException.java b/src/org/apache/http/conn/ConnectTimeoutException.java deleted file mode 100644 index 6cc6922..0000000 --- a/src/org/apache/http/conn/ConnectTimeoutException.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ConnectTimeoutException.java $ - * $Revision: 617645 $ - * $Date: 2008-02-01 13:05:31 -0800 (Fri, 01 Feb 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn; - -import java.io.InterruptedIOException; - -/** - * A timeout while connecting to an HTTP server or waiting for an - * available connection from an HttpConnectionManager. - * - * @author <a href="mailto:laura@lwerner.org">Laura Werner</a> - * - * @since 4.0 - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public class ConnectTimeoutException extends InterruptedIOException { - - private static final long serialVersionUID = -4816682903149535989L; - - /** - * Creates a ConnectTimeoutException with a <tt>null</tt> detail message. - */ - public ConnectTimeoutException() { - super(); - } - - /** - * Creates a ConnectTimeoutException with the specified detail message. - * - * @param message The exception detail message - */ - public ConnectTimeoutException(final String message) { - super(message); - } - -} diff --git a/src/org/apache/http/conn/scheme/HostNameResolver.java b/src/org/apache/http/conn/scheme/HostNameResolver.java deleted file mode 100644 index d488a4b..0000000 --- a/src/org/apache/http/conn/scheme/HostNameResolver.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * $HeadURL:$ - * $Revision:$ - * $Date:$ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.scheme; - -import java.io.IOException; -import java.net.InetAddress; -/** - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. -*/ - -@Deprecated -public interface HostNameResolver { - - InetAddress resolve (String hostname) throws IOException; - -} diff --git a/src/org/apache/http/conn/scheme/LayeredSocketFactory.java b/src/org/apache/http/conn/scheme/LayeredSocketFactory.java deleted file mode 100644 index b9f5348..0000000 --- a/src/org/apache/http/conn/scheme/LayeredSocketFactory.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/scheme/LayeredSocketFactory.java $ - * $Revision: 645850 $ - * $Date: 2008-04-08 04:08:52 -0700 (Tue, 08 Apr 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.scheme; - -import java.io.IOException; -import java.net.Socket; -import java.net.UnknownHostException; - -/** - * A {@link SocketFactory SocketFactory} for layered sockets (SSL/TLS). - * See there for things to consider when implementing a socket factory. - * - * @author Michael Becke - * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> - * @since 4.0 - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public interface LayeredSocketFactory extends SocketFactory { - - /** - * Returns a socket connected to the given host that is layered over an - * existing socket. Used primarily for creating secure sockets through - * proxies. - * - * @param socket the existing socket - * @param host the host name/IP - * @param port the port on the host - * @param autoClose a flag for closing the underling socket when the created - * socket is closed - * - * @return Socket a new socket - * - * @throws IOException if an I/O error occurs while creating the socket - * @throws UnknownHostException if the IP address of the host cannot be - * determined - */ - Socket createSocket( - Socket socket, - String host, - int port, - boolean autoClose - ) throws IOException, UnknownHostException; - -} diff --git a/src/org/apache/http/conn/scheme/SocketFactory.java b/src/org/apache/http/conn/scheme/SocketFactory.java deleted file mode 100644 index c6bc03c..0000000 --- a/src/org/apache/http/conn/scheme/SocketFactory.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/scheme/SocketFactory.java $ - * $Revision: 645850 $ - * $Date: 2008-04-08 04:08:52 -0700 (Tue, 08 Apr 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.scheme; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; - -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.params.HttpParams; - -/** - * A factory for creating and connecting sockets. - * The factory encapsulates the logic for establishing a socket connection. - * <br/> - * Both {@link java.lang.Object#equals(java.lang.Object) Object.equals()} - * and {@link java.lang.Object#hashCode() Object.hashCode()} - * must be overridden for the correct operation of some connection managers. - * - * @author <a href="mailto:rolandw at apache.org">Roland Weber</a> - * @author Michael Becke - * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public interface SocketFactory { - - /** - * Creates a new, unconnected socket. - * The socket should subsequently be passed to - * {@link #connectSocket connectSocket}. - * - * @return a new socket - * - * @throws IOException if an I/O error occurs while creating the socket - */ - Socket createSocket() - throws IOException - ; - - - /** - * Connects a socket to the given host. - * - * @param sock the socket to connect, as obtained from - * {@link #createSocket createSocket}. - * <code>null</code> indicates that a new socket - * should be created and connected. - * @param host the host to connect to - * @param port the port to connect to on the host - * @param localAddress the local address to bind the socket to, or - * <code>null</code> for any - * @param localPort the port on the local machine, - * 0 or a negative number for any - * @param params additional {@link HttpParams parameters} for connecting - * - * @return the connected socket. The returned object may be different - * from the <code>sock</code> argument if this factory supports - * a layered protocol. - * - * @throws IOException if an I/O error occurs - * @throws UnknownHostException if the IP address of the target host - * can not be determined - * @throws ConnectTimeoutException if the socket cannot be connected - * within the time limit defined in the <code>params</code> - */ - Socket connectSocket( - Socket sock, - String host, - int port, - InetAddress localAddress, - int localPort, - HttpParams params - ) throws IOException, UnknownHostException, ConnectTimeoutException; - - - /** - * Checks whether a socket provides a secure connection. - * The socket must be {@link #connectSocket connected} - * by this factory. - * The factory will <i>not</i> perform I/O operations - * in this method. - * <br/> - * As a rule of thumb, plain sockets are not secure and - * TLS/SSL sockets are secure. However, there may be - * application specific deviations. For example, a plain - * socket to a host in the same intranet ("trusted zone") - * could be considered secure. On the other hand, a - * TLS/SSL socket could be considered insecure based on - * the cypher suite chosen for the connection. - * - * @param sock the connected socket to check - * - * @return <code>true</code> if the connection of the socket - * should be considered secure, or - * <code>false</code> if it should not - * - * @throws IllegalArgumentException - * if the argument is invalid, for example because it is - * not a connected socket or was created by a different - * socket factory. - * Note that socket factories are <i>not</i> required to - * check these conditions, they may simply return a default - * value when called with an invalid socket argument. - */ - boolean isSecure(Socket sock) - throws IllegalArgumentException - ; - -} diff --git a/src/org/apache/http/conn/ssl/AbstractVerifier.java b/src/org/apache/http/conn/ssl/AbstractVerifier.java deleted file mode 100644 index a56a6d4..0000000 --- a/src/org/apache/http/conn/ssl/AbstractVerifier.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java $ - * $Revision: 653041 $ - * $Date: 2008-05-03 03:39:28 -0700 (Sat, 03 May 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.ssl; - -import org.apache.http.conn.util.InetAddressUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.security.cert.Certificate; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.logging.Logger; -import java.util.logging.Level; - -import javax.net.ssl.DistinguishedNameParser; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - -/** - * Abstract base class for all standard {@link X509HostnameVerifier} - * implementations. - * - * @author Julius Davies - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public abstract class AbstractVerifier implements X509HostnameVerifier { - - /** - * This contains a list of 2nd-level domains that aren't allowed to - * have wildcards when combined with country-codes. - * For example: [*.co.uk]. - * <p/> - * The [*.co.uk] problem is an interesting one. Should we just hope - * that CA's would never foolishly allow such a certificate to happen? - * Looks like we're the only implementation guarding against this. - * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. - */ - private final static String[] BAD_COUNTRY_2LDS = - { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", - "lg", "ne", "net", "or", "org" }; - - static { - // Just in case developer forgot to manually sort the array. :-) - Arrays.sort(BAD_COUNTRY_2LDS); - } - - public AbstractVerifier() { - super(); - } - - public final void verify(String host, SSLSocket ssl) - throws IOException { - if(host == null) { - throw new NullPointerException("host to verify is null"); - } - - SSLSession session = ssl.getSession(); - Certificate[] certs = session.getPeerCertificates(); - X509Certificate x509 = (X509Certificate) certs[0]; - verify(host, x509); - } - - public final boolean verify(String host, SSLSession session) { - try { - Certificate[] certs = session.getPeerCertificates(); - X509Certificate x509 = (X509Certificate) certs[0]; - verify(host, x509); - return true; - } - catch(SSLException e) { - return false; - } - } - - public final void verify(String host, X509Certificate cert) - throws SSLException { - String[] cns = getCNs(cert); - String[] subjectAlts = getDNSSubjectAlts(cert); - verify(host, cns, subjectAlts); - } - - public final void verify(final String host, final String[] cns, - final String[] subjectAlts, - final boolean strictWithSubDomains) - throws SSLException { - - // Build the list of names we're going to check. Our DEFAULT and - // STRICT implementations of the HostnameVerifier only use the - // first CN provided. All other CNs are ignored. - // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way). - LinkedList<String> names = new LinkedList<String>(); - if(cns != null && cns.length > 0 && cns[0] != null) { - names.add(cns[0]); - } - if(subjectAlts != null) { - for (String subjectAlt : subjectAlts) { - if (subjectAlt != null) { - names.add(subjectAlt); - } - } - } - - if(names.isEmpty()) { - String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt"; - throw new SSLException(msg); - } - - // StringBuffer for building the error message. - StringBuffer buf = new StringBuffer(); - - // We're can be case-insensitive when comparing the host we used to - // establish the socket to the hostname in the certificate. - String hostName = host.trim().toLowerCase(Locale.ENGLISH); - boolean match = false; - for(Iterator<String> it = names.iterator(); it.hasNext();) { - // Don't trim the CN, though! - String cn = it.next(); - cn = cn.toLowerCase(Locale.ENGLISH); - // Store CN in StringBuffer in case we need to report an error. - buf.append(" <"); - buf.append(cn); - buf.append('>'); - if(it.hasNext()) { - buf.append(" OR"); - } - - // The CN better have at least two dots if it wants wildcard - // action. It also can't be [*.co.uk] or [*.co.jp] or - // [*.org.uk], etc... - boolean doWildcard = cn.startsWith("*.") && - cn.indexOf('.', 2) != -1 && - acceptableCountryWildcard(cn) && - !InetAddressUtils.isIPv4Address(host); - - if(doWildcard) { - match = hostName.endsWith(cn.substring(1)); - if(match && strictWithSubDomains) { - // If we're in strict mode, then [*.foo.com] is not - // allowed to match [a.b.foo.com] - match = countDots(hostName) == countDots(cn); - } - } else { - match = hostName.equals(cn); - } - if(match) { - break; - } - } - if(!match) { - throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf); - } - } - - public static boolean acceptableCountryWildcard(String cn) { - int cnLen = cn.length(); - if(cnLen >= 7 && cnLen <= 9) { - // Look for the '.' in the 3rd-last position: - if(cn.charAt(cnLen - 3) == '.') { - // Trim off the [*.] and the [.XX]. - String s = cn.substring(2, cnLen - 3); - // And test against the sorted array of bad 2lds: - int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); - return x < 0; - } - } - return true; - } - - public static String[] getCNs(X509Certificate cert) { - DistinguishedNameParser dnParser = - new DistinguishedNameParser(cert.getSubjectX500Principal()); - List<String> cnList = dnParser.getAllMostSpecificFirst("cn"); - - if(!cnList.isEmpty()) { - String[] cns = new String[cnList.size()]; - cnList.toArray(cns); - return cns; - } else { - return null; - } - } - - - /** - * Extracts the array of SubjectAlt DNS names from an X509Certificate. - * Returns null if there aren't any. - * <p/> - * Note: Java doesn't appear able to extract international characters - * from the SubjectAlts. It can only extract international characters - * from the CN field. - * <p/> - * (Or maybe the version of OpenSSL I'm using to test isn't storing the - * international characters correctly in the SubjectAlts?). - * - * @param cert X509Certificate - * @return Array of SubjectALT DNS names stored in the certificate. - */ - public static String[] getDNSSubjectAlts(X509Certificate cert) { - LinkedList<String> subjectAltList = new LinkedList<String>(); - Collection<List<?>> c = null; - try { - c = cert.getSubjectAlternativeNames(); - } - catch(CertificateParsingException cpe) { - Logger.getLogger(AbstractVerifier.class.getName()) - .log(Level.FINE, "Error parsing certificate.", cpe); - } - if(c != null) { - for (List<?> aC : c) { - List<?> list = aC; - int type = ((Integer) list.get(0)).intValue(); - // If type is 2, then we've got a dNSName - if (type == 2) { - String s = (String) list.get(1); - subjectAltList.add(s); - } - } - } - if(!subjectAltList.isEmpty()) { - String[] subjectAlts = new String[subjectAltList.size()]; - subjectAltList.toArray(subjectAlts); - return subjectAlts; - } else { - return null; - } - } - - /** - * Counts the number of dots "." in a string. - * @param s string to count dots from - * @return number of dots - */ - public static int countDots(final String s) { - int count = 0; - for(int i = 0; i < s.length(); i++) { - if(s.charAt(i) == '.') { - count++; - } - } - return count; - } - -} diff --git a/src/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java b/src/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java deleted file mode 100644 index c2bf4c4..0000000 --- a/src/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/AllowAllHostnameVerifier.java $ - * $Revision: 617642 $ - * $Date: 2008-02-01 12:54:07 -0800 (Fri, 01 Feb 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.ssl; - -/** - * The ALLOW_ALL HostnameVerifier essentially turns hostname verification - * off. This implementation is a no-op, and never throws the SSLException. - * - * @author Julius Davies - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public class AllowAllHostnameVerifier extends AbstractVerifier { - - public final void verify( - final String host, - final String[] cns, - final String[] subjectAlts) { - // Allow everything - so never blowup. - } - - @Override - public final String toString() { - return "ALLOW_ALL"; - } - -} diff --git a/src/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java b/src/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java deleted file mode 100644 index 48a7bf9..0000000 --- a/src/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/BrowserCompatHostnameVerifier.java $ - * $Revision: 617642 $ - * $Date: 2008-02-01 12:54:07 -0800 (Fri, 01 Feb 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.ssl; - -import javax.net.ssl.SSLException; - -/** - * The HostnameVerifier that works the same way as Curl and Firefox. - * <p/> - * The hostname must match either the first CN, or any of the subject-alts. - * A wildcard can occur in the CN, and in any of the subject-alts. - * <p/> - * The only difference between BROWSER_COMPATIBLE and STRICT is that a wildcard - * (such as "*.foo.com") with BROWSER_COMPATIBLE matches all subdomains, - * including "a.b.foo.com". - * - * @author Julius Davies - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public class BrowserCompatHostnameVerifier extends AbstractVerifier { - - public final void verify( - final String host, - final String[] cns, - final String[] subjectAlts) throws SSLException { - verify(host, cns, subjectAlts, false); - } - - @Override - public final String toString() { - return "BROWSER_COMPATIBLE"; - } - -} diff --git a/src/org/apache/http/conn/ssl/SSLSocketFactory.java b/src/org/apache/http/conn/ssl/SSLSocketFactory.java deleted file mode 100644 index 1e46fee..0000000 --- a/src/org/apache/http/conn/ssl/SSLSocketFactory.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/SSLSocketFactory.java $ - * $Revision: 659194 $ - * $Date: 2008-05-22 11:33:47 -0700 (Thu, 22 May 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.ssl; - -import org.apache.http.conn.scheme.HostNameResolver; -import org.apache.http.conn.scheme.LayeredSocketFactory; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.UnrecoverableKeyException; - -/** - * Layered socket factory for TLS/SSL connections, based on JSSE. - *. - * <p> - * SSLSocketFactory can be used to validate the identity of the HTTPS - * server against a list of trusted certificates and to authenticate to - * the HTTPS server using a private key. - * </p> - * - * <p> - * SSLSocketFactory will enable server authentication when supplied with - * a {@link KeyStore truststore} file containg one or several trusted - * certificates. The client secure socket will reject the connection during - * the SSL session handshake if the target HTTPS server attempts to - * authenticate itself with a non-trusted certificate. - * </p> - * - * <p> - * Use JDK keytool utility to import a trusted certificate and generate a truststore file: - * <pre> - * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore - * </pre> - * </p> - * - * <p> - * SSLSocketFactory will enable client authentication when supplied with - * a {@link KeyStore keystore} file containg a private key/public certificate - * pair. The client secure socket will use the private key to authenticate - * itself to the target HTTPS server during the SSL session handshake if - * requested to do so by the server. - * The target HTTPS server will in its turn verify the certificate presented - * by the client in order to establish client's authenticity - * </p> - * - * <p> - * Use the following sequence of actions to generate a keystore file - * </p> - * <ul> - * <li> - * <p> - * Use JDK keytool utility to generate a new key - * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre> - * For simplicity use the same password for the key as that of the keystore - * </p> - * </li> - * <li> - * <p> - * Issue a certificate signing request (CSR) - * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre> - * </p> - * </li> - * <li> - * <p> - * Send the certificate request to the trusted Certificate Authority for signature. - * One may choose to act as her own CA and sign the certificate request using a PKI - * tool, such as OpenSSL. - * </p> - * </li> - * <li> - * <p> - * Import the trusted CA root certificate - * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre> - * </p> - * </li> - * <li> - * <p> - * Import the PKCS#7 file containg the complete certificate chain - * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre> - * </p> - * </li> - * <li> - * <p> - * Verify the content the resultant keystore file - * <pre>keytool -list -v -keystore my.keystore</pre> - * </p> - * </li> - * </ul> - * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> - * @author Julius Davies - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ - -@Deprecated -public class SSLSocketFactory implements LayeredSocketFactory { - - public static final String TLS = "TLS"; - public static final String SSL = "SSL"; - public static final String SSLV2 = "SSLv2"; - - public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER - = new AllowAllHostnameVerifier(); - - public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER - = new BrowserCompatHostnameVerifier(); - - public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER - = new StrictHostnameVerifier(); - - /* - * Put defaults into holder class to avoid class preloading creating an - * instance of the classes referenced. - */ - private static class NoPreloadHolder { - /** - * The factory using the default JVM settings for secure connections. - */ - private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory(); - } - - /** - * Gets an singleton instance of the SSLProtocolSocketFactory. - * @return a SSLProtocolSocketFactory - */ - public static SSLSocketFactory getSocketFactory() { - return NoPreloadHolder.DEFAULT_FACTORY; - } - - private final SSLContext sslcontext; - private final javax.net.ssl.SSLSocketFactory socketfactory; - private final HostNameResolver nameResolver; - private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER; - - public SSLSocketFactory( - String algorithm, - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore, - final SecureRandom random, - final HostNameResolver nameResolver) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException - { - super(); - if (algorithm == null) { - algorithm = TLS; - } - KeyManager[] keymanagers = null; - if (keystore != null) { - keymanagers = createKeyManagers(keystore, keystorePassword); - } - TrustManager[] trustmanagers = null; - if (truststore != null) { - trustmanagers = createTrustManagers(truststore); - } - this.sslcontext = SSLContext.getInstance(algorithm); - this.sslcontext.init(keymanagers, trustmanagers, random); - this.socketfactory = this.sslcontext.getSocketFactory(); - this.nameResolver = nameResolver; - } - - public SSLSocketFactory( - final KeyStore keystore, - final String keystorePassword, - final KeyStore truststore) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException - { - this(TLS, keystore, keystorePassword, truststore, null, null); - } - - public SSLSocketFactory(final KeyStore keystore, final String keystorePassword) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException - { - this(TLS, keystore, keystorePassword, null, null, null); - } - - public SSLSocketFactory(final KeyStore truststore) - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException - { - this(TLS, null, null, truststore, null, null); - } - - /** - * Constructs an HttpClient SSLSocketFactory backed by the given JSSE - * SSLSocketFactory. - * - * @hide - */ - public SSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory) { - super(); - this.sslcontext = null; - this.socketfactory = socketfactory; - this.nameResolver = null; - } - - /** - * Creates the default SSL socket factory. - * This constructor is used exclusively to instantiate the factory for - * {@link #getSocketFactory getSocketFactory}. - */ - private SSLSocketFactory() { - super(); - this.sslcontext = null; - this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory(); - this.nameResolver = null; - } - - private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password) - throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { - if (keystore == null) { - throw new IllegalArgumentException("Keystore may not be null"); - } - KeyManagerFactory kmfactory = KeyManagerFactory.getInstance( - KeyManagerFactory.getDefaultAlgorithm()); - kmfactory.init(keystore, password != null ? password.toCharArray(): null); - return kmfactory.getKeyManagers(); - } - - private static TrustManager[] createTrustManagers(final KeyStore keystore) - throws KeyStoreException, NoSuchAlgorithmException { - if (keystore == null) { - throw new IllegalArgumentException("Keystore may not be null"); - } - TrustManagerFactory tmfactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - tmfactory.init(keystore); - return tmfactory.getTrustManagers(); - } - - - // non-javadoc, see interface org.apache.http.conn.SocketFactory - public Socket createSocket() - throws IOException { - - // the cast makes sure that the factory is working as expected - return (SSLSocket) this.socketfactory.createSocket(); - } - - - // non-javadoc, see interface org.apache.http.conn.SocketFactory - public Socket connectSocket( - final Socket sock, - final String host, - final int port, - final InetAddress localAddress, - int localPort, - final HttpParams params - ) throws IOException { - - if (host == null) { - throw new IllegalArgumentException("Target host may not be null."); - } - if (params == null) { - throw new IllegalArgumentException("Parameters may not be null."); - } - - SSLSocket sslsock = (SSLSocket) - ((sock != null) ? sock : createSocket()); - - if ((localAddress != null) || (localPort > 0)) { - - // we need to bind explicitly - if (localPort < 0) - localPort = 0; // indicates "any" - - InetSocketAddress isa = - new InetSocketAddress(localAddress, localPort); - sslsock.bind(isa); - } - - int connTimeout = HttpConnectionParams.getConnectionTimeout(params); - int soTimeout = HttpConnectionParams.getSoTimeout(params); - - InetSocketAddress remoteAddress; - if (this.nameResolver != null) { - remoteAddress = new InetSocketAddress(this.nameResolver.resolve(host), port); - } else { - remoteAddress = new InetSocketAddress(host, port); - } - - sslsock.connect(remoteAddress, connTimeout); - - sslsock.setSoTimeout(soTimeout); - try { - hostnameVerifier.verify(host, sslsock); - // verifyHostName() didn't blowup - good! - } catch (IOException iox) { - // close the socket before re-throwing the exception - try { sslsock.close(); } catch (Exception x) { /*ignore*/ } - throw iox; - } - - return sslsock; - } - - - /** - * Checks whether a socket connection is secure. - * This factory creates TLS/SSL socket connections - * which, by default, are considered secure. - * <br/> - * Derived classes may override this method to perform - * runtime checks, for example based on the cypher suite. - * - * @param sock the connected socket - * - * @return <code>true</code> - * - * @throws IllegalArgumentException if the argument is invalid - */ - public boolean isSecure(Socket sock) - throws IllegalArgumentException { - - if (sock == null) { - throw new IllegalArgumentException("Socket may not be null."); - } - // This instanceof check is in line with createSocket() above. - if (!(sock instanceof SSLSocket)) { - throw new IllegalArgumentException - ("Socket not created by this factory."); - } - // This check is performed last since it calls the argument object. - if (sock.isClosed()) { - throw new IllegalArgumentException("Socket is closed."); - } - - return true; - - } // isSecure - - - // non-javadoc, see interface LayeredSocketFactory - public Socket createSocket( - final Socket socket, - final String host, - final int port, - final boolean autoClose - ) throws IOException, UnknownHostException { - SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket( - socket, - host, - port, - autoClose - ); - hostnameVerifier.verify(host, sslSocket); - // verifyHostName() didn't blowup - good! - return sslSocket; - } - - public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) { - if ( hostnameVerifier == null ) { - throw new IllegalArgumentException("Hostname verifier may not be null"); - } - this.hostnameVerifier = hostnameVerifier; - } - - public X509HostnameVerifier getHostnameVerifier() { - return hostnameVerifier; - } - -} diff --git a/src/org/apache/http/conn/ssl/StrictHostnameVerifier.java b/src/org/apache/http/conn/ssl/StrictHostnameVerifier.java deleted file mode 100644 index bd9e70d..0000000 --- a/src/org/apache/http/conn/ssl/StrictHostnameVerifier.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/StrictHostnameVerifier.java $ - * $Revision: 617642 $ - * $Date: 2008-02-01 12:54:07 -0800 (Fri, 01 Feb 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.ssl; - -import javax.net.ssl.SSLException; - -/** - * The Strict HostnameVerifier works the same way as Sun Java 1.4, Sun - * Java 5, Sun Java 6-rc. It's also pretty close to IE6. This - * implementation appears to be compliant with RFC 2818 for dealing with - * wildcards. - * <p/> - * The hostname must match either the first CN, or any of the subject-alts. - * A wildcard can occur in the CN, and in any of the subject-alts. The - * one divergence from IE6 is how we only check the first CN. IE6 allows - * a match against any of the CNs present. We decided to follow in - * Sun Java 1.4's footsteps and only check the first CN. (If you need - * to check all the CN's, feel free to write your own implementation!). - * <p/> - * A wildcard such as "*.foo.com" matches only subdomains in the same - * level, for example "a.foo.com". It does not match deeper subdomains - * such as "a.b.foo.com". - * - * @author Julius Davies - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public class StrictHostnameVerifier extends AbstractVerifier { - - public final void verify( - final String host, - final String[] cns, - final String[] subjectAlts) throws SSLException { - verify(host, cns, subjectAlts, true); - } - - @Override - public final String toString() { - return "STRICT"; - } - -} diff --git a/src/org/apache/http/conn/ssl/X509HostnameVerifier.java b/src/org/apache/http/conn/ssl/X509HostnameVerifier.java deleted file mode 100644 index e38db5f..0000000 --- a/src/org/apache/http/conn/ssl/X509HostnameVerifier.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/X509HostnameVerifier.java $ - * $Revision: 618365 $ - * $Date: 2008-02-04 10:20:08 -0800 (Mon, 04 Feb 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.conn.ssl; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; -import java.io.IOException; -import java.security.cert.X509Certificate; - -/** - * Interface for checking if a hostname matches the names stored inside the - * server's X.509 certificate. Implements javax.net.ssl.HostnameVerifier, but - * we don't actually use that interface. Instead we added some methods that - * take String parameters (instead of javax.net.ssl.HostnameVerifier's - * SSLSession). JUnit is a lot easier this way! :-) - * <p/> - * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and - * HostnameVerifier.ALLOW_ALL implementations. But feel free to define - * your own implementation! - * <p/> - * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the - * HttpClient "contrib" repository. - * - * @author Julius Davies - * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a> - * - * @since 4.0 (8-Dec-2006) - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public interface X509HostnameVerifier extends HostnameVerifier { - - boolean verify(String host, SSLSession session); - - void verify(String host, SSLSocket ssl) throws IOException; - - void verify(String host, X509Certificate cert) throws SSLException; - - /** - * Checks to see if the supplied hostname matches any of the supplied CNs - * or "DNS" Subject-Alts. Most implementations only look at the first CN, - * and ignore any additional CNs. Most implementations do look at all of - * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards - * according to RFC 2818. - * - * @param cns CN fields, in order, as extracted from the X.509 - * certificate. - * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted - * from the X.509 certificate. - * @param host The hostname to verify. - * @throws SSLException If verification failed. - */ - void verify(String host, String[] cns, String[] subjectAlts) - throws SSLException; - - -} diff --git a/src/org/apache/http/conn/ssl/package.html b/src/org/apache/http/conn/ssl/package.html deleted file mode 100644 index a5c737f..0000000 --- a/src/org/apache/http/conn/ssl/package.html +++ /dev/null @@ -1,40 +0,0 @@ -<html> -<head> -<!-- -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/package.html $ - * $Revision: 555193 $ - * $Date: 2007-07-11 00:36:47 -0700 (Wed, 11 Jul 2007) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ ---> -</head> -<body> -TLS/SSL specific parts of the <i>HttpConn</i> API. - -</body> -</html> diff --git a/src/org/apache/http/params/CoreConnectionPNames.java b/src/org/apache/http/params/CoreConnectionPNames.java deleted file mode 100644 index 9479db1..0000000 --- a/src/org/apache/http/params/CoreConnectionPNames.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/params/CoreConnectionPNames.java $ - * $Revision: 576077 $ - * $Date: 2007-09-16 04:50:22 -0700 (Sun, 16 Sep 2007) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.params; - - -/** - * Defines parameter names for connections in HttpCore. - * - * @version $Revision: 576077 $ - * - * @since 4.0 - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public interface CoreConnectionPNames { - - /** - * Defines the default socket timeout (<tt>SO_TIMEOUT</tt>) in milliseconds which is the - * timeout for waiting for data. A timeout value of zero is interpreted as an infinite - * timeout. This value is used when no socket timeout is set in the - * method parameters. - * <p> - * This parameter expects a value of type {@link Integer}. - * </p> - * @see java.net.SocketOptions#SO_TIMEOUT - */ - public static final String SO_TIMEOUT = "http.socket.timeout"; - - /** - * Determines whether Nagle's algorithm is to be used. The Nagle's algorithm - * tries to conserve bandwidth by minimizing the number of segments that are - * sent. When applications wish to decrease network latency and increase - * performance, they can disable Nagle's algorithm (that is enable TCP_NODELAY). - * Data will be sent earlier, at the cost of an increase in bandwidth consumption. - * <p> - * This parameter expects a value of type {@link Boolean}. - * </p> - * @see java.net.SocketOptions#TCP_NODELAY - */ - public static final String TCP_NODELAY = "http.tcp.nodelay"; - - /** - * Determines the size of the internal socket buffer used to buffer data - * while receiving / transmitting HTTP messages. - * <p> - * This parameter expects a value of type {@link Integer}. - * </p> - */ - public static final String SOCKET_BUFFER_SIZE = "http.socket.buffer-size"; - - /** - * Sets SO_LINGER with the specified linger time in seconds. The maximum timeout - * value is platform specific. Value <tt>0</tt> implies that the option is disabled. - * Value <tt>-1</tt> implies that the JRE default is used. The setting only affects - * socket close. - * <p> - * This parameter expects a value of type {@link Integer}. - * </p> - * @see java.net.SocketOptions#SO_LINGER - */ - public static final String SO_LINGER = "http.socket.linger"; - - /** - * Determines the timeout until a connection is etablished. A value of zero - * means the timeout is not used. The default value is zero. - * <p> - * This parameter expects a value of type {@link Integer}. - * </p> - */ - public static final String CONNECTION_TIMEOUT = "http.connection.timeout"; - - /** - * Determines whether stale connection check is to be used. Disabling - * stale connection check may result in slight performance improvement - * at the risk of getting an I/O error when executing a request over a - * connection that has been closed at the server side. - * <p> - * This parameter expects a value of type {@link Boolean}. - * </p> - */ - public static final String STALE_CONNECTION_CHECK = "http.connection.stalecheck"; - - /** - * Determines the maximum line length limit. If set to a positive value, any HTTP - * line exceeding this limit will cause an IOException. A negative or zero value - * will effectively disable the check. - * <p> - * This parameter expects a value of type {@link Integer}. - * </p> - */ - public static final String MAX_LINE_LENGTH = "http.connection.max-line-length"; - - /** - * Determines the maximum HTTP header count allowed. If set to a positive value, - * the number of HTTP headers received from the data stream exceeding this limit - * will cause an IOException. A negative or zero value will effectively disable - * the check. - * <p> - * This parameter expects a value of type {@link Integer}. - * </p> - */ - public static final String MAX_HEADER_COUNT = "http.connection.max-header-count"; - -} diff --git a/src/org/apache/http/params/HttpConnectionParams.java b/src/org/apache/http/params/HttpConnectionParams.java deleted file mode 100644 index a7b31fc..0000000 --- a/src/org/apache/http/params/HttpConnectionParams.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/params/HttpConnectionParams.java $ - * $Revision: 576089 $ - * $Date: 2007-09-16 05:39:56 -0700 (Sun, 16 Sep 2007) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.params; - -/** - * An adaptor for accessing connection parameters in {@link HttpParams}. - * <br/> - * Note that the <i>implements</i> relation to {@link CoreConnectionPNames} - * is for compatibility with existing application code only. References to - * the parameter names should use the interface, not this class. - * - * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> - * - * @version $Revision: 576089 $ - * - * @since 4.0 - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public final class HttpConnectionParams implements CoreConnectionPNames { - - /** - */ - private HttpConnectionParams() { - super(); - } - - /** - * Returns the default socket timeout (<tt>SO_TIMEOUT</tt>) in milliseconds which is the - * timeout for waiting for data. A timeout value of zero is interpreted as an infinite - * timeout. This value is used when no socket timeout is set in the - * method parameters. - * - * @return timeout in milliseconds - */ - public static int getSoTimeout(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0); - } - - /** - * Sets the default socket timeout (<tt>SO_TIMEOUT</tt>) in milliseconds which is the - * timeout for waiting for data. A timeout value of zero is interpreted as an infinite - * timeout. This value is used when no socket timeout is set in the - * method parameters. - * - * @param timeout Timeout in milliseconds - */ - public static void setSoTimeout(final HttpParams params, int timeout) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout); - - } - - /** - * Tests if Nagle's algorithm is to be used. - * - * @return <tt>true</tt> if the Nagle's algorithm is to NOT be used - * (that is enable TCP_NODELAY), <tt>false</tt> otherwise. - */ - public static boolean getTcpNoDelay(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getBooleanParameter - (CoreConnectionPNames.TCP_NODELAY, true); - } - - /** - * Determines whether Nagle's algorithm is to be used. The Nagle's algorithm - * tries to conserve bandwidth by minimizing the number of segments that are - * sent. When applications wish to decrease network latency and increase - * performance, they can disable Nagle's algorithm (that is enable TCP_NODELAY). - * Data will be sent earlier, at the cost of an increase in bandwidth consumption. - * - * @param value <tt>true</tt> if the Nagle's algorithm is to NOT be used - * (that is enable TCP_NODELAY), <tt>false</tt> otherwise. - */ - public static void setTcpNoDelay(final HttpParams params, boolean value) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, value); - } - - public static int getSocketBufferSize(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getIntParameter - (CoreConnectionPNames.SOCKET_BUFFER_SIZE, -1); - } - - public static void setSocketBufferSize(final HttpParams params, int size) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, size); - } - - /** - * Returns linger-on-close timeout. Value <tt>0</tt> implies that the option is - * disabled. Value <tt>-1</tt> implies that the JRE default is used. - * - * @return the linger-on-close timeout - */ - public static int getLinger(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getIntParameter(CoreConnectionPNames.SO_LINGER, -1); - } - - /** - * Returns linger-on-close timeout. This option disables/enables immediate return - * from a close() of a TCP Socket. Enabling this option with a non-zero Integer - * timeout means that a close() will block pending the transmission and - * acknowledgement of all data written to the peer, at which point the socket is - * closed gracefully. Value <tt>0</tt> implies that the option is - * disabled. Value <tt>-1</tt> implies that the JRE default is used. - * - * @param value the linger-on-close timeout - */ - public static void setLinger(final HttpParams params, int value) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setIntParameter(CoreConnectionPNames.SO_LINGER, value); - } - - /** - * Returns the timeout until a connection is etablished. A value of zero - * means the timeout is not used. The default value is zero. - * - * @return timeout in milliseconds. - */ - public static int getConnectionTimeout(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getIntParameter - (CoreConnectionPNames.CONNECTION_TIMEOUT, 0); - } - - /** - * Sets the timeout until a connection is etablished. A value of zero - * means the timeout is not used. The default value is zero. - * - * @param timeout Timeout in milliseconds. - */ - public static void setConnectionTimeout(final HttpParams params, int timeout) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setIntParameter - (CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); - } - - /** - * Tests whether stale connection check is to be used. Disabling - * stale connection check may result in slight performance improvement - * at the risk of getting an I/O error when executing a request over a - * connection that has been closed at the server side. - * - * @return <tt>true</tt> if stale connection check is to be used, - * <tt>false</tt> otherwise. - */ - public static boolean isStaleCheckingEnabled(final HttpParams params) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - return params.getBooleanParameter - (CoreConnectionPNames.STALE_CONNECTION_CHECK, true); - } - - /** - * Defines whether stale connection check is to be used. Disabling - * stale connection check may result in slight performance improvement - * at the risk of getting an I/O error when executing a request over a - * connection that has been closed at the server side. - * - * @param value <tt>true</tt> if stale connection check is to be used, - * <tt>false</tt> otherwise. - */ - public static void setStaleCheckingEnabled(final HttpParams params, boolean value) { - if (params == null) { - throw new IllegalArgumentException("HTTP parameters may not be null"); - } - params.setBooleanParameter - (CoreConnectionPNames.STALE_CONNECTION_CHECK, value); - } - -} diff --git a/src/org/apache/http/params/HttpParams.java b/src/org/apache/http/params/HttpParams.java deleted file mode 100644 index 9562e54..0000000 --- a/src/org/apache/http/params/HttpParams.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/params/HttpParams.java $ - * $Revision: 610763 $ - * $Date: 2008-01-10 04:01:13 -0800 (Thu, 10 Jan 2008) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package org.apache.http.params; - -/** - * Represents a collection of HTTP protocol and framework parameters. - * - * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> - * - * @version $Revision: 610763 $ - * - * @since 4.0 - * - * @deprecated Please use {@link java.net.URL#openConnection} instead. - * Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a> - * for further details. - */ -@Deprecated -public interface HttpParams { - - /** - * Obtains the value of the given parameter. - * - * @param name the parent name. - * - * @return an object that represents the value of the parameter, - * <code>null</code> if the parameter is not set or if it - * is explicitly set to <code>null</code> - * - * @see #setParameter(String, Object) - */ - Object getParameter(String name); - - /** - * Assigns the value to the parameter with the given name. - * - * @param name parameter name - * @param value parameter value - */ - HttpParams setParameter(String name, Object value); - - /** - * Creates a copy of these parameters. - * - * @return a new set of parameters holding the same values as this one - */ - HttpParams copy(); - - /** - * Removes the parameter with the specified name. - * - * @param name parameter name - * - * @return true if the parameter existed and has been removed, false else. - */ - boolean removeParameter(String name); - - /** - * Returns a {@link Long} parameter value with the given name. - * If the parameter is not explicitly set, the default value is returned. - * - * @param name the parent name. - * @param defaultValue the default value. - * - * @return a {@link Long} that represents the value of the parameter. - * - * @see #setLongParameter(String, long) - */ - long getLongParameter(String name, long defaultValue); - - /** - * Assigns a {@link Long} to the parameter with the given name - * - * @param name parameter name - * @param value parameter value - */ - HttpParams setLongParameter(String name, long value); - - /** - * Returns an {@link Integer} parameter value with the given name. - * If the parameter is not explicitly set, the default value is returned. - * - * @param name the parent name. - * @param defaultValue the default value. - * - * @return a {@link Integer} that represents the value of the parameter. - * - * @see #setIntParameter(String, int) - */ - int getIntParameter(String name, int defaultValue); - - /** - * Assigns an {@link Integer} to the parameter with the given name - * - * @param name parameter name - * @param value parameter value - */ - HttpParams setIntParameter(String name, int value); - - /** - * Returns a {@link Double} parameter value with the given name. - * If the parameter is not explicitly set, the default value is returned. - * - * @param name the parent name. - * @param defaultValue the default value. - * - * @return a {@link Double} that represents the value of the parameter. - * - * @see #setDoubleParameter(String, double) - */ - double getDoubleParameter(String name, double defaultValue); - - /** - * Assigns a {@link Double} to the parameter with the given name - * - * @param name parameter name - * @param value parameter value - */ - HttpParams setDoubleParameter(String name, double value); - - /** - * Returns a {@link Boolean} parameter value with the given name. - * If the parameter is not explicitly set, the default value is returned. - * - * @param name the parent name. - * @param defaultValue the default value. - * - * @return a {@link Boolean} that represents the value of the parameter. - * - * @see #setBooleanParameter(String, boolean) - */ - boolean getBooleanParameter(String name, boolean defaultValue); - - /** - * Assigns a {@link Boolean} to the parameter with the given name - * - * @param name parameter name - * @param value parameter value - */ - HttpParams setBooleanParameter(String name, boolean value); - - /** - * Checks if a boolean parameter is set to <code>true</code>. - * - * @param name parameter name - * - * @return <tt>true</tt> if the parameter is set to value <tt>true</tt>, - * <tt>false</tt> if it is not set or set to <code>false</code> - */ - boolean isParameterTrue(String name); - - /** - * Checks if a boolean parameter is not set or <code>false</code>. - * - * @param name parameter name - * - * @return <tt>true</tt> if the parameter is either not set or - * set to value <tt>false</tt>, - * <tt>false</tt> if it is set to <code>true</code> - */ - boolean isParameterFalse(String name); - -} |