? ortwin.diff 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.18 diff -u -w -r1.18 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 19 Jul 2002 08:24:04 -0000 @@ -64,6 +64,7 @@ import java.io.IOException; import java.net.URL; +import javax.net.ssl.SSLSocketFactory; import org.apache.commons.httpclient.log.Log; import org.apache.commons.httpclient.log.LogSource; @@ -78,8 +79,10 @@ * @author Rodney Waldhoff * @author Sean C. Sullivan * @author dIon Gillard + * @author Ortwin Glück * @version $Revision: 1.48 $ $Date: 2002/07/14 04:40:00 $ */ + public class HttpClient { @@ -108,6 +111,10 @@ */ private HttpState state; + private SSLSocketFactory sslSocketFactory = null; + + private int timeout = 0; + // ------------------------------------------------------------- Properties /** @@ -133,6 +140,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 +335,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 19 Jul 2002 08:24:05 -0000 @@ -69,6 +69,7 @@ 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; @@ -81,6 +82,7 @@ *
* @author Rod Waldhoff * @author Sean C. Sullivan + * @author Ortwin Glück * @version $Revision: 1.10 $ $Date: 2002/07/14 05:16:06 $ */ public class HttpConnection { @@ -144,6 +146,18 @@ // ------------------------------------------ Attribute Setters and Getters /** + * Specifies an alternative factory for SSL sockets. Iffactory
+ * 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 +297,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 +317,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 +341,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 +459,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 +664,8 @@
}
_socket = null;
_open = false;
+ _tunnelEstablished = false;
+ _usingSecureSocket = false;
}
/**
@@ -648,5 +718,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.32
diff -u -w -r1.32 HttpMethodBase.java
--- org/apache/commons/httpclient/HttpMethodBase.java 18 Jul 2002 17:05:51 -0000 1.32
+++ org/apache/commons/httpclient/HttpMethodBase.java 19 Jul 2002 08:24:07 -0000
@@ -113,6 +113,7 @@
* @author Sean C. Sullivan
* @author dIon Gillard
* @author Jeff Dever
+ * @author Ortwin Glück
* @version $Revision: 1.32 $ $Date: 2002/07/18 17:05:51 $
*/
public abstract class HttpMethodBase implements HttpMethod {
@@ -280,7 +281,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());
@@ -463,9 +463,11 @@
//pre-emptively add the authorization header, if required.
Authenticator.authenticate(this, state);
+ if (connection.isProxied()) Authenticator.authenticateProxy(this, state);
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")));
@@ -508,10 +510,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())) {
@@ -522,23 +528,46 @@
}
}
- 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 + ".");
+ log.info("Already tried to authenticate to \"" + wwwauth.getValue() + "\" but still receiving " + statusCode + ".");
}
break;
} else {
- realms.add(pathAndCreds);
+ realmsUsed.add(pathAndCreds);
}
- removeRequestHeader(Authenticator.WWW_AUTH_RESP); //remove preemptively header
boolean authenticated = false;
try {
+ switch (statusCode) {
+ case HttpStatus.SC_UNAUTHORIZED:
+ removeRequestHeader(Authenticator.WWW_AUTH_RESP); //remove preemptively header
authenticated = Authenticator.authenticate(this, state);
+ break;
+
+ case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
+ removeRequestHeader(Authenticator.PROXY_AUTH_RESP); //remove preemptively header
+ authenticated = Authenticator.authenticateProxy(this,state);
+ break;
+ }
} catch (HttpException httpe) {
log.warn(httpe.getMessage());
} catch (UnsupportedOperationException uoe) {
@@ -800,6 +829,7 @@
addHostRequestHeader(state,conn);
addCookieRequestHeader(state,conn);
addAuthorizationRequestHeader(state,conn);
+ addProxyAuthorizationRequestHeader(state, conn);
addContentLengthRequestHeader(state,conn);
}
@@ -857,8 +887,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);
@@ -870,6 +900,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
@@ -1213,13 +1262,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;
}
@@ -1342,7 +1392,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/HttpMultiClient.java
===================================================================
RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMultiClient.java,v
retrieving revision 1.5
diff -u -w -r1.5 HttpMultiClient.java
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 19 Jul 2002 08:24:11 -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 19 Jul 2002 08:24:12 -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;
@@ -97,9 +100,21 @@
* @author Jeffrey Dever
* @author Remy Maucherat
* @author Doug Sale
+ * @author Ortwin Glück
*/
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.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 +207,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 +295,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 +319,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 +404,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 +428,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 +454,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 +467,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 +504,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 +548,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 +561,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 19 Jul 2002 08:24:12 -0000 @@ -96,6 +96,7 @@ * No-arg constructor. */ public PutMethod() { + setFollowRedirects(false); } @@ -105,6 +106,7 @@ */ public PutMethod(String path) { super(path); + setFollowRedirects(false); }