? org/apache/commons/httpclient/ConnectMethod.java Index: 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.17 diff -u -w -r1.17 Authenticator.java Index: org/apache/commons/httpclient/HttpClient.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpClient.java,v retrieving revision 1.48 diff -u -w -r1.48 HttpClient.java --- org/apache/commons/httpclient/HttpClient.java 14 Jul 2002 04:40:00 -0000 1.48 +++ org/apache/commons/httpclient/HttpClient.java 17 Jul 2002 15:09:13 -0000 @@ -64,9 +64,8 @@ import java.io.IOException; import java.net.URL; - -import org.apache.commons.httpclient.log.Log; -import org.apache.commons.httpclient.log.LogSource; +import javax.net.ssl.SSLSocketFactory; +import org.apache.commons.httpclient.log.*; /** *

@@ -80,6 +79,7 @@ * @author dIon Gillard * @version $Revision: 1.48 $ $Date: 2002/07/14 04:40:00 $ */ + public class HttpClient { @@ -108,6 +108,10 @@ */ private HttpState state; + private SSLSocketFactory sslSocketFactory = null; + + private int timeout = 0; + // ------------------------------------------------------------- Properties /** @@ -133,6 +137,23 @@ this.state = state; } + /** + * Specifies an alternative factory for SSL sockets. + * @see HttpConnection#setSSLSocketFactory(SSLSocketFactory) HttpConnection.setSSLSocketFactory + * @param sslSocketFactory a living instance of the alternative SSLSocketFactory + */ + public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + /** + * Sets the SO_TIMEOUT which is the timeout for waiting for data. + * @param timeout Timeout in milliseconds + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + // --------------------------------------------------------- Public Methods /** @@ -311,7 +332,12 @@ } if (!connection.isOpen()) { + connection.setSSLSocketFactory(sslSocketFactory); + connection.setSoTimeout(timeout); connection.open(); + if (connection.isProxied() && connection.isSecure()) { + method = new ConnectMethod(method); + } } return method.execute(getState(),connection); } Index: 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.10 diff -u -w -r1.10 HttpConnection.java --- org/apache/commons/httpclient/HttpConnection.java 14 Jul 2002 05:16:06 -0000 1.10 +++ org/apache/commons/httpclient/HttpConnection.java 17 Jul 2002 15:09:14 -0000 @@ -69,9 +69,10 @@ import java.net.Socket; import java.net.SocketException; import javax.net.ssl.SSLSocketFactory; +import javax.net.SocketFactory; -import org.apache.commons.httpclient.log.Log; -import org.apache.commons.httpclient.log.LogSource; +import org.apache.commons.httpclient.log.*; +import java.lang.reflect.Method; /** @@ -144,6 +145,18 @@ // ------------------------------------------ Attribute Setters and Getters /** + * Specifies an alternative factory for SSL sockets. If factory + * is null the default implementation is used. + * + * @param factory An instance of a SSLSocketFactory or null. + * @throws IllegalStateException If called after the connection was opened + */ + public void setSSLSocketFactory(SSLSocketFactory factory) { + assertNotOpen(); + sslSocketFactory = factory; + } + + /** * Return my host. * * @return my host. @@ -283,7 +296,7 @@ */ public void setSoTimeout(int timeout) throws SocketException, IllegalStateException { - log.debug("HttpConnection.setSoTimeout()"); + log.debug("HttpConnection.setSoTimeout("+ timeout +")"); _so_timeout = timeout; if(_socket != null){ _socket.setSoTimeout(timeout); @@ -303,10 +316,15 @@ if (null == _socket) { String host = (null == _proxyHost) ? _host : _proxyHost; int port = (null == _proxyHost) ? _port : _proxyPort; - if (_ssl) { - _socket = SSLSocketFactory.getDefault().createSocket(host,port); + if (isSecure() && !isProxied()) { + if (sslSocketFactory == null) { + sslSocketFactory = SSLSocketFactory.getDefault(); + } + _socket = sslSocketFactory.createSocket(host,port); + _usingSecureSocket = true; } else { _socket = new Socket(host,port); + _usingSecureSocket = false; } } _socket.setSoTimeout(_so_timeout); @@ -322,6 +340,42 @@ } /** + * Calling this method indicates that the proxy has successfully created + * the tunnel to the host. The socket will be switched to the secure socket. + * Subsequent communication is done via the secure socket. The method can only + * be called once on a proxied secure connection. + * + * @throws IllegalStateException if connection is not secure and proxied or + * if the socket is already secure. + * @throws IOException if an error occured creating the secure socket + */ + public void tunnelCreated() throws IllegalStateException, IOException { + if (!isSecure() || !isProxied()) throw new IllegalStateException("Connection must be secure and proxied to use this feature"); + if (_usingSecureSocket) throw new IllegalStateException("Already using a secure socket"); + + if (sslSocketFactory == null) { + sslSocketFactory = SSLSocketFactory.getDefault(); + } + _socket = ((SSLSocketFactory) sslSocketFactory).createSocket(_socket, _host, _port, true); + _input = _socket.getInputStream(); + _output = _socket.getOutputStream(); + _usingSecureSocket = true; + _tunnelEstablished = true; + log.debug("Secure tunnel created"); + } + + + /** + * Indicates if the connection is completely transparent from end to end. + * + * @return true if conncetion is not proxied or tunneled through a transparent + * proxy; false otherwise. + */ + public boolean isTransparent() { + return !isProxied() || _tunnelEstablished; + } + + /** * Return a {@link RequestOutputStream} suitable for writing (possibly * chunked) bytes to my {@link OutputStream}. * @@ -404,6 +458,19 @@ } } + /** + * Write the specified bytes to my output stream. The general contract for + * write(b, off, len) is that some of the bytes in the array b are written + * to the output stream in order; element b[off] is the first byte written + * and b[off+len-1] is the last byte written by this operation. + * + * @param data array containing the data to be written. + * @param off the start offset in the data. + * @param len the number of bytes to write. + */ + public void write(byte[] data, int off, int len) throws IOException { + _output.write(data, off, len); + } /** * Write the specified bytes, followed by "\r\n".getBytes() to my @@ -596,6 +663,8 @@ } _socket = null; _open = false; + _tunnelEstablished = false; + _usingSecureSocket = false; } /** @@ -648,5 +717,10 @@ private static final byte[] CRLF = "\r\n".getBytes(); /** SO_TIMEOUT value */ private int _so_timeout = 0; - + /** An alternative factory for SSL sockets to use */ + private SocketFactory sslSocketFactory = null; + /** Whether or not the _socket is a secure one. Note the difference to _ssl */ + private boolean _usingSecureSocket = false; + /** Whether I am tunneling a proxy or not */ + private boolean _tunnelEstablished = false; } Index: 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 -w -r1.12 HttpMethod.java Index: 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.31 diff -u -w -r1.31 HttpMethodBase.java --- org/apache/commons/httpclient/HttpMethodBase.java 13 Jul 2002 09:06:29 -0000 1.31 +++ org/apache/commons/httpclient/HttpMethodBase.java 17 Jul 2002 15:09:16 -0000 @@ -269,7 +269,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()); @@ -452,6 +451,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"))); @@ -493,10 +493,14 @@ } if (!http11) { + if (getName().equals(ConnectMethod.NAME) && (statusCode == HttpStatus.SC_OK)) { + log.debug("HttpMethodBase.execute(): leaving connection open for tunneling"); + } else { if (log.isDebugEnabled()) { log.debug("HttpMethodBase.execute(): closing connection since we're using HTTP/1.0"); } connection.close(); + } } else { Header connectionHeader = getResponseHeader("connection"); if (null != connectionHeader && "close".equalsIgnoreCase(connectionHeader.getValue())) { @@ -507,22 +511,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 { + 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 httpe) { log.warn(httpe.getMessage()); } catch (UnsupportedOperationException uoe) { @@ -784,6 +811,7 @@ addHostRequestHeader(state,conn); addCookieRequestHeader(state,conn); addAuthorizationRequestHeader(state,conn); + addProxyAuthorizationRequestHeader(state, conn); addContentLengthRequestHeader(state,conn); } @@ -841,8 +869,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); @@ -854,6 +882,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 @@ -1197,13 +1244,14 @@ if ("chunked".equalsIgnoreCase(transferEncodingHeader.getValue())) { expectedLength = -1; } - } else if(canResponseHaveBody(statusCode)){ + } else if(canResponseHaveBody(statusCode) && !getName().equals(ConnectMethod.NAME)){ /* * According to the specification, a response with neither Content-Length * nor Transfer-Encoding indicates that the response has no body. In * the real world, this usually means that the server just didn't know * the content-length when it sent the response. FIXME: Should we do * this only in non-strict mode? + * If we do this we will hang forever waiting for a body! */ expectedLength = -1; } @@ -1326,7 +1374,7 @@ buf.append(qString); } - if (!connection.isProxied()) { + if (!connection.isProxied() || connection.isTransparent()) { return (name + " " + buf.toString() + " " + protocol + "\r\n"); } else { if (connection.isSecure()) { Index: 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 -w -r1.10 GetMethod.java --- org/apache/commons/httpclient/methods/GetMethod.java 29 Apr 2002 16:04:05 -0000 1.10 +++ org/apache/commons/httpclient/methods/GetMethod.java 17 Jul 2002 15:09:16 -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: 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.11 diff -u -w -r1.11 PostMethod.java --- org/apache/commons/httpclient/methods/PostMethod.java 10 Jul 2002 11:33:29 -0000 1.11 +++ org/apache/commons/httpclient/methods/PostMethod.java 17 Jul 2002 15:09:17 -0000 @@ -73,6 +73,9 @@ import org.apache.commons.httpclient.log.LogSource; import java.util.Vector; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.IOException; import java.util.Iterator; import java.util.List; @@ -99,7 +102,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 @@ -108,6 +122,7 @@ */ public PostMethod() { super(); + setFollowRedirects(false); } /** @@ -116,6 +131,7 @@ */ public PostMethod(String path) { super(path); + setFollowRedirects(false); } /** @@ -125,6 +141,7 @@ */ public PostMethod(String path, String tempDir) { super(path, tempDir); + setFollowRedirects(false); } /** @@ -135,6 +152,7 @@ */ public PostMethod(String path, String tempDir, String tempFile) { super(path, tempDir, tempFile); + setFollowRedirects(false); } @@ -149,12 +167,26 @@ } /** + * A POST request can only be redirected if input is buffered. + * Overrides method of {@link HttpMethodBase}. + * @return true if request is buffered and setFollowRedirects + * was set to true. + */ + public boolean getFollowRedirects() { + if (!super.getFollowRedirects()) return false; + return (buffer == null); + } + + + /** * Override method of {@link HttpMethodBase} * to clear my request body. */ public void recycle() { super.recycle(); requestBody = null; + requestContentLength = CONTENT_LENGTH_AUTO; + buffer = null; parameters.clear(); } @@ -174,6 +206,30 @@ } /** + * Sets length information about the request body. + *

Note: If you specify a content length the request is unbuffered. This + * prevents automatic retry if a request fails the first time. This means + * that the HttpClient can not perform authorization automatically but will + * throw an Exception. You will have to set the necessary 'Authorization' or + * 'Proxy-Authorization' headers manually.

+ * + * @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; + } + + /** * Add a new parameter to be used in the POST request body. * * @param paramName The parameter name to add. @@ -238,6 +294,7 @@ /** * Gets the parameter of the specified name. + * * If there exists more than one parameter with the name paramName, * then only the first one is returned. * @@ -261,6 +318,7 @@ /** * Gets the parameters currently added to the PostMethod. + * * If there are no parameters, a valid array is returned with * zero elements. * The returned array object contains an array of pointers to @@ -345,11 +403,20 @@ * cannot be altered until I am {@link #recycle recycled}. * * @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; } @@ -360,7 +427,7 @@ * request body from the paramters if they exist. Null otherwise. * @since 2.0 */ - public String getRequestBody() { + public InputStream getRequestBody() { if(requestBody != null){ return requestBody; }else if (!parameters.isEmpty()){ @@ -386,6 +453,9 @@ * 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. + * *

Once this method has been invoked, the request parameters * cannot be altered until I am {@link #recycle recycled}. * @@ -396,7 +466,31 @@ if(null == requestBody) { requestBody = generateRequestBody(parameters); } - conn.print(requestBody); + if ((repeatCount > 0) && (buffer == null)) { + throw new HttpException("Sorry, unbuffered POST request can not be repeated."); + } + repeatCount++; + + 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; } @@ -409,10 +503,41 @@ protected int getRequestContentLength() { if(null == requestBody) { requestBody = generateRequestBody(parameters); + bufferContent(); + } + + if (requestContentLength != CONTENT_LENGTH_AUTO) { + return requestContentLength; } - return requestBody.getBytes().length; + + 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; + } + } // -------------------------------------------------------------- Class Methods @@ -422,7 +547,7 @@ * TODO: consider moving this out into URIUtil. * @return urlencoded string */ - static String generateRequestBody(List params) { + static InputStream generateRequestBody(List params) { Iterator it = params.iterator(); StringBuffer sb = new StringBuffer(); while(it.hasNext()) { @@ -435,15 +560,16 @@ sb.append("&"); } } - return sb.toString(); + return new ByteArrayInputStream(sb.toString().getBytes()); } - // -------------------------------------------------------------- Instance Variables - private String requestBody = null; + private InputStream requestBody = null; private Vector parameters = new Vector(); - + private int requestContentLength = CONTENT_LENGTH_AUTO; + private ByteArrayOutputStream buffer = null; + private int repeatCount = 0; // -------------------------------------------------------------- Constants Index: org/apache/commons/httpclient/methods/PutMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/PutMethod.java,v retrieving revision 1.9 diff -u -w -r1.9 PutMethod.java --- org/apache/commons/httpclient/methods/PutMethod.java 25 Apr 2002 20:09:45 -0000 1.9 +++ org/apache/commons/httpclient/methods/PutMethod.java 17 Jul 2002 15:09:17 -0000 @@ -96,6 +96,7 @@ * No-arg constructor. */ public PutMethod() { + setFollowRedirects(false); } @@ -105,6 +106,7 @@ */ public PutMethod(String path) { super(path); + setFollowRedirects(false); }