? diffs-evert.txt Index: java/org/apache/commons/httpclient/Authenticator.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/Authenticator.java,v retrieving revision 1.11 diff -u -r1.11 Authenticator.java --- java/org/apache/commons/httpclient/Authenticator.java 5 Jan 2002 11:15:59 -0000 1.11 +++ java/org/apache/commons/httpclient/Authenticator.java 9 Jul 2002 05:12:50 -0000 @@ -70,28 +70,82 @@ *

Utility methods for HTTP authorization and authentication.

*

* This class provides utility methods for generating - * responses to HTTP authentication challenges. + * responses to HTTP www and proxy authentication challenges. *

* @author Remy Maucherat * @author Rodney Waldhoff * @version $Revision: 1.11 $ $Date: 2002/01/05 11:15:59 $ */ class Authenticator { + /** + * The www authenticate challange header + */ + public static final String WWW_AUTH = "WWW-Authenticate"; + + /** + * The www authenticate response header + */ + public static final String WWW_AUTH_RESP = "Authorization"; + + /** + * The proxy authenticate challange header + */ + public static final String PROXY_AUTH = "Proxy-Authenticate"; + + /** + * The proxy authenticate response header + */ + public static final String PROXY_AUTH_RESP = "Proxy-Authorization"; /** * Add requisite authentication credentials to the given * {@link HttpMethod}, if possible. * + * @see HttpState#setCredentials(String, Credentials) HttpState.setCredentials + * * @param method a {@link HttpMethod} which requires authentication * @param state a {@link HttpState} object providing {@link Credentials} * * @throws HttpException when a parsing or other error occurs * @throws UnsupportedOperationException when the given challenge type is not supported + * @return true if only if a response header was added */ static boolean authenticate(HttpMethod method, HttpState state) throws HttpException { log.debug("Authenticator.authenticate(HttpMethod, HttpState)"); - Header challengeHeader = method.getResponseHeader("WWW-Authenticate"); + + Header challengeHeader = method.getResponseHeader(WWW_AUTH); if(null == challengeHeader) { return false; } + + return authenticate(method, state, challengeHeader, WWW_AUTH_RESP); + } + + /** + * Add requisite proxy authentication credentials to the given + * {@link HttpMethod}, if possible. + * + * @see HttpState#setProxyCredentials(String, Credentials) HttpState.setProxyCredentials + * + * @param method a {@link HttpMethod} which requires authentication + * @param state a {@link HttpState} object providing {@link Credentials} + * + * @throws HttpException when a parsing or other error occurs + * @throws UnsupportedOperationException when the given challenge type is not supported + * @return true if only if a response header was added + */ + static boolean authenticateProxy(HttpMethod method, HttpState state) throws HttpException { + log.debug("Authenticator.authenticateProxy(HttpMethod, HttpState)"); + + Header challengeHeader = method.getResponseHeader(PROXY_AUTH); + if (null == challengeHeader) { return false; } + return authenticate(method, state, challengeHeader, PROXY_AUTH_RESP); + } + + + private static boolean authenticate(HttpMethod method, HttpState state, + Header challengeHeader, + String respHeader) + throws HttpException { + String challenge = challengeHeader.getValue(); if(null == challenge) { return false; } @@ -119,7 +173,7 @@ } String realm = realmstr.substring("realm=\"".length(),realmstr.length()-1); log.debug("Parsed realm \"" + realm + "\" from challenge \"" + challenge + "\"."); - Header header = Authenticator.basic(realm,state); + Header header = Authenticator.basic(realm, state, respHeader); if(null != header) { method.addRequestHeader(header); return true; @@ -137,19 +191,23 @@ * Create a Basic Authorization header for the given * realm and state to the given method. * - * @param method the {@link HttpMethod} to authenticate to * @param realm the basic realm to authenticate to * @param state a {@link HttpState} object providing {@link Credentials} + * @param respHeader the header's name to store the authentication response + * in. PROXY_AUTH_RESP will force the proxy credentials to be used. * - * @return a basic Authorization value + * @return a basic Authorization header * * @throws HttpException when no matching credentials are available */ - static Header basic(String realm, HttpState state) throws HttpException { + static Header basic(String realm, HttpState state, String respHeader) throws HttpException { log.debug("Authenticator.basic(String,HttpState)"); + boolean proxy = PROXY_AUTH_RESP.equals(respHeader); UsernamePasswordCredentials cred = null; try { - cred = (UsernamePasswordCredentials)(state.getCredentials(realm)); + cred = (UsernamePasswordCredentials) ( proxy ? + state.getProxyCredentials(realm) : + state.getCredentials(realm)); } catch(ClassCastException e) { throw new HttpException("UsernamePasswordCredentials required for Basic authentication."); } @@ -158,7 +216,9 @@ log.info("No credentials found for realm \"" + realm + "\", attempting to use default credentials."); } try { - cred = (UsernamePasswordCredentials)(state.getCredentials(null)); + cred = (UsernamePasswordCredentials)( proxy ? + state.getProxyCredentials(null) : + state.getCredentials(null)); } catch(ClassCastException e) { throw new HttpException("UsernamePasswordCredentials required for Basic authentication."); } @@ -166,7 +226,7 @@ if(null == cred) { throw new HttpException("No credentials available for the Basic authentication realm \"" + realm + "\"/"); } else { - return new Header("Authorization",Authenticator.basic(cred)); + return new Header(respHeader, Authenticator.basic(cred)); } } Index: java/org/apache/commons/httpclient/HttpConnection.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v retrieving revision 1.9 diff -u -r1.9 HttpConnection.java --- java/org/apache/commons/httpclient/HttpConnection.java 12 Apr 2002 21:09:20 -0000 1.9 +++ java/org/apache/commons/httpclient/HttpConnection.java 9 Jul 2002 05:12:53 -0000 @@ -66,11 +66,14 @@ import java.net.SocketException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.io.IOException; +import javax.net.SocketFactory; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import org.apache.commons.httpclient.log.*; import java.lang.reflect.Method; +import java.util.HashSet; /** *

@@ -115,7 +118,7 @@ } /** - * Fully-specified constructor. + * Constructor. * @param proxyHost the host I should proxy via * @param proxyPort the port I should proxy via * @param host the host I should connect to. Parameter value must be non-null. @@ -123,6 +126,21 @@ * @param secure when true, connect via HTTPS (SSL) */ public HttpConnection(String proxyHost, int proxyPort, String host, int port, boolean secure) { + this(proxyHost, proxyPort, host, port, secure, null, null); + } + + /** + * Fully-specified constructor. + * @param proxyHost the host I should proxy via + * @param proxyPort the port I should proxy via + * @param host the host I should connect to. Parameter value must be non-null. + * @param port the port I should connect to + * @param secure when true, connect via HTTPS (SSL) + * @param state the HttpSharedState for establishing connections through authenticated proxies. + * @param factory the factory to be used for creating SSL sockets. Or, null for the default. + */ + public HttpConnection(String proxyHost, int proxyPort, String host, int port, boolean secure, + HttpSharedState state, SSLSocketFactory factory) { log.debug("HttpConnection.HttpConnection"); if (host == null) { throw new NullPointerException("host parameter is null"); @@ -132,6 +150,8 @@ _host = host; _port = port; _ssl = secure; + _state = state; + _sslSocketFactory = factory; } // ------------------------------------------ Attribute Setters and Getters @@ -278,12 +298,26 @@ assertNotOpen(); // ??? is this worth doing? try { if (null == _socket) { - String host = (null == _proxyHost) ? _host : _proxyHost; - int port = (null == _proxyHost) ? _port : _proxyPort; if (_ssl) { - _socket = SSLSocketFactory.getDefault().createSocket(host,port); + if (_sslSocketFactory == null) + _sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); + if ((_proxyHost != null) && (_proxyPort > 0)) { + // ssl using proxy - create a tunnellec connection + doTunnelHandshake(false, null); + Socket tunnel = _socket; + _socket = _sslSocketFactory.createSocket(tunnel, _host, _port, true); + } else { + // using with no proxy + _socket = _sslSocketFactory.createSocket(_host, _port); + } } else { + // non ssl connection + String host = (null == _proxyHost) ? _host : _proxyHost; + int port = (null == _proxyHost) ? _port : _proxyPort; _socket = new Socket(host,port); + // Not creating a tunnel socket, but authenticating. + //if ((_proxyHost != null) && (_proxyPort > 0)) + // doTunnelHandshake(false, null); } } _socket.setSoTimeout(_so_timeout); @@ -293,11 +327,115 @@ } catch (IOException e) { // Connection wasn't opened properly // so close everything out + //log.debug("IOException in opening connection: " + e, e); closeSocketAndStreams(); throw e; + } finally { + _attemptedRealms.clear(); } } + private void doTunnelHandshake(boolean authenticate, String realm) throws IOException + { + log.debug("doTunnelHandshake(authenticate='" + authenticate + "', realm='" + realm + "')"); + _attemptedRealms.add(realm); + String authorizationHeader = null; + if (authenticate && _state != null) { + Credentials credentials = _state.getProxyCredentials(realm); + if ((credentials != null) && + (credentials instanceof UsernamePasswordCredentials)) { + try { + authorizationHeader = Authenticator.basic((UsernamePasswordCredentials)credentials); + } catch (HttpException ex) { + log.debug("doTunnelHandshake(): Exception reading credentials: " + ex); + } + } + else if (realm != null) { + // Try it once with the default credentials that are stored + // with a null realm. + doTunnelHandshake(true, null); + return; + } + } + _socket = new Socket(_proxyHost, _proxyPort); + _output = _socket.getOutputStream(); + String msg = "CONNECT " + _host + ":" + _port + " HTTP/1.0\r\n" + + "User-Agent: " + sun.net.www.protocol.http.HttpURLConnection.userAgent + "\r\n" + + //"Host: " + _host + ":" + _port + "\r\n" + + "Host: " + _host + "\r\n" + + "Content-Length: 0\r\n" + + "Proxy-Connection: Keep-Alive\r\n" + + "Pragma: no-cache\r\n"; + if (authorizationHeader != null) + msg += "Proxy-Authorization: " + authorizationHeader + "\r\n"; + msg += "\r\n"; + + wireLog.info(">>\r\n" + msg); + + byte b[]; + try { + b = msg.getBytes("ASCII7"); + } catch (UnsupportedEncodingException ignored) { + b = msg.getBytes(); + } + _output.write(b); + _output.flush(); + + _input = _socket.getInputStream(); + boolean error = false; + + _open = true; + StringBuffer replyBuffer = new StringBuffer(); + String proxyAuthenticate = null; + String firstLine = null; + while (true) { + String line = readLine(); + if (line == null || line.length() < 1) + break; + if (firstLine == null) + firstLine = line; + replyBuffer.append(line + "\r\n"); + int colonPosition = line.indexOf(":"); + if (colonPosition > -1) { + String key = line.substring(0, colonPosition); + String value = line.substring(colonPosition); + if (key.trim().equalsIgnoreCase("Proxy-Authenticate")) + proxyAuthenticate = value; + } + } + String replyStr = replyBuffer.toString(); + wireLog.info("<<\r\n" + replyStr); + _open = false; + + String newRealm = null; + if (proxyAuthenticate != null) { + int start = proxyAuthenticate.indexOf("\"") + 1; + int end = proxyAuthenticate.lastIndexOf("\""); + if (start > -1 && end > -1 && start != end) + newRealm = proxyAuthenticate.substring(start, end); + } + + if (newRealm != null && !_attemptedRealms.contains(newRealm)) { + //closeSocketAndStreams(); + doTunnelHandshake(true, newRealm); + return; + } + + log.debug("doTunnelHandshake(): Reply: " + replyStr); + + /* We asked for HTTP/1.0, so we should get that back */ + if (!replyStr.startsWith("HTTP/1.0 200")) { + msg = "Unable to tunnel through " + + _proxyHost + ":" + _proxyPort + + ". Proxy returns \"" + firstLine + "\""; + log.debug("doTunnelHandshake(): " + msg); + throw new IOException(msg); + } + + /* tunneling Handshake was successful! */ + + } + /** * Return a {@link RequestOutputStream} * suitable for writing (possibly chunked) @@ -347,11 +485,17 @@ } /** - * Write the specified bytes to my output stream. + * Write the specified bytes to my output stream. The first byte written is + * data[off]. len bytes of data will + * be written. + * + * @param data The data that will be written + * @param off Offset + * @param len Length * @throws IllegalStateException if I am not connected * @throws IOException if an I/O problem occurs */ - public void write(byte[] data) throws IOException, IllegalStateException, HttpRecoverableException { + public void write(byte[] data, int off, int len) throws IOException, IllegalStateException, HttpRecoverableException { if(log.isDebugEnabled()){ log.debug("HttpConnection.write(byte[])"); } @@ -360,7 +504,7 @@ wireLog.info(">> \"" + new String(data) + "\""); } try { - _output.write(data); + _output.write(data, off, len); } catch(SocketException e){ if(log.isDebugEnabled()) { log.debug("HttpConnection: Socket exception while writing data",e); @@ -374,6 +518,15 @@ } } + /** + * Write the specified bytes to my output stream. + * @param data The data that will be written + * @throws IllegalStateException if I am not connected + * @throws IOException if an I/O problem occurs + */ + public void write(byte[] data) throws IOException, IllegalStateException, HttpRecoverableException { + write(data, 0, data.length); + } /** * Write the specified bytes, followed by @@ -593,5 +746,10 @@ private static final byte[] CRLF = "\r\n".getBytes(); /** SO_TIMEOUT value */ private int _so_timeout = 0; - + /** The factory to be used for creating SSL sockets. **/ + private SSLSocketFactory _sslSocketFactory = null; + /** The state to be used for establishing connections through authenticated proxies. **/ + private HttpSharedState _state = null; + /** All the realms for which authentication has already been attempted once. **/ + private HashSet _attemptedRealms = new HashSet(); } Index: java/org/apache/commons/httpclient/HttpConnectionManager.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnectionManager.java,v retrieving revision 1.5 diff -u -r1.5 HttpConnectionManager.java --- java/org/apache/commons/httpclient/HttpConnectionManager.java 15 Apr 2002 18:35:29 -0000 1.5 +++ java/org/apache/commons/httpclient/HttpConnectionManager.java 9 Jul 2002 05:12:55 -0000 @@ -67,6 +67,7 @@ import java.util.HashMap; import java.util.List; import java.util.LinkedList; +import javax.net.ssl.SSLSocketFactory; import org.apache.commons.httpclient.log.*; @@ -87,6 +88,8 @@ private int maxConnections = 2; // Per RFC 2616 sec 8.1.4 private String proxyHost = null; private int proxyPort = -1; + private HttpSharedState state = null; + private SSLSocketFactory factory = null; /** * No-args constructor @@ -96,6 +99,15 @@ } /** + * This constructor is necessary for making connections through authenticated proxies. + * @param state for making connections through authenticated proxies, or null. + */ + public HttpConnectionManager(HttpSharedState state) + { + this.state = state; + } + + /** * Set the proxy host to use for all connections. * * @param proxyHost - the proxy host name @@ -157,6 +169,15 @@ } /** + * Allows you to specify a new factory to be used as the default + * for creating SSL sockets. Setting the factory to null will + * reset it to using SSLSocketFactory.getDefault(). + */ + public void setSSLSocketFactory(SSLSocketFactory factory) { + this.factory = factory; + } + + /** * Get an HttpConnection for a given URL. The URL must be fully * specified (i.e. contain a protocol and a host (and optional port number). * If the maximum number of connections for the host has been reached, this @@ -249,7 +270,7 @@ if(log.isDebugEnabled()){ log.debug("HttpConnectionManager.getConnection: creating connection for " + host + ":" + port + " via " + proxyHost + ":" + proxyPort); } - conn = new HttpConnection(proxyHost, proxyPort, host, port, isSecure); + conn = new HttpConnection(proxyHost, proxyPort, host, port, isSecure, state, factory); numConnections = new Integer(numConnections.intValue()+1); mapNumConnections.put(key, numConnections); }else{ Index: java/org/apache/commons/httpclient/HttpMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethod.java,v retrieving revision 1.12 diff -u -r1.12 HttpMethod.java --- java/org/apache/commons/httpclient/HttpMethod.java 22 Feb 2002 19:15:54 -0000 1.12 +++ java/org/apache/commons/httpclient/HttpMethod.java 9 Jul 2002 05:12:56 -0000 @@ -262,6 +262,29 @@ public InputStream getResponseBodyAsStream() throws IOException; /** + * Returns the time in milliseconds when a connection was + * available and open. + * If an error occurred before this event, then it will return -1. + */ + public long getWhenConnectedMillis(); + + /** + * Returns the time in milliseconds when the headers of the request + * were sent, not the body of the request. It might first wait for + * a 100 response before sending the body of the request. + * If an error occurred before this event, then it will return -1. + */ + public long getWhenRequestedMillis(); + + /** + * Returns the time in milliseconds when the first response was received + * from the server, regardless of whether it was a success, failure + * or continue response. + * If an error occurred before this event, then it will return -1. + */ + public long getWhenRespondedMillis(); + + /** * Return true if I have been {@link #execute executed} * but not recycled. */ Index: java/org/apache/commons/httpclient/HttpMethodBase.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v retrieving revision 1.28 diff -u -r1.28 HttpMethodBase.java --- java/org/apache/commons/httpclient/HttpMethodBase.java 16 Apr 2002 14:30:42 -0000 1.28 +++ java/org/apache/commons/httpclient/HttpMethodBase.java 9 Jul 2002 05:13:03 -0000 @@ -267,7 +267,6 @@ * Remove the request header associated with the given name. * Note that header-name matching is case insensitive. * @param headerName the header name - * @return the header */ public void removeRequestHeader(String headerName) { requestHeaders.remove(headerName.toLowerCase()); @@ -405,6 +404,35 @@ } /** + * Returns the time in milliseconds when a connection was + * available and open. + * If an error occurred before this event, then it will return -1. + */ + public long getWhenConnectedMillis() { + return whenConnected; + } + + /** + * Returns the time in milliseconds when the headers of the request + * were sent, not the body of the request. It might first wait for + * a 100 response before sending the body of the request. + * If an error occurred before this event, then it will return -1. + */ + public long getWhenRequestedMillis() { + return whenRequested; + } + + /** + * Returns the time in milliseconds when the first response was received + * from the server, regardless of whether it was a success, failure + * or continue response. + * If an error occurred before this event, then it will return -1. + */ + public long getWhenRespondedMillis() { + return whenResponded; + } + + /** * Return true if I have been {@link #execute executed} * but not recycled. */ @@ -450,6 +478,7 @@ Set visited = new HashSet(); Set realms = new HashSet(); + Set proxyRealms = new HashSet(); int retryCount = 0; for(;;) { visited.add(connection.getHost() + ":" + connection.getPort() + "|" + HttpMethodBase.generateRequestLine(connection, getName(),getPath(),getQueryString(),(http11 ? "HTTP/1.1" : "HTTP/1.0"))); @@ -466,11 +495,20 @@ connection.open(); } + if (whenConnected == -1) + whenConnected = System.currentTimeMillis(); + writeRequest(state,connection); used = true; + if (whenRequested == -1) + whenRequested = System.currentTimeMillis(); + // need to close output?, but when? readResponse(state,connection); + + if (whenResponded == -1) + whenResponded = System.currentTimeMillis(); }catch(HttpRecoverableException e){ if(retryCount >= maxRetries){ throw new HttpException(e.toString()); @@ -505,22 +543,45 @@ } } - if (HttpStatus.SC_UNAUTHORIZED == statusCode) { - Header wwwauth = getResponseHeader("WWW-Authenticate"); + if ((HttpStatus.SC_UNAUTHORIZED == statusCode) + || (HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED == statusCode)) { + + Header wwwauth = null; + Set realmsUsed = null; + switch (statusCode) { + case HttpStatus.SC_UNAUTHORIZED: + wwwauth = getResponseHeader(Authenticator.WWW_AUTH); + realmsUsed = realms; + break; + + case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: + wwwauth = getResponseHeader(Authenticator.PROXY_AUTH); + realmsUsed = proxyRealms; + break; + } if (null != wwwauth) { String pathAndCreds = getPath() + ":" + wwwauth.getValue(); - if (realms.contains(pathAndCreds)) { + if (realmsUsed.contains(pathAndCreds)) { if (log.isInfoEnabled()) { log.info("Already tried to authenticate to \"" + wwwauth.getValue() + "\" but still receiving " + HttpStatus.SC_UNAUTHORIZED + "."); } break; } else { - realms.add(pathAndCreds); + realmsUsed.add(pathAndCreds); } boolean authenticated = false; try { - authenticated = Authenticator.authenticate(this,state); + switch (statusCode) { + case HttpStatus.SC_UNAUTHORIZED: + authenticated = Authenticator.authenticate(this,state); + break; + + case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: + authenticated = Authenticator.authenticateProxy(this,state); + break; + } + } catch(HttpException e) { // ignored } @@ -539,119 +600,11 @@ continue; } } - } else if (HttpStatus.SC_MOVED_TEMPORARILY == statusCode || - HttpStatus.SC_MOVED_PERMANENTLY == statusCode || - HttpStatus.SC_TEMPORARY_REDIRECT == statusCode) { - if (getFollowRedirects()) { - // - // Note that we cannot current support - // redirects that change the HttpConnection - // parameters (host, port, protocol) - // because we don't yet have a good way to - // get the new connection. - // - // For the time being, we just return - // the 302 response, and allow the user - // agent to resubmit if desired. - // - Header location = getResponseHeader("location"); - if (location != null) { - URL url = null; - try { - if (location.getValue().startsWith("/")) { - if (log.isDebugEnabled()) { - log.debug("Following relative Location header \"" + location + "\"."); - } - String protocol = connection.isSecure() ? "https" : "http"; - int port = connection.getPort(); - if (-1 == port) { - port = connection.isSecure() ? 443 : 80; - } - url = new URL(protocol,connection.getHost(),port,location.getValue()); - } else if(!isStrictMode() && location.getValue().indexOf("://") < 0) { - /* - * Location doesn't start with / but it doesn't contain a protocol. - * Per RFC 2616, that's an error. In non-strict mode we'll try - * to build a URL relative to the current path. - */ - String protocol = connection.isSecure() ? "https" : "http"; - int port = connection.getPort(); - if(-1 == port) { - port = connection.isSecure() ? 443 : 80; - } - URL currentUrl = new URL(protocol,connection.getHost(),port,getPath()); - url = new URL(currentUrl, location.getValue()); - } else { - url = new URL(location.getValue()); - } - } catch(MalformedURLException e) { - log.error("Exception while parsing location header \"" + location + "\"",e); - throw new HttpException(e.toString()); - } - if ("http".equalsIgnoreCase(url.getProtocol())) { - if (connection.isSecure()) { - log.info("Server is attempting to redirect an HTTPS request to an HTTP one."); - throw new HttpException("Server is attempting to redirect an HTTPS request to an HTTP one."); - } - } else if ("https".equalsIgnoreCase(url.getProtocol())) { - if (!connection.isSecure()) { - log.info("Server is attempting to convert an HTTP request to an HTTP one, which is currently not supported. Returning " + statusCode + "."); - break; - } - } - if (!connection.getHost().equalsIgnoreCase(url.getHost())) { - log.info("Server is attempting to redirect a different host, which is currently not supported. Returning " + statusCode + "."); - break; - } - if (url.getPort() == -1) { - if (connection.isSecure()) { - if (connection.getPort() != 443) { - log.info("Server is attempting to redirect a different port, which is currently not supported. Returning " + statusCode + "."); - break; - } - } else { - if (connection.getPort() != 80) { - log.info("Server is attempting to redirect a different port, which is currently not supported. Returning " + statusCode + "."); - break; - } - } - } else if (connection.getPort() != url.getPort()) { - log.info("Server is attempting to redirect a different port, which is currently not supported. Returning " + statusCode + "."); - break; - } - String absolutePath = URIUtil.getPath(url.toString()); - String qs = URIUtil.getQueryString(url.toString()); - - // if we haven't already, let's try it again with the new path - if (visited.contains(connection.getHost() + ":" + connection.getPort() + "|" + HttpMethodBase.generateRequestLine(connection, getName(),absolutePath,qs,(http11 ? "HTTP/1.1" : "HTTP/1.0")))) { - throw new HttpException("Redirect going into a loop, visited \"" + absolutePath + "\" already."); - } else { - if (log.isDebugEnabled()) { - log.debug("Changing path from \"" + getPath() + "\" to \"" + absolutePath + "\" in response to " + statusCode + " response."); - log.debug("Changing query string from \"" + getQueryString() + "\" to \"" + qs + "\" in response to " + statusCode + " response."); - } - setPath(URIUtil.decode(absolutePath)); - setQueryString(qs); - continue; - } - } else { - // got a redirect response, but no location header - if (log.isInfoEnabled()) { - log.info("HttpMethodBase.execute(): Received " + statusCode + " response, but no \"Location\" header. Returning " + statusCode + "."); - } - break; - } - } else { - // got a redirect response, - // but followRedirects is false - log.info("HttpMethodBase.execute(): Received " + statusCode + " response, but followRedirects is false. Returning " + statusCode + "."); - break; - } - } else { - // neither an UNAUTHORIZED nor a redirect response - // so exit - break; } + // Moved the handling of redirects to HttpMultiClient. Replaced all the + // code to build a URL relative to the original URL with: + // URL newUrl = new URL( URL context, String newUrlStr ) --Evert + break; } return statusCode; @@ -767,6 +720,7 @@ addHostRequestHeader(state,conn); addCookieRequestHeader(state,conn); addAuthorizationRequestHeader(state,conn); + addProxyAuthorizationRequestHeader(state, conn); addContentLengthRequestHeader(state,conn); } @@ -810,8 +764,8 @@ */ protected void addAuthorizationRequestHeader(HttpState state, HttpConnection conn) throws IOException, HttpException { // add authorization header, if needed - if (!requestHeaders.containsKey("authorization")) { - Header wwwAuthenticateHeader = (Header)(responseHeaders.get("www-authenticate")); + if (!requestHeaders.containsKey(Authenticator.WWW_AUTH_RESP.toLowerCase())) { + Header wwwAuthenticateHeader = (Header)(responseHeaders.get(Authenticator.WWW_AUTH.toLowerCase())); if (null != wwwAuthenticateHeader) { try { Authenticator.authenticate(this,state); @@ -823,6 +777,25 @@ } /** + * Adds a Proxy-Authorization request if needed, + * as long as no Proxy-Authorization request header + * already exists. + */ + protected void addProxyAuthorizationRequestHeader(HttpState state, HttpConnection conn) throws IOException, HttpException { + // add authorization header, if needed + if (!requestHeaders.containsKey(Authenticator.PROXY_AUTH_RESP.toLowerCase())) { + Header wwwAuthenticateHeader = (Header)(responseHeaders.get(Authenticator.PROXY_AUTH.toLowerCase())); + if (null != wwwAuthenticateHeader) { + try { + Authenticator.authenticateProxy(this,state); + } catch(HttpException e) { + // ignored + } + } + } + } + + /** * Adds a Content-Length or * Transer-Encoding: Chunked request header, * as long as no Content-Length request header @@ -1239,6 +1212,9 @@ http11 = true; bodySent = false; responseBody = null; + whenConnected = -1; + whenRequested = -1; + whenResponded = -1; } // ---------------------------------------------- Protected Utility Methods @@ -1299,14 +1275,15 @@ return (name + " " + buf.toString() + " " + protocol + "\r\n"); } else { if (connection.isSecure()) { - return (name + - " https://" + - connection.getHost() + - ((443 == connection.getPort() || -1 == connection.getPort()) ? "" : (":" + connection.getPort()) ) + - buf.toString() + - " " + - protocol + - "\r\n"); +// return (name + +// " https://" + +// connection.getHost() + +// ((443 == connection.getPort() || -1 == connection.getPort()) ? "" : (":" + connection.getPort()) ) + +// buf.toString() + +// " " + +// protocol + +// "\r\n"); + return (name + " " + buf.toString() + " " + protocol + "\r\n"); } else { return (name + " http://" + @@ -1367,6 +1344,12 @@ private int maxRetries = 3; /** True if we're in strict mode. */ private boolean strictMode = true; + /** The moment when an open connection is available. **/ + private long whenConnected = -1; + /** The moment when the request headers have been sent, not the request body. **/ + private long whenRequested = -1; + /** The moment of the first response received back from the server. **/ + private long whenResponded = -1; // -------------------------------------------------------------- Constants Index: java/org/apache/commons/httpclient/HttpMultiClient.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMultiClient.java,v retrieving revision 1.5 diff -u -r1.5 HttpMultiClient.java --- java/org/apache/commons/httpclient/HttpMultiClient.java 5 Jul 2002 07:13:46 -0000 1.5 +++ java/org/apache/commons/httpclient/HttpMultiClient.java 9 Jul 2002 05:13:05 -0000 @@ -63,8 +63,13 @@ package org.apache.commons.httpclient; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import javax.net.ssl.SSLSocketFactory; import org.apache.commons.httpclient.log.*; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; /** * @@ -85,7 +90,7 @@ // ----------------------------------------------------- Instance Variables private HttpSharedState state = null; - private HttpConnectionManager mgr = new HttpConnectionManager(); + private HttpConnectionManager mgr = null; private boolean strictMode = true; private int timeoutConnection = 0; private int timeoutRequest = 0; @@ -100,6 +105,7 @@ */ public HttpMultiClient() { + mgr = new HttpConnectionManager(getState()); } /** @@ -266,6 +272,26 @@ } /** + * Set the maximum number of connections allowed for a given host:port. + * Per RFC 2616 section 8.1.4, this value defaults to 2. + * + * @param maxConnections - number of connections allowed for each host:port + */ + public void setMaxConnectionsPerHost(int maxConnections) + { + mgr.setMaxConnectionsPerHost(maxConnections); + } + + /** + * Allows you to specify a new factory to be used as the default + * for creating SSL sockets. Setting the factory to null will + * reset it to using SSLSocketFactory.getDefault(). + */ + public void setSSLSocketFactory(SSLSocketFactory factory) { + mgr.setSSLSocketFactory(factory); + } + + /** * * Execute the given {@link HttpUrlMethod} using my current * {@link HttpConnection connection} and {@link HttpState}. @@ -279,12 +305,21 @@ */ public int executeMethod(HttpUrlMethod method) throws IOException, HttpException { + return executeMethod(method, 0); + } + protected int executeMethod(HttpUrlMethod method, int numberOfRedirects) + throws IOException, HttpException + { if (null == method) { throw new NullPointerException("method parameter"); } + if(numberOfRedirects > 10) + { + throw new HttpException("Redirected more than 10 times."); + } HttpConnection connection = mgr.getConnection(method.getUrl(), timeoutConnection); connection.setSoTimeout(timeoutRequest); @@ -309,8 +344,13 @@ mgr.releaseConnection(connection); } - if (status == 301 || status == 302 || - status == 303 || status == 307) + // Excluding post methods from redirects. Paragraph 10.3 + // of HTTP/1.1 RFC2616 says that any method other than GET or HEAD + // requires user intervention before redirecting. --Evert + if (method.getFollowRedirects() && + (method instanceof GetMethod || method instanceof PostMethod) && + (status == 301 || status == 302 || + status == 303 || status == 307)) { Header header = method.getResponseHeader("Location"); String url = header.getValue(); @@ -319,13 +359,34 @@ log.error("HttpMultiClient.executeMethod: Received redirect without Location header."); throw new HttpException("Received redirect without Location header."); } - + + log.debug("HttpMultiClient.executeMethod: Following redirect to: " + url); + + String oldUrlStr = method.getUrl(); + //String oldRequestBody = null; + //if (method instanceof PostMethod) + //{ + // oldRequestBody = ((PostMethod)method).getRequestBody(); + //} + URL oldUrl = null; + try + { + oldUrl = new URL(oldUrlStr); + } catch (MalformedURLException e) + { + // This means the original url was also malformed. But, if that + // was the case we should never have gotten this far. + log.debug("HttpMultiClient.executemethod: The original url was malformed: " + e); + throw e; + } + URL newUrl = new java.net.URL(oldUrl, url); method.recycle(); - method.setUrl(url); - return executeMethod(method); + method.setUrl(newUrl.toString()); + //if (method instanceof PostMethod) + // ((PostMethod)method).setRequestBody(oldRequestBody); + return executeMethod(method, numberOfRedirects++); } return status; } - } Index: java/org/apache/commons/httpclient/HttpState.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpState.java,v retrieving revision 1.5 diff -u -r1.5 HttpState.java --- java/org/apache/commons/httpclient/HttpState.java 5 Jan 2002 11:16:00 -0000 1.5 +++ java/org/apache/commons/httpclient/HttpState.java 9 Jul 2002 05:13:06 -0000 @@ -92,6 +92,11 @@ private HashMap credMap = new HashMap(); /** + * My proxy {@link Credentials Credentials}, by realm. + */ + private HashMap proxyCred = new HashMap(); + + /** * My {@link Cookie Cookie}s. */ private ArrayList cookies = new ArrayList(); @@ -226,4 +231,40 @@ return (Credentials)(credMap.get(realm)); } + /** + *

+ * Set the {@link Credentials} for the proxy with the given + * authentication realm. + *

+ *

+ * When realm is null, I'll use the given + * credentials when no other {@link Credentials} have + * been supplied for the given challenging realm. + * (I.e., use a null realm to set the "default" + * credentials.) Realms rarely make much sense with proxies, so + * null is normally a good choice here. + *

+ * @param realm the authentication realm + * @param credentials the authentication credentials for the given realm + */ + public void setProxyCredentials(String realm, Credentials credentials) { + proxyCred.put(realm, credentials); + } + + /** + *

+ * Get the {@link Credentials} for the proxy with the given authentication + * realm. + *

+ *

+ * When realm is null, I'll return the + * "default" credentials. + * (See {@link #setCredentials setCredentials}.) + *

+ * @param realm the authentication realm + * @return + */ + public Credentials getProxyCredentials(String realm) { + return (Credentials) (proxyCred.get(realm)); + } } Index: java/org/apache/commons/httpclient/methods/GetMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/GetMethod.java,v retrieving revision 1.10 diff -u -r1.10 GetMethod.java --- java/org/apache/commons/httpclient/methods/GetMethod.java 29 Apr 2002 16:04:05 -0000 1.10 +++ java/org/apache/commons/httpclient/methods/GetMethod.java 9 Jul 2002 05:13:07 -0000 @@ -190,9 +190,10 @@ /** - * Use disk setter. + * Buffer the response in a file or not. The default is false. * - * @param useDisk New value of useDisk + * @param useDisk If true the entire response will be buffered in a + * temporary file. */ public void setUseDisk(boolean useDisk) { checkNotUsed(); @@ -201,9 +202,10 @@ /** - * Use disk getter. + * Tells if the response will be buffered in a file. * - * @param boolean useDisk value + * @param boolean If true the entire response will be buffered in a + * temporary file. */ public boolean getUseDisk() { return useDisk; Index: java/org/apache/commons/httpclient/methods/PostMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/PostMethod.java,v retrieving revision 1.9 diff -u -r1.9 PostMethod.java --- java/org/apache/commons/httpclient/methods/PostMethod.java 24 Apr 2002 14:44:50 -0000 1.9 +++ java/org/apache/commons/httpclient/methods/PostMethod.java 9 Jul 2002 05:13:10 -0000 @@ -82,7 +82,18 @@ * @author Doug Sale */ public class PostMethod extends GetMethod { + /** + * The content length will be calculated automatically. This implies buffering + * of the content. + */ + public static final int CONTENT_LENGTH_AUTO = -2; + /** + * The request will use chunked transfer encoding. Content length is not + * calculated and the content is not buffered.
+ * Note: Chunked requests are not supported at the moment. + */ + public static final int CONTENT_LENGTH_CHUNKED = -1; // ----------------------------------------------------------- Constructors @@ -137,6 +148,8 @@ public void recycle() { super.recycle(); requestBody = null; + requestContentLength = CONTENT_LENGTH_AUTO; + buffer = null; parameters.clear(); } @@ -157,6 +170,22 @@ } /** + * Sets length information about the request body. + * + * @param length size in bytes or any of CONTENT_LENGTH_AUTO, + * CONTENT_LENGTH_CHUNKED. If number of bytes is specified the content will + * not be buffered internally and the Content-Length header of the request + * will be used. In this case the user is responsible to supply the correct + * content length. + */ + public void setRequestContentLength(int length) { + if ((length == CONTENT_LENGTH_CHUNKED) && !isHttp11()) { + throw new RuntimeException("Chunked transfer encoding not allowed for HTTP/1.0"); + } + requestContentLength = length; + } + + /** * Overrides method of {@link HttpMethodBase} * to throw {@link IllegalStateException} if * my request body has already been @@ -238,11 +267,20 @@ /** * @throws IllegalStateException if request params have been added + * @deprecated This method converts characters to bytes in a platform dependent + * encoding. Use setRequestBody(InputStream) instead. */ public void setRequestBody(String body) { if(!parameters.isEmpty()) { throw new IllegalStateException("Request parameters have already been added."); } + requestBody = new ByteArrayInputStream(body.getBytes()); + } + + public void setRequestBody(InputStream body) { + if(!parameters.isEmpty()) { + throw new IllegalStateException("Request parameters have already been added."); + } requestBody = body; } @@ -259,16 +297,37 @@ } /** - * Override method of {@link HttpMethodBase} - * to write request parameters as the - * request body. + * Override method of {@link HttpMethodBase} to write request parameters as + * the request body. + * The input stream will be truncated after the specified content length. + * @throws IOException if the stream ends before the specified content length. */ protected boolean writeRequestBody(HttpState state, HttpConnection conn) throws IOException, HttpException { log.debug("PostMethod.writeRequestBody(HttpState,HttpConnection)"); if(null == requestBody) { requestBody = generateRequestBody(parameters); } - conn.print(requestBody); + + byte[] data = new byte[10000]; + int l = requestBody.read(data); + int total = 0; + while (l > 0) { + if ((requestContentLength > 0) && (total + l > requestContentLength)) { + l = requestContentLength - total; + conn.write(data, 0, l); + break; + } + conn.write(data, 0, l); + total += l; + l = requestBody.read(data); + } + if ((requestContentLength > 0) && (total < requestContentLength)) { + throw new IOException("unexpected end of input stream"); + } + if (buffer != null) { + //restore buffered content for repeated requests + requestBody = new ByteArrayInputStream(buffer.toByteArray()); + } return true; } @@ -283,11 +342,44 @@ protected int getRequestContentLength() { if(null == requestBody) { requestBody = generateRequestBody(parameters); + bufferContent(); + } + + if (requestContentLength != CONTENT_LENGTH_AUTO) { + return requestContentLength; + } + + bufferContent(); + + return requestContentLength; + } + + /** + * Buffers the request body and calculates the content length. + * If the method was called earlier it returns immediately. + */ + private void bufferContent() { + if (buffer != null) return; + try { + buffer = new ByteArrayOutputStream(); + byte[] data = new byte[10000]; + int l = requestBody.read(data); + int total = 0; + while (l > 0) { + buffer.write(data, 0, l); + total += l; + l = requestBody.read(data); + } + requestBody = new ByteArrayInputStream(buffer.toByteArray()); + requestContentLength = total; + } catch(IOException e) { + requestBody = null; + requestContentLength = 0; } - return requestBody.getBytes().length; } - protected String generateRequestBody(Map params) { + //shouldn't this be private ? + protected InputStream generateRequestBody(Map params) { if (!params.isEmpty()) { StringBuffer sb = new StringBuffer(); Iterator it = parameters.keySet().iterator(); @@ -315,14 +407,16 @@ } } } - return sb.toString(); + return new ByteArrayInputStream(sb.toString().getBytes()); } else { - return ""; + return new ByteArrayInputStream(new byte[0]); } } - private String requestBody = null; + private InputStream requestBody = null; private HashMap parameters = new HashMap(); + private int requestContentLength = CONTENT_LENGTH_AUTO; + private ByteArrayOutputStream buffer = null; // -------------------------------------------------------------- Constants Index: test/org/apache/commons/httpclient/TestWebappMethods.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappMethods.java,v retrieving revision 1.4 diff -u -r1.4 TestWebappMethods.java --- test/org/apache/commons/httpclient/TestWebappMethods.java 24 Apr 2002 14:44:50 -0000 1.4 +++ test/org/apache/commons/httpclient/TestWebappMethods.java 9 Jul 2002 05:13:12 -0000 @@ -62,6 +62,8 @@ package org.apache.commons.httpclient; +import java.io.ByteArrayInputStream; + import junit.framework.*; import org.apache.commons.httpclient.methods.*; @@ -322,6 +324,65 @@ assertTrue(method.getResponseBodyAsString().indexOf("quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times.") >= 0); assertEquals(200,method.getStatusCode()); } + + + public void testPostBodyCustomLength() throws Exception { + HttpClient client = new HttpClient(); + client.startSession(host, port); + PostMethod method = new PostMethod("/" + context + "/body"); + method.setUseDisk(false); + String body = "quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times."; + method.setRequestBody(new ByteArrayInputStream(body.getBytes())); + method.setRequestContentLength(body.length()); + try { + client.executeMethod(method); + } catch (Throwable t) { + t.printStackTrace(); + fail("Unable to execute method : " + t.toString()); + } + assertTrue(method.getResponseBodyAsString().indexOf("quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times.") >= 0); + assertEquals(200,method.getStatusCode()); + } + + + public void testPostBodyAutoLength() throws Exception { + HttpClient client = new HttpClient(); + client.startSession(host, port); + PostMethod method = new PostMethod("/" + context + "/body"); + method.setUseDisk(false); + String body = "quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times."; + method.setRequestBody(new ByteArrayInputStream(body.getBytes())); + method.setRequestContentLength(PostMethod.CONTENT_LENGTH_AUTO); + try { + client.executeMethod(method); + } catch (Throwable t) { + t.printStackTrace(); + fail("Unable to execute method : " + t.toString()); + } + assertTrue(method.getResponseBodyAsString().indexOf("quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times.") >= 0); + assertEquals(200,method.getStatusCode()); + } + + /* + //enable this when chunked requests are properly implemented + public void testPostBodyChunked() throws Exception { + HttpClient client = new HttpClient(); + client.startSession(host, port); + PostMethod method = new PostMethod("/" + context + "/body"); + method.setUseDisk(false); + String body = "quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times."; + method.setRequestBody(new ByteArrayInputStream(body.getBytes())); + method.setRequestContentLength(PostMethod.CONTENT_LENGTH_CHUNKED); + try { + client.executeMethod(method); + } catch (Throwable t) { + t.printStackTrace(); + fail("Unable to execute method : " + t.toString()); + } + assertTrue(method.getResponseBodyAsString().indexOf("quote=It+was+the+best+of+times%2C+it+was+the+worst+of+times.") >= 0); + assertEquals(200,method.getStatusCode()); + } + */ public void testPutBody() throws Exception { HttpClient client = new HttpClient(); Index: test/org/apache/commons/httpclient/TestWebappRedirect.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappRedirect.java,v retrieving revision 1.4 diff -u -r1.4 TestWebappRedirect.java --- test/org/apache/commons/httpclient/TestWebappRedirect.java 4 Feb 2002 15:26:43 -0000 1.4 +++ test/org/apache/commons/httpclient/TestWebappRedirect.java 9 Jul 2002 05:13:13 -0000 @@ -206,11 +206,7 @@ t.printStackTrace(); fail("Unable to execute method : " + t.toString()); } - assertEquals(200,method.getStatusCode()); - assertTrue(method.getResponseBodyAsString().indexOf("Param Servlet: POST") >= 0); - assertTrue(method.getResponseBodyAsString().indexOf("

QueryString=\"foo=bar&bar=foo\"

") >= 0); - assertTrue(method.getResponseBodyAsString().indexOf("name=\"para\";value=\"meter\"") >= 0); - assertTrue(method.getResponseBodyAsString().indexOf("name=\"param\";value=\"eter\"") >= 0); + assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,method.getStatusCode()); } public void testPutRedirect() throws Exception { @@ -226,8 +222,7 @@ t.printStackTrace(); fail("Unable to execute method : " + t.toString()); } - assertTrue(method.getResponseBodyAsString(),method.getResponseBodyAsString().indexOf("This is data to be sent in the body of an HTTP PUT.") >= 0); - assertEquals(200,method.getStatusCode()); + assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,method.getStatusCode()); } }