View Javadoc

1   /***
2   *
3   * Copyright 2003-2005 Core Developers Network Ltd.
4   *
5   *  Licensed under the Apache License, Version 2.0 (the "License");
6   *  you may not use this file except in compliance with the License.
7   *  You may obtain a copy of the License at
8   *
9   *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */
17  
18  //this started life as org.mortbay.servlet.ProxyServlet. I copied the
19  //whole thing to use as a starting point - Thanks Greg !
20  
21  // ========================================================================
22  // $Id: CommonsHttpProxy.java 1737 2006-04-27 15:13:16 +0100 (Thu, 27 Apr 2006) jules $
23  // Copyright 2004-2004 Mort Bay Consulting Pty. Ltd.
24  // ------------------------------------------------------------------------
25  // Licensed under the Apache License, Version 2.0 (the "License");
26  // you may not use this file except in compliance with the License.
27  // You may obtain a copy of the License at
28  // http://www.apache.org/licenses/LICENSE-2.0
29  // Unless required by applicable law or agreed to in writing, software
30  // distributed under the License is distributed on an "AS IS" BASIS,
31  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32  // See the License for the specific language governing permissions and
33  // limitations under the License.
34  // ========================================================================
35  
36  package org.codehaus.wadi.web.impl;
37  
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.OutputStream;
41  import java.net.URI;
42  import java.util.Enumeration;
43  import java.util.HashMap;
44  import java.util.Map;
45  
46  import javax.servlet.http.HttpServletRequest;
47  import javax.servlet.http.HttpServletResponse;
48  
49  import org.apache.commons.httpclient.ConnectMethod;
50  import org.apache.commons.httpclient.Cookie;
51  import org.apache.commons.httpclient.Header;
52  import org.apache.commons.httpclient.HostConfiguration;
53  import org.apache.commons.httpclient.HttpClient;
54  import org.apache.commons.httpclient.HttpMethod;
55  import org.apache.commons.httpclient.HttpState;
56  import org.apache.commons.httpclient.methods.DeleteMethod;
57  import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
58  import org.apache.commons.httpclient.methods.GetMethod;
59  import org.apache.commons.httpclient.methods.HeadMethod;
60  import org.apache.commons.httpclient.methods.MultipartPostMethod;
61  import org.apache.commons.httpclient.methods.OptionsMethod;
62  import org.apache.commons.httpclient.methods.PutMethod;
63  import org.apache.commons.httpclient.methods.TraceMethod;
64  import org.apache.commons.logging.Log;
65  import org.apache.commons.logging.LogFactory;
66  
67  //This does not seem as performant as the StandardHttpProxy - it also seems to be able to crash Firefox reproducibly !
68  //commons-httpclient does not [seem to] allow us to pass Cookie headers straight through. So, we have
69  //to deal with them explicitly - This leaves scope for cookie mutation as request passes through proxy - Bad news.
70  
71  //I've tried moving up to 3.0rc1, but ProxyServlet with this HttpProxy produces wierd browser effects...
72  
73  /***
74   * HttpProxy implementation based on commons-httpclient
75   *
76   * @author <a href="mailto:jules@coredevelopers.net">Jules Gosnell</a>
77   * @author <a href="mailto:gregw@mortbay.com">Greg Wilkins</a>
78   * @version $Revision: 1737 $
79   */
80  public class CommonsHttpProxy extends AbstractHttpProxy {
81  	
82  	protected static final Log _log=LogFactory.getLog(CommonsHttpProxy.class);
83  	
84  	protected static final Map _methods=new HashMap();
85  	static {
86  		_methods.put("CONNECT",ConnectMethod.class);
87  		_methods.put("DELETE",DeleteMethod.class);
88  		_methods.put("GET", GetMethod.class);
89  		_methods.put("HEAD",HeadMethod.class);
90  		_methods.put("OPTIONS",OptionsMethod.class);
91  		_methods.put("TRACE",TraceMethod.class);
92  		_methods.put("POST",MultipartPostMethod.class);
93  		_methods.put("PUT",PutMethod.class);
94  		// WebDav methods ? e.g. PROCFIND ?
95  	}
96  	
97  	public CommonsHttpProxy(String sessionPathParamKey) {
98  		super(sessionPathParamKey);
99  	}
100 	
101     protected void doProxy(URI uri, WebInvocation context) throws ProxyingException {
102 		HttpServletRequest hreq = context.getHreq();
103 		HttpServletResponse hres = context.getHres();
104 		
105 		long startTime=System.currentTimeMillis();
106 		
107 		String m=hreq.getMethod();
108 		Class clazz=(Class)_methods.get(m);
109 		if (clazz==null) {
110 			throw new IrrecoverableException("unsupported http method: "+m);
111 		}
112 		
113 		HttpMethod hm=null;
114 		try {
115 			hm=(HttpMethod)clazz.newInstance();
116 		} catch (Exception e) {
117 			throw new IrrecoverableException("could not create HttpMethod instance", e); // should never happen
118 		}
119 		
120 		String requestURI = getRequestURI(hreq);
121 		hm.setPath(requestURI);
122 		
123 		String queryString=hreq.getQueryString();
124 		if (queryString!=null) {
125 			hm.setQueryString(queryString);
126             requestURI += queryString;
127 		}
128 		
129 		hm.setFollowRedirects(false);
130 		//hm.setURI(new URI(uri));
131 		hm.setStrictMode(false);
132 		
133 		// check connection header
134 		String connectionHdr = hreq.getHeader("Connection"); // TODO - what if there are multiple values ?
135 		if (connectionHdr != null) {
136 			connectionHdr = connectionHdr.toLowerCase();
137 			if (connectionHdr.equals("keep-alive")|| connectionHdr.equals("close"))
138 				connectionHdr = null; // TODO  ??
139 		}
140 		
141 		// copy headers
142 		boolean xForwardedFor = false;
143 		boolean hasContent = false;
144 		int contentLength=0;
145 		Enumeration enm = hreq.getHeaderNames();
146 		while (enm.hasMoreElements()) {
147 			// TODO could be better than this! - using javax.servlet ?
148 			String hdr = (String) enm.nextElement();
149 			String lhdr = hdr.toLowerCase();
150 			
151 			if (_DontProxyHeaders.contains(lhdr))
152 				continue;
153 			if (connectionHdr != null && connectionHdr.indexOf(lhdr) >= 0)
154 				continue;
155 			
156 			if ("content-length".equals(lhdr)) {
157 				try {
158 					contentLength=hreq.getIntHeader(hdr);
159 					hasContent=contentLength>0;
160 				} catch (NumberFormatException e) {
161 					if (_log.isWarnEnabled()) _log.warn("bad Content-Length header value: "+hreq.getHeader(hdr), e);
162 				}
163 			}
164 			
165 			if ("content-type".equals(lhdr)) {
166 				hasContent=true;
167 			}
168 			
169 			Enumeration vals = hreq.getHeaders(hdr);
170 			while (vals.hasMoreElements()) {
171 				String val = (String) vals.nextElement();
172 				if (val != null) {
173 					hm.addRequestHeader(hdr, val);
174 					// if (_log.isInfoEnabled()) _log.info("Request " + hdr + ": " + val);
175 					xForwardedFor |= "X-Forwarded-For".equalsIgnoreCase(hdr); // why is this not in the outer loop ?
176 				}
177 			}
178 		}
179 		
180 		// cookies...
181 		
182 		// although we copy cookie headers into the request abover - commons-httpclient thinks it knows better and strips them out before sending.
183 		// we have to explicitly use their interface to add the cookies - painful...
184 		
185 		// DOH! - an org.apache.commons.httpclient.Cookie is NOT a
186 		// javax.servlet.http.Cookie - and it looks like the two don't
187 		// map onto each other without data loss...
188 		HttpState state=new HttpState();
189 		javax.servlet.http.Cookie[] cookies=hreq.getCookies();
190 		if (cookies!=null)
191 		{
192 			for (int i=0;i<cookies.length;i++)
193 			{
194 				javax.servlet.http.Cookie c=cookies[i];
195 				String domain=c.getDomain();
196 				if (domain==null) {
197 					domain=hreq.getServerName(); // TODO - tmp test
198 					// _log.warn("defaulting cookie domain");
199 				}
200 				//	  domain=null;
201 				String cpath=c.getPath();
202 				if (cpath==null) {
203 					cpath=hreq.getContextPath(); // fix for Jetty
204 					// _log.warn("defaulting cookie path");
205 				}
206 				//if (_log.isTraceEnabled()) _log.trace("PATH: value="+path+" length="+(path==null?0:path.length()));
207 				Cookie cookie=new Cookie(domain, c.getName(), c.getValue(), cpath, c.getMaxAge(), c.getSecure()); // TODO - sort out domain
208 				//if (_log.isTraceEnabled()) _log.trace("Cookie: "+cookie.getDomain()+","+ cookie.getName()+","+ cookie.getValue()+","+ cookie.getPath()+","+ cookie.getExpiryDate()+","+ cookie.getSecure());
209 				state.addCookie(cookie);
210 				//if (_log.isTraceEnabled()) _log.trace("Cookie: "+cookie.toString());
211 			}
212 		}
213 		
214 		// Proxy headers
215 		hm.addRequestHeader("Via", "1.1 "+hreq.getLocalName()+":"+hreq.getLocalPort()+" \"WADI\"");
216 		if (!xForwardedFor)
217 			hm.addRequestHeader("X-Forwarded-For", hreq.getRemoteAddr());
218 		// Max-Forwards...
219 		
220 		// a little bit of cache control
221 //		String cache_control = hreq.getHeader("Cache-Control");
222 //		if (cache_control != null && (cache_control.indexOf("no-cache") >= 0 || cache_control.indexOf("no-store") >= 0))
223 //		httpMethod.setUseCaches(false);
224 		
225 		// customize Connection
226 //		uc.setDoInput(true);
227 		
228 		int client2ServerTotal=0;
229 		if (hasContent) {
230 //			uc.setDoOutput(true);
231 			
232 			try {
233 				if (hm instanceof EntityEnclosingMethod)
234 					((EntityEnclosingMethod)hm).setRequestBody(hreq.getInputStream());
235 				// TODO - do we need to close response stream at end... ?
236 			} catch (IOException e) {
237 				throw new IrrecoverableException("could not pss request input across proxy", e);
238 			}
239 		}
240 		
241 		try
242 		{
243 			HttpClient client=new HttpClient();
244 			HostConfiguration hc=new HostConfiguration();
245 			//String host=location.getAddress().getHostAddress();
246 			// inefficient - but stops httpclient from rejecting half our cookies...
247 			String host= uri.getHost();
248 			hc.setHost(host, uri.getPort());
249 			client.executeMethod(hc, hm, state);
250 		}
251 		catch (IOException e)	// TODO
252 		{
253 			_log.warn("problem proxying connection:", e);
254 		}
255 		
256 		InputStream fromServer = null;
257 		
258 		// handler status codes etc.
259 		int code=502;
260 //		String message="Bad Gateway: could not read server response code or message";
261 		
262 		code=hm.getStatusCode(); // IOException
263 //		message=hm.getStatusText(); // IOException
264 		hres.setStatus(code);
265 //		hres.setStatus(code, message); - deprecated...
266 		
267 		try {
268 			fromServer=hm.getResponseBodyAsStream(); // IOException
269 		} catch (IOException e) {
270 			_log.warn("problem acquiring http client output", e);
271 		}
272 		
273 		
274 		// clear response defaults.
275 		hres.setHeader("Date", null);
276 		hres.setHeader("Server", null);
277 		
278 		// set response headers
279 		// TODO - is it a bug in Jetty that I have to start my loop at 1 ? or that key[0]==null ?
280 		// Try this inside Tomcat...
281 		Header[] headers=hm.getResponseHeaders();
282 		for (int i=0; i<headers.length; i++) {
283 			String h=headers[i].toExternalForm();
284 			int index=h.indexOf(':');
285 			String key=h.substring(0, index).trim().toLowerCase();
286 			String val=h.substring(index+1, h.length()).trim();
287 			if (val!=null && !_DontProxyHeaders.contains(key)) {
288 				hres.addHeader(key, val);
289 				// if (_log.isInfoEnabled()) _log.info("Response: "+key+" - "+val);
290 			}
291 		}
292 		
293 		hres.addHeader("Via", "1.1 (WADI)");
294 		
295 		// copy server->client
296 		int server2ClientTotal=0;
297 		if (fromServer!=null) {
298 			try {
299 				OutputStream toClient=hres.getOutputStream();// IOException
300 				server2ClientTotal+=copy(fromServer, toClient, 8192);// IOException
301 			} catch (IOException e) {
302 				_log.warn("problem proxying server response back to client", e);
303 			} finally {
304 				try {
305 					fromServer.close();
306 				} catch (IOException e) {
307 					// well - we did our best...
308 					_log.warn("problem closing server response stream", e);
309 				}
310 			}
311 		}
312 		
313 		long endTime=System.currentTimeMillis();
314 		long elapsed=endTime-startTime;
315 		if (_log.isDebugEnabled()) {
316             _log.debug("in:" + client2ServerTotal + ", out:" + server2ClientTotal + ", status:" + code + ", time:"
317                     + elapsed + ", uri:" + uri);
318         }
319 	}
320 }