Index: examples/ProxyTunnelDemo.java =================================================================== RCS file: examples/ProxyTunnelDemo.java diff -N examples/ProxyTunnelDemo.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ examples/ProxyTunnelDemo.java 4 Apr 2004 10:29:22 -0000 @@ -0,0 +1,76 @@ +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.Socket; + +import org.apache.commons.httpclient.ProxyClient; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.HttpAuthRealm; + +/* + * $Header$ + * $Revision$ + * $Date$ + * ==================================================================== + * + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +/** + * @author Oleg Kalnichevski + */ +public class ProxyTunnelDemo { + + public static void main(String[] args) throws Exception { + + ProxyClient proxyclient = new ProxyClient(); + proxyclient.getHostConfiguration().setHost("www.yahoo.com"); + proxyclient.getHostConfiguration().setProxy("localhost", 8888); + proxyclient.getState().setProxyCredentials( + new HttpAuthRealm("localhost", 8888, "squid"), + new UsernamePasswordCredentials("squid", "squid")); + Socket socket = proxyclient.connect(); + if (socket != null) { + try { + Writer out = new OutputStreamWriter( + socket.getOutputStream(), "ISO-8859-1"); + out.write("GET http://www.yahoo.com/ HTTP/1.1\r\n"); + out.write("Host: www.yahoo.com\r\n"); + out.write("Agent: whatever\r\n"); + out.write("\r\n"); + out.flush(); + BufferedReader in = new BufferedReader( + new InputStreamReader(socket.getInputStream(), "ISO-8859-1")); + String line = null; + while ((line = in.readLine()) != null) { + System.out.println(line); + } + } finally { + socket.close(); + } + } + } + +} 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.85 diff -u -r1.85 HttpConnection.java --- java/org/apache/commons/httpclient/HttpConnection.java 22 Feb 2004 18:08:45 -0000 1.85 +++ java/org/apache/commons/httpclient/HttpConnection.java 4 Apr 2004 10:29:26 -0000 @@ -1,5 +1,5 @@ /* - * $Header: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v 1.85 2004/02/22 18:08:45 olegk Exp $ + * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v 1.85 2004/02/22 18:08:45 olegk Exp $ * $Revision: 1.85 $ * $Date: 2004/02/22 18:08:45 $ * @@ -199,6 +199,15 @@ } // ------------------------------------------ Attribute Setters and Getters + + /** + * Returns the connection socket. + * + * @return the socket. + */ + protected Socket getSocket() { + return this.socket; + } /** * Returns the host. 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.19 diff -u -r1.19 HttpMethodDirector.java --- java/org/apache/commons/httpclient/HttpMethodDirector.java 25 Mar 2004 20:37:19 -0000 1.19 +++ java/org/apache/commons/httpclient/HttpMethodDirector.java 4 Apr 2004 10:29:28 -0000 @@ -1,5 +1,5 @@ /* - * $Header: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodDirector.java,v 1.19 2004/03/25 20:37:19 olegk Exp $ + * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodDirector.java,v 1.19 2004/03/25 20:37:19 olegk Exp $ * $Revision: 1.19 $ * $Date: 2004/03/25 20:37:19 $ * @@ -278,7 +278,6 @@ private void authenticateProxy(final HttpMethod method) throws AuthenticationException { - // Clean up existing authentication headers // Clean up existing authentication headers if (!cleanAuthHeaders(method, PROXY_AUTH_RESP)) { // User defined authentication header(s) present Index: java/org/apache/commons/httpclient/ProxyClient.java =================================================================== RCS file: java/org/apache/commons/httpclient/ProxyClient.java diff -N java/org/apache/commons/httpclient/ProxyClient.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ java/org/apache/commons/httpclient/ProxyClient.java 4 Apr 2004 10:29:29 -0000 @@ -0,0 +1,410 @@ +/* + * $Header$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * Copyright 1999-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient; + +import java.io.IOException; +import java.net.Socket; +import java.util.Map; + +import org.apache.commons.httpclient.auth.AuthChallengeException; +import org.apache.commons.httpclient.auth.AuthChallengeParser; +import org.apache.commons.httpclient.auth.AuthChallengeProcessor; +import org.apache.commons.httpclient.auth.AuthScheme; +import org.apache.commons.httpclient.auth.AuthState; +import org.apache.commons.httpclient.auth.AuthenticationException; +import org.apache.commons.httpclient.auth.CredentialsNotAvailableException; +import org.apache.commons.httpclient.auth.CredentialsProvider; +import org.apache.commons.httpclient.auth.HttpAuthRealm; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.commons.httpclient.params.HttpParams; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * @author Oleg Kalnichevski + * + * @version $Revision$ + */ +public class ProxyClient { + + + // -------------------------------------------------------------- Constants + + /** Log object for this class. */ + private static final Log LOG = LogFactory.getLog(ProxyClient.class); + + // ----------------------------------------------------- Instance Variables + + /** The proxy authenticate response header. */ + public static final String PROXY_AUTH_RESP = "Proxy-Authorization"; + + /** The proxy authenticate challange header. */ + public static final String PROXY_AUTH_CHALLENGE = "Proxy-Authenticate"; + + /** Maximum number of authentications that will be followed */ + private static final int MAX_FORWARDS = 100; + + /** + * The {@link HttpState HTTP state} associated with this ProxyClient. + */ + private HttpState state = new HttpState(); + + /** + * The {@link HttpClientParams collection of parameters} associated with this ProxyClient. + */ + private HttpClientParams params = null; + + /** + * The {@link HostConfiguration host configuration} associated with + * the ProxyClient + */ + private HostConfiguration hostConfiguration = new HostConfiguration(); + + /** Authentication processor */ + private AuthChallengeProcessor authProcessor = null; + + private static final int AUTH_UNINITIATED = 0; + private static final int AUTH_PREEMPTIVE = 1; + private static final int AUTH_PROXY_REQUIRED = 3; + private static final int AUTH_NOT_REQUIRED = Integer.MAX_VALUE; + + /** Actual state of authentication process */ + private int authProcess = AUTH_UNINITIATED; + /** + * Creates an instance of ProxyClient using default {@link HttpClientParams parameter set}. + * + * @see HttpClientParams + */ + public ProxyClient() { + this(new HttpClientParams()); + } + + /** + * Creates an instance of ProxyClient using the given + * {@link HttpClientParams parameter set}. + * + * @param params The {@link HttpClientParams parameters} to use. + * + * @see HttpClientParams + * + * @since 2.1 + */ + public ProxyClient(HttpClientParams params) { + super(); + if (params == null) { + throw new IllegalArgumentException("Params may not be null"); + } + this.params = params; + this.authProcessor = new AuthChallengeProcessor(this.params); + } + + // ------------------------------------------------------------- Properties + + /** + * Returns {@link HttpState HTTP state} associated with the ProxyClient. + * + * @see #setState(HttpState) + * @return the shared client state + */ + public synchronized HttpState getState() { + return state; + } + + /** + * Assigns {@link HttpState HTTP state} for the ProxyClient. + * + * @see #getState() + * @param state the new {@link HttpState HTTP state} for the client + */ + public synchronized void setState(HttpState state) { + this.state = state; + } + + /** + * Returns the {@link HostConfiguration host configuration} associated with the + * ProxyClient. + * + * @return {@link HostConfiguration host configuration} + * + * @since 2.0 + */ + public synchronized HostConfiguration getHostConfiguration() { + return hostConfiguration; + } + + /** + * Assigns the {@link HostConfiguration host configuration} to use with the + * ProxyClient. + * + * @param hostConfiguration The {@link HostConfiguration host configuration} to set + * + * @since 2.0 + */ + public synchronized void setHostConfiguration(HostConfiguration hostConfiguration) { + this.hostConfiguration = hostConfiguration; + } + + /** + * Returns {@link HttpClientParams HTTP protocol parameters} associated with this ProxyClient. + * + * @since 2.1 + * + * @see HttpClientParams + */ + public synchronized HttpClientParams getParams() { + return this.params; + } + + /** + * Assigns {@link HttpClientParams HTTP protocol parameters} for this ProxyClient. + * + * @since 2.1 + * + * @see HttpClientParams + */ + public synchronized void setParams(final HttpClientParams params) { + if (params == null) { + throw new IllegalArgumentException("Parameters may not be null"); + } + this.params = params; + } + + + private void authenticateProxy(final HttpMethod method, final HttpConnection conn) throws AuthenticationException { + method.removeRequestHeader(PROXY_AUTH_RESP); + AuthScheme authscheme = method.getProxyAuthState().getAuthScheme(); + if (authscheme == null) { + return; + } + if ((this.authProcess == AUTH_PROXY_REQUIRED) || (!authscheme.isConnectionBased())) { + HttpAuthRealm realm = new HttpAuthRealm( + conn.getProxyHost(), conn.getProxyPort(), + authscheme.getRealm(), + authscheme.getSchemeName()); + if (LOG.isDebugEnabled()) { + LOG.debug("Authenticating with " + realm); + } + Credentials credentials = this.state.getProxyCredentials(realm); + if (credentials != null) { + String authstring = authscheme.authenticate(credentials, method); + if (authstring != null) { + method.addRequestHeader(new Header(PROXY_AUTH_RESP, authstring, true)); + } + } else { + LOG.warn("Required credentials not available"); + } + } + } + + + private Credentials promptForProxyCredentials( + final AuthScheme authScheme, + final HttpParams params, + final HttpAuthRealm realm) + { + Credentials creds = null; + CredentialsProvider credProvider = + (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER); + if (credProvider != null) { + try { + creds = credProvider.getCredentials( + authScheme, realm.getHost(), realm.getPort(), true); + } catch (CredentialsNotAvailableException e) { + LOG.warn(e.getMessage()); + } + if (creds != null) { + this.state.setProxyCredentials(realm, creds); + if (LOG.isDebugEnabled()) { + LOG.debug("New proxy credentials for " + realm); + } + } + } + return creds; + } + + private boolean processAuthenticationResponse(final HttpMethod method, final HttpConnection conn) { + try { + AuthState authstate = method.getProxyAuthState(); + if (authstate.isPreemptive()) { + authstate.invalidate(); + } + Map proxyChallenges = AuthChallengeParser.parseChallenges( + method.getResponseHeaders(PROXY_AUTH_CHALLENGE)); + if (proxyChallenges.isEmpty()) { + return false; + } + AuthScheme authscheme = null; + try { + authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges); + } catch (AuthChallengeException e) { + if (LOG.isWarnEnabled()) { + LOG.warn(e.getMessage()); + } + } + if (authscheme == null) { + return false; + } + HttpAuthRealm realm = new HttpAuthRealm( + conn.getProxyHost(), conn.getProxyPort(), + authscheme.getRealm(), + authscheme.getSchemeName()); + + if ((this.authProcess == AUTH_PROXY_REQUIRED) && (authscheme.isComplete())) { + // Already tried and failed + Credentials credentials = promptForProxyCredentials( + authscheme, method.getParams(), realm); + if (credentials == null) { + if (LOG.isInfoEnabled()) { + LOG.info("Failure authenticating with " + realm); + } + return false; + } else { + return true; + } + } else { + this.authProcess = AUTH_PROXY_REQUIRED; + + Credentials credentials = this.state.getProxyCredentials(realm); + if (credentials == null) { + credentials = promptForProxyCredentials( + authscheme, method.getParams(), realm); + } + if (credentials == null) { + if (LOG.isInfoEnabled()) { + LOG.info("No proxy credentials available for the " + realm); + } + return false; + } else { + return true; + } + } + } catch (Exception e) { + if (LOG.isErrorEnabled()) { + LOG.error(e.getMessage(), e); + } + return false; + } + } + + public synchronized Socket connect() throws IOException, HttpException { + ConnectMethod method = new ConnectMethod(); + method.getParams().setDefaults(this.params); + HttpConnection conn = new HttpConnection(this.hostConfiguration); + conn.getParams().setDefaults(this.params); + // Just to keep the connection happy + conn.setHttpConnectionManager(new DummyConnectionManager()); + + if (!conn.isProxied()) { + throw new IllegalStateException("Connection is not proxied"); + } + + if (this.params.isAuthenticationPreemptive()) { + LOG.debug("Preemptively sending default basic credentials"); + method.getProxyAuthState().setPreemptive(); + this.authProcess = AUTH_PREEMPTIVE; + } + + int code = -1; + int forwardCount = 0; //protect from an infinite loop + while (forwardCount++ < MAX_FORWARDS) { + // on every retry, reset this state information. + if (LOG.isDebugEnabled()) { + LOG.debug("Execute loop try " + forwardCount); + } + try { + authenticateProxy(method, conn); + } catch (AuthenticationException e) { + LOG.error(e.getMessage(), e); + } + + if (!conn.isOpen) { + conn.open(); + } + method.execute(this.state, conn); + code = method.getStatusCode(); + boolean retry = false; + if (code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { + if (processAuthenticationResponse(method, conn)) { + retry = true; + } + } else { + this.authProcess = AUTH_NOT_REQUIRED; + } + if (!retry) { + break; + } + if (method.getResponseBodyAsStream() != null) { + method.getResponseBodyAsStream().close(); + } + } + if ((code >= 200) && (code < 300)) { + return conn.getSocket(); + } else { + conn.close(); + return null; + } + } + + class DummyConnectionManager implements HttpConnectionManager { + + /** + * @deprecated + */ + public HttpConnection getConnection(HostConfiguration hostConfiguration, long timeout) + throws HttpException + { + return null; + } + + public HttpConnection getConnection(HostConfiguration hostConfiguration) { + return null; + } + + public HttpConnection getConnectionWithTimeout(HostConfiguration hostConfiguration, long timeout) + throws ConnectTimeoutException + { + return null; + } + + public HttpConnectionManagerParams getParams() { + return null; + } + + public void releaseConnection(HttpConnection conn) { + } + + public void setParams(HttpConnectionManagerParams params) { + } + } +}