Index: examples/TrivialApp.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/examples/TrivialApp.java,v retrieving revision 1.15 diff -u -r1.15 TrivialApp.java --- examples/TrivialApp.java 26 Oct 2003 09:49:16 -0000 1.15 +++ examples/TrivialApp.java 16 Nov 2003 16:15:16 -0000 @@ -1,5 +1,5 @@ /* - * $Header: /home/cvspublic/jakarta-commons/httpclient/src/examples/TrivialApp.java,v 1.15 2003/10/26 09:49:16 olegk Exp $ + * $Header: /home/cvs/jakarta-commons/httpclient/src/examples/TrivialApp.java,v 1.15 2003/10/26 09:49:16 olegk Exp $ * $Revision: 1.15 $ * $Date: 2003/10/26 09:49:16 $ * @@ -124,7 +124,6 @@ //create a method object method = new GetMethod(url); method.setFollowRedirects(true); - method.setStrictMode(false); //} catch (MalformedURLException murle) { // System.out.println(" argument '" + url // + "' is not a valid URL"); 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.31 diff -u -r1.31 HttpMethod.java --- java/org/apache/commons/httpclient/HttpMethod.java 10 Nov 2003 23:19:49 -0000 1.31 +++ java/org/apache/commons/httpclient/HttpMethod.java 16 Nov 2003 16:15:19 -0000 @@ -154,6 +154,9 @@ * * @param strictMode true for strict mode, false otherwise * + * @deprecated Use {@link HttpParams#setParameter(String, Object)} to exercise + * a more granular control over HTTP protocol strictness. + * * @see #isStrictMode() */ void setStrictMode(boolean strictMode); @@ -162,6 +165,9 @@ * Returns the value of the strict mode flag. * * @return true if strict mode is enabled, false otherwise + * + * @deprecated Use {@link HttpParams#setParameter(String, Object)} to exercise + * a more granular control over HTTP protocol strictness. * * @see #setStrictMode(boolean) */ Index: java/org/apache/commons/httpclient/HttpMethodDirector.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodDirector.java,v retrieving revision 1.9 diff -u -r1.9 HttpMethodDirector.java --- java/org/apache/commons/httpclient/HttpMethodDirector.java 13 Nov 2003 22:24:47 -0000 1.9 +++ java/org/apache/commons/httpclient/HttpMethodDirector.java 16 Nov 2003 16:15:23 -0000 @@ -86,7 +86,7 @@ private static final Log LOG = LogFactory.getLog(HttpMethodDirector.class); - private HttpMethod method; + private ConnectMethod connectMethod; private HttpState state; @@ -139,8 +139,7 @@ { throw new IllegalArgumentException("Method may not be null"); } - this.method = method; - this.method.getParams().setDefaults(this.params); + method.getParams().setDefaults(this.params); try { int forwardCount = 0; //protect from an infinite loop @@ -168,10 +167,26 @@ realms = new HashSet(); proxyRealms = new HashSet(); - addPreemtiveAuthenticationHeaders(); + addPreemtiveAuthenticationHeaders(method); } - executeWithRetry(this.method); - if (!isRetryNeeded()) { + executeWithRetry(method); + if (this.connectMethod != null) { + //CONNECT method failed. Bow out. + fakeResponse(method); + break; + } else if (isRedirectNeeded(method)) { + //if the redirect is unsuccessful, return the statusCode + //otherwise, drop through the switch and try again. + if (!processRedirectResponse(method)) { + break; + } + } else if (isAuthenticationNeeded(method)) { + //if the authentication is unsuccessful, return the statusCode + //otherwise, drop through the switch and try again. + if (!processAuthenticationResponse(method)) { + break; + } + } else { // nope, no retry needed, exit loop. break; } @@ -179,8 +194,8 @@ // retry - close previous stream. Caution - this causes // responseBodyConsumed to be called, which may also close the // connection. - if (this.method.getResponseBodyAsStream() != null) { - this.method.getResponseBodyAsStream().close(); + if (method.getResponseBodyAsStream() != null) { + method.getResponseBodyAsStream().close(); } } //end of retry loop @@ -201,7 +216,7 @@ // for example, reading the entire response into a file and then // setting the file as the stream. if ( - (releaseConnection || this.method.getResponseBodyAsStream() == null) + (releaseConnection || method.getResponseBodyAsStream() == null) && this.conn != null ) { this.conn.releaseConnection(); @@ -210,30 +225,42 @@ } + private void applyDefaultCredentials(final HttpMethod method) { + try { + if (HttpAuthenticator.authenticateDefault(method, this.conn, state)) { + LOG.debug("Default basic credentials applied"); + } + } catch (AuthenticationException e) { + // Log error and move on + LOG.error(e.getMessage(), e); + } + } + + private void applyDefaultProxyCredentials(final HttpMethod method) { + try { + if (HttpAuthenticator.authenticateProxyDefault(method, this.conn, state)) { + LOG.debug("Default basic proxy credentials applied"); + } + } catch (AuthenticationException e) { + // Log error and move on + LOG.error(e.getMessage(), e); + } + } + /** * Adds authentication headers if authenticationPreemtive has been set. * - * @see HttpState#isAuthenticationPreemptive() + * @see HttpClientParams#isAuthenticationPreemptive() */ - private void addPreemtiveAuthenticationHeaders() { - + private void addPreemtiveAuthenticationHeaders(final HttpMethod method) { + //pre-emptively add the authorization header, if required. if (this.params.isAuthenticationPreemptive()) { LOG.debug("Preemptively sending default basic credentials"); - - try { - if (HttpAuthenticator.authenticateDefault(this.method, this.conn, state)) { - LOG.debug("Default basic credentials applied"); - } - if (this.conn.isProxied()) { - if (HttpAuthenticator.authenticateProxyDefault(this.method, this.conn, state)) { - LOG.debug("Default basic proxy credentials applied"); - } - } - } catch (AuthenticationException e) { - // Log error and move on - LOG.error(e.getMessage(), e); + applyDefaultCredentials(method); + if (this.conn.isProxied()) { + applyDefaultProxyCredentials(method); } } } @@ -246,7 +273,7 @@ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ - private void executeWithRetry(final HttpMethod curmethod) + private void executeWithRetry(final HttpMethod method) throws IOException, HttpException { /** How many times did this transparently handle a recoverable exception? */ @@ -270,11 +297,11 @@ try { this.conn.open(); if (this.conn.isProxied() && this.conn.isSecure() - && !(curmethod instanceof ConnectMethod)) { + && !(method instanceof ConnectMethod)) { // we need to create a secure tunnel before we can execute the real method if (!executeConnect()) { // abort, the connect method failed - break; + return; } } } catch (IOException e) { @@ -286,7 +313,7 @@ } } try { - curmethod.execute(state, this.conn); + method.execute(state, this.conn); break; } catch (HttpRecoverableException httpre) { if (LOG.isDebugEnabled()) { @@ -298,12 +325,12 @@ recoverableExceptionCount++; // test if this method should be retried - MethodRetryHandler handler = curmethod.getMethodRetryHandler(); + MethodRetryHandler handler = method.getMethodRetryHandler(); if (handler == null) { handler = new DefaultMethodRetryHandler(); } if (!handler.retryMethod( - curmethod, + method, this.conn, httpre, execCount, @@ -333,66 +360,92 @@ private boolean executeConnect() throws IOException, HttpException { - ConnectMethod connectMethod = new ConnectMethod(); - executeWithRetry(connectMethod); - int code = connectMethod.getStatusCode(); + this.connectMethod = new ConnectMethod(); + this.connectMethod.getParams().setDefaults(this.params); + int code; + if (this.params.isAuthenticationPreemptive()) { + applyDefaultProxyCredentials(this.connectMethod); + } + for (;;) { + executeWithRetry(this.connectMethod); + code = this.connectMethod.getStatusCode(); + if (code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { + if (!processAuthenticationResponse(this.connectMethod)) { + break; + } + } else { + break; + } + // retry - close previous stream. Caution - this causes + // responseBodyConsumed to be called, which may also close the + // connection. + if (this.connectMethod.getResponseBodyAsStream() != null) { + this.connectMethod.getResponseBodyAsStream().close(); + } + } if ((code >= 200) && (code < 300)) { this.conn.tunnelCreated(); + // Drop the connection method, as it is no longer needed + this.connectMethod = null; return true; } else { - // What is to follow is an ugly hack. - // I REALLY hate having to resort to such - // an appalling trick - // The only feasible solution is to split monolithic - // HttpMethod into HttpRequest/HttpResponse pair. - // That would allow to execute CONNECT method - // behind the scene and return CONNECT HttpResponse - // object in response to the original request that - // contains the correct status line, headers & - // response body. - - LOG.debug("CONNECT failed, fake the response for the original method"); - // Pass the status, headers and response stream to the wrapped - // method. - // To ensure that the connection is not released more than once - // this method is still responsible for releasing the connection. - // This will happen when the response body is consumed, or when - // the wrapped method closes the response connection in - // releaseConnection(). - if (this.method instanceof HttpMethodBase) { - ((HttpMethodBase) this.method).fakeResponse( - connectMethod.getStatusLine(), - connectMethod.getResponseHeaderGroup(), - connectMethod.getResponseBodyAsStream() - ); - } else { - releaseConnection = true; - LOG.warn( - "Unable to fake response on method as it is not derived from HttpMethodBase."); - } return false; } } + + /** + * Fake response + * @param method + * @return + */ + + private void fakeResponse(final HttpMethod method) + throws IOException, HttpException { + // What is to follow is an ugly hack. + // I REALLY hate having to resort to such + // an appalling trick + // The only feasible solution is to split monolithic + // HttpMethod into HttpRequest/HttpResponse pair. + // That would allow to execute CONNECT method + // behind the scene and return CONNECT HttpResponse + // object in response to the original request that + // contains the correct status line, headers & + // response body. + LOG.debug("CONNECT failed, fake the response for the original method"); + // Pass the status, headers and response stream to the wrapped + // method. + // To ensure that the connection is not released more than once + // this method is still responsible for releasing the connection. + // This will happen when the response body is consumed, or when + // the wrapped method closes the response connection in + // releaseConnection(). + if (method instanceof HttpMethodBase) { + ((HttpMethodBase) method).fakeResponse( + this.connectMethod.getStatusLine(), + this.connectMethod.getResponseHeaderGroup(), + this.connectMethod.getResponseBodyAsStream() + ); + this.connectMethod = null; + } else { + releaseConnection = true; + LOG.warn( + "Unable to fake response on method as it is not derived from HttpMethodBase."); + } + } /** * Process the redirect response. * * @return true if the redirect was successful */ - private boolean processRedirectResponse() { - - if (!this.method.getFollowRedirects()) { - LOG.info("Redirect requested but followRedirects is " - + "disabled"); - return false; - } + private boolean processRedirectResponse(final HttpMethod method) { //get the location header to find out where to redirect to - Header locationHeader = this.method.getResponseHeader("location"); + Header locationHeader = method.getResponseHeader("location"); if (locationHeader == null) { // got a redirect response, but no location header - LOG.error("Received redirect response " + this.method.getStatusCode() + LOG.error("Received redirect response " + method.getStatusCode() + " but no location header"); return false; } @@ -413,13 +466,12 @@ null, this.conn.getHost(), this.conn.getPort(), - this.method.getPath() + method.getPath() ); redirectUri = new URI(location, true); if (redirectUri.isRelativeURI()) { - if (this.method.isStrictMode()) { - LOG.warn("Redirected location '" + location - + "' is not acceptable in strict mode"); + if (this.params.isParameterTrue(HttpClientParams.REJECT_RELATIVE_REDIRECT)) { + LOG.warn("Relative redirect location '" + location + "' not allowed"); return false; } else { //location is incomplete, use current values for defaults @@ -435,12 +487,12 @@ //invalidate the list of authentication attempts this.realms.clear(); //remove exisitng authentication headers - this.method.removeRequestHeader(HttpAuthenticator.WWW_AUTH_RESP); + method.removeRequestHeader(HttpAuthenticator.WWW_AUTH_RESP); //update the current location with the redirect location. //avoiding use of URL.getPath() and URL.getQuery() to keep //jdk1.2 comliance. - this.method.setPath(redirectUri.getEscapedPath()); - this.method.setQueryString(redirectUri.getEscapedQuery()); + method.setPath(redirectUri.getEscapedPath()); + method.setQueryString(redirectUri.getEscapedQuery()); hostConfiguration.setHost(redirectUri); if (LOG.isDebugEnabled()) { @@ -457,21 +509,23 @@ * @param state the current state * @param conn The connection * - * @return true if the request has completed processing, false - * if more attempts are needed + * @return true if the authentication challenge can be responsed to, + * (that is, at least one of the requested authentication scheme is supported, + * and matching credentials have been found), false otherwise. */ - private boolean processAuthenticationResponse() { + private boolean processAuthenticationResponse(final HttpMethod method) { LOG.trace("enter HttpMethodBase.processAuthenticationResponse(" + "HttpState, HttpConnection)"); - int statusCode = this.method.getStatusCode(); + boolean authenticated = false; + int statusCode = method.getStatusCode(); // handle authentication required Header[] challenges = null; Set realmsUsed = null; String host = null; switch (statusCode) { case HttpStatus.SC_UNAUTHORIZED: - challenges = this.method.getResponseHeaders(HttpAuthenticator.WWW_AUTH); + challenges = method.getResponseHeaders(HttpAuthenticator.WWW_AUTH); realmsUsed = realms; host = this.conn.getVirtualHost(); if (host == null) { @@ -479,12 +533,11 @@ } break; case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: - challenges = this.method.getResponseHeaders(HttpAuthenticator.PROXY_AUTH); + challenges = method.getResponseHeaders(HttpAuthenticator.PROXY_AUTH); realmsUsed = proxyRealms; host = this.conn.getProxyHost(); break; } - boolean authenticated = false; // if there was a header requesting authentication if (challenges.length > 0) { AuthScheme authscheme = null; @@ -494,12 +547,12 @@ if (LOG.isErrorEnabled()) { LOG.error(e.getMessage(), e); } - return true; + return false; } catch (UnsupportedOperationException e) { if (LOG.isErrorEnabled()) { LOG.error(e.getMessage(), e); } - return true; + return false; } StringBuffer buffer = new StringBuffer(); @@ -516,27 +569,27 @@ buffer.append("' authentication realm at "); buffer.append(host); buffer.append(", but still receiving: "); - buffer.append(this.method.getStatusLine().toString()); + buffer.append(method.getStatusLine().toString()); LOG.info(buffer.toString()); } - return true; + return false; } else { realmsUsed.add(realm); } - this.method.removeRequestHeader(HttpAuthenticator.WWW_AUTH_RESP); - this.method.removeRequestHeader(HttpAuthenticator.PROXY_AUTH_RESP); + method.removeRequestHeader(HttpAuthenticator.WWW_AUTH_RESP); + method.removeRequestHeader(HttpAuthenticator.PROXY_AUTH_RESP); try { //remove preemptive header and reauthenticate switch (statusCode) { case HttpStatus.SC_UNAUTHORIZED: authenticated = HttpAuthenticator.authenticate( - authscheme, this.method, this.conn, this.state); + authscheme, method, this.conn, this.state); this.realm = authscheme.getRealm(); break; case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: authenticated = HttpAuthenticator.authenticateProxy( - authscheme, this.method, this.conn, this.state); + authscheme, method, this.conn, this.state); this.proxyRealm = authscheme.getRealm(); break; } @@ -544,12 +597,12 @@ if (LOG.isWarnEnabled()) { LOG.warn(e.getMessage()); } - return true; // finished request + return false; // finished request } catch (AuthenticationException e) { if (LOG.isErrorEnabled()) { LOG.error(e.getMessage(), e); } - return true; // finished request + return false; // finished request } if (!authenticated) { // won't be able to authenticate to this challenge @@ -564,48 +617,58 @@ } } - return !authenticated; // finished processing if we aren't authenticated + return authenticated; } - /** - * Returns true if a retry is needed. + /** + * Tests if the {@link HttpMethod method} requires a redirect to another location. * - * @return boolean true if a retry is needed. - */ - private boolean isRetryNeeded() { - switch (this.method.getStatusCode()) { - case HttpStatus.SC_UNAUTHORIZED: - case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: - LOG.debug("Authorization required"); - if (this.method.getDoAuthentication()) { //process authentication response - //if the authentication is successful, return the statusCode - //otherwise, drop through the switch and try again. - if (processAuthenticationResponse()) { - return false; - } - } else { //let the client handle the authenticaiton - return false; - } - break; - + * @param method HTTP method + * + * @return boolean true if a retry is needed, false otherwise. + */ + private boolean isRedirectNeeded(final HttpMethod method) { + switch (method.getStatusCode()) { case HttpStatus.SC_MOVED_TEMPORARILY: case HttpStatus.SC_MOVED_PERMANENTLY: case HttpStatus.SC_SEE_OTHER: case HttpStatus.SC_TEMPORARY_REDIRECT: LOG.debug("Redirect required"); - - if (!processRedirectResponse()) { - return false; - } - break; - + if (method.getFollowRedirects()) { + return true; + } else { + LOG.info("Redirect requested but followRedirects is " + + "disabled"); + return false; + } default: - // neither an unauthorized nor a redirect response return false; } //end of switch - - return true; } + + /** + * Tests if the {@link HttpMethod method} requires authentication. + * + * @param method HTTP method + * + * @return boolean true if a retry is needed, false otherwise. + */ + private boolean isAuthenticationNeeded(final HttpMethod method) { + switch (method.getStatusCode()) { + case HttpStatus.SC_UNAUTHORIZED: + case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: + LOG.debug("Authorization required"); + if (method.getDoAuthentication()) { //process authentication response + return true; + } else { //let the client handle the authenticaiton + LOG.info("Redirect requested but doAuthentication is " + + "disabled"); + return false; + } + default: + return false; + } //end of switch + } /** * @return