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  //this started life as org.mortbay.servlet.ProxyServlet. I copied the
18  //whole thing to use as a starting point - Thanks Greg !
19  
20  // ========================================================================
21  // $Id: StandardHttpProxy.java 1737 2006-04-27 15:13:16 +0100 (Thu, 27 Apr 2006) jules $
22  // Copyright 2004-2004 Mort Bay Consulting Pty. Ltd.
23  // ------------------------------------------------------------------------
24  // Licensed under the Apache License, Version 2.0 (the "License");
25  // you may not use this file except in compliance with the License.
26  // You may obtain a copy of the License at
27  // http://www.apache.org/licenses/LICENSE-2.0
28  // Unless required by applicable law or agreed to in writing, software
29  // distributed under the License is distributed on an "AS IS" BASIS,
30  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31  // See the License for the specific language governing permissions and
32  // limitations under the License.
33  // ========================================================================
34  
35  package org.codehaus.wadi.web.impl;
36  
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.net.HttpURLConnection;
41  import java.net.MalformedURLException;
42  import java.net.ProtocolException;
43  import java.net.URI;
44  import java.net.URL;
45  import java.util.Enumeration;
46  
47  import javax.servlet.http.HttpServletRequest;
48  import javax.servlet.http.HttpServletResponse;
49  
50  import org.apache.commons.logging.Log;
51  import org.apache.commons.logging.LogFactory;
52  
53  // My choice of proxy - still suboptimal - servlet spec imposes a very clumsy API
54  // for copying the headers out of the HttpServletRequest (a proprietary solution
55  // would be faster), but at least the Cookie headers can be copied straight across
56  // (see CommonsHttpProxy where they cannot...).
57  
58  // This does not yet support e.g. WebDav methods like PROPFIND etc...
59  
60  /***
61   * HttpProxy implementation based on java.net.HttpURLConnection
62   *
63   * @author <a href="mailto:jules@coredevelopers.net">Jules Gosnell</a>
64   * @author <a href="mailto:gregw@mortbay.com">Greg Wilkins</a>
65   * @version $Revision: 1737 $
66   */
67  public class StandardHttpProxy extends AbstractHttpProxy {
68  	
69  	protected static final Log _log=LogFactory.getLog(StandardHttpProxy.class);
70  	
71  	public StandardHttpProxy(String sessionPathParamKey) {
72  		super(sessionPathParamKey);
73  	}
74  	
75      protected void doProxy(URI uri, WebInvocation context) throws ProxyingException {
76  		HttpServletRequest req = context.getHreq();
77  		HttpServletResponse res = context.getHres();
78  		
79  		String requestURI=getRequestURI(req);
80  		String qs=req.getQueryString();
81  		if (qs!=null) {
82              requestURI=new StringBuffer(requestURI).append("?").append(qs).toString();
83  		}
84  		
85  		URL url=null;
86  		try {
87  			url=new URL("http", uri.getHost(), uri.getPort(), requestURI);
88  			if (_log.isTraceEnabled()) _log.trace("proxying to: "+url);
89  		} catch (MalformedURLException e) {
90  			if (_log.isWarnEnabled()) _log.warn("bad proxy url: "+url, e);
91  			throw new IrrecoverableException("bad proxy url", e);
92  		}
93  		
94  		long startTime=System.currentTimeMillis();
95  		
96  		HttpURLConnection huc=null;
97  		String m=req.getMethod();
98  		try {
99  			huc=(HttpURLConnection)url.openConnection(); // IOException
100 			huc.setRequestMethod(m); // ProtocolException
101 		} catch (ProtocolException e) {
102 			if (_log.isWarnEnabled()) _log.warn("unsupported http method: "+m, e);
103 			throw new IrrecoverableException("unsupported HTTP method: "+m, e);
104 		} catch (IOException e) {
105 			if (_log.isWarnEnabled()) _log.warn("proxy IO problem", e);
106 			throw new RecoverableException("could not open proxy connection", e);
107 		}
108 		
109 		huc.setAllowUserInteraction(false);
110 		huc.setInstanceFollowRedirects(false);
111 		
112 		// check connection header
113 		// TODO - this might need some more time: see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
114 		String connectionHdr = req.getHeader("Connection"); // TODO - what if there are multiple values ?
115 		if (connectionHdr != null) {
116 			connectionHdr = connectionHdr.toLowerCase();
117 			if (connectionHdr.equals("keep-alive")|| connectionHdr.equals("close"))
118 				connectionHdr = null; // TODO  ??
119 		}
120 		
121 		// copy headers - inefficient, but we are constrained by servlet API
122 		{
123 			for (Enumeration e=req.getHeaderNames(); e.hasMoreElements();) {
124 				String hdr = (String) e.nextElement();
125 				String lhdr = hdr.toLowerCase();
126 				
127 				if (_DontProxyHeaders.contains(lhdr))
128 					continue;
129 				if (connectionHdr != null && connectionHdr.indexOf(lhdr) >= 0) // what is going on here ?
130 					continue;
131 				// HTTP/1.1 proxies MUST parse the Connection header field before a message is forwarded and, for each connection-token in this field, remove any header field(s) from the message with the same name as the connection-token. Connection options are signaled by the presence of a connection-token in the Connection header field, not by any corresponding additional header field(s), since the additional header field may not be sent if there are no parameters associated with that connection option
132 				if (_WADI_IsSecure.equals(hdr)) // don't worry about case - we should be the only one messing with this header...
133 					continue; // strip this out - we may be being spoofed
134 				
135 				for (Enumeration f=req.getHeaders(hdr); f.hasMoreElements();) {
136 					String val=(String)f.nextElement();
137 					if (val!=null) {
138 						huc.addRequestProperty(hdr, val);
139 					}
140 				}
141 			}
142 		}
143 		
144 		// content ?
145 		boolean hasContent=false;
146 		{
147 			int contentLength=0;
148 			String tmp=huc.getRequestProperty("Content-Length");
149 			if (tmp!=null) {
150 				try {
151 					contentLength=Integer.parseInt(tmp);
152 				} catch (NumberFormatException ignore) {
153 					// ignore
154 				}
155 			}
156 			
157 			if (contentLength>0)
158 				hasContent=true;
159 			else
160 				hasContent=(huc.getRequestProperty("Content-Type")!=null);
161 		}
162 		
163 		// proxy
164 		{
165 			huc.addRequestProperty("Via", "1.1 "+req.getLocalName()+":"+req.getLocalPort()+" \"WADI\""); // TODO - should we be giving out personal details ?
166 			huc.addRequestProperty("X-Forwarded-For", req.getRemoteAddr()); // adds last link in request chain...
167 			// String tmp=uc.getRequestProperty("Max-Forwards"); // TODO - do we really need to bother with this ?
168 		}
169 		
170 		// cache-control
171 		{
172 			String cacheControl=huc.getRequestProperty("Cache-Control");
173 			if (cacheControl!=null && (cacheControl.indexOf("no-cache")>=0 || cacheControl.indexOf("no-store")>=0))
174 				huc.setUseCaches(false);
175 		}
176 		
177 		// confidentiality
178 		{
179 			if (req.isSecure()) {
180 				huc.addRequestProperty(_WADI_IsSecure, req.getLocalAddr().toString());
181 			}
182 			
183 			// at the other end, if this header is present we must :
184 			
185 			// wrap the request so that req.isSecure()=true, before processing...
186 			// mask the header - so it is never seen by the app.
187 			
188 			// the code for the other end should live in this class.
189 			
190 			// this code should also confirm that it not being spoofed by confirming that req.getRemoteAddress() is a cluster member...
191 		}
192 		// customize Connection
193 		huc.setDoInput(true);
194 		
195 		// client->server
196 		int client2ServerTotal=0;
197 		{
198 			if (hasContent) {
199 				huc.setDoOutput(true);
200 				
201 				OutputStream toServer=null;
202 				try {
203 					InputStream fromClient=req.getInputStream(); // IOException
204 					toServer=huc.getOutputStream(); // IOException
205 					client2ServerTotal=copy(fromClient, toServer, 8192);
206 				} catch (IOException e) {
207 					new IrrecoverableException("problem proxying client request to server", e);
208 				} finally {
209 					if (toServer!=null) {
210 						try {
211 							toServer.close(); // IOException
212 						} catch (IOException e) {
213 							_log.warn("problem closing server request stream", e);
214 						}
215 					}
216 				}
217 			}
218 		}
219 		
220 		// Connect
221 		try {
222 			huc.connect(); // IOException
223 		} catch (IOException e) {
224 			if (_log.isWarnEnabled()) _log.warn("proxy connection problem: "+url, e);
225 			throw new RecoverableException("could not connect to proxy target", e);
226 		}
227 		
228 		InputStream fromServer=null;
229 		
230 		// handler status codes etc.
231 		int code=0;
232 		if (huc==null) {
233 			try {
234 				fromServer = huc.getInputStream(); // IOException
235 			} catch (IOException e) {
236 				if (_log.isWarnEnabled()) _log.warn("proxying problem", e);
237 				throw new IrrecoverableException("problem acquiring client output", e);
238 			}
239 		} else {
240 			code=502;
241 			//			String message="Bad Gateway: could not read server response code or message";
242 			try {
243 				code=huc.getResponseCode(); // IOException
244 				//				message=huc.getResponseMessage(); // IOException
245 			} catch (IOException e) {
246 				if (_log.isWarnEnabled()) _log.warn("proxying problem", e);
247 				throw new IrrecoverableException("problem acquiring http server response code/message", e);
248 			} finally {
249 				//				res.setStatus(code, message); - deprecated
250 				res.setStatus(code);
251 			}
252 			
253 			if (code<400) {
254 				// 1XX:continue, 2XX:successful, 3XX:multiple-choices...
255 				try {
256 					fromServer=huc.getInputStream(); // IOException
257 				} catch (IOException e) {
258 					if (_log.isWarnEnabled()) _log.warn("proxying problem", e);
259 					throw new IrrecoverableException("problem acquiring http client output", e);
260 				}
261 			} else {
262 				// 4XX:client, 5XX:server error...
263 				fromServer = huc.getErrorStream(); // why does this not throw IOException ?
264 				// TODO - do we need to use sendError()?
265 			}
266 		}
267 		
268 		// clear response defaults.
269 		res.setHeader("Date", null);
270 		res.setHeader("Server", null);
271 		
272 		// set response headers
273 		if (false) {
274 			int h = 0;
275 			String hdr = huc.getHeaderFieldKey(h);
276 			String val = huc.getHeaderField(h);
277 			while (hdr != null || val != null) {
278 				String lhdr = (hdr != null) ? hdr.toLowerCase() : null;
279 				if (hdr != null && val != null && !_DontProxyHeaders.contains(lhdr))
280 					res.addHeader(hdr, val);
281 				
282 				// if (_log.isDebugEnabled()) _log.debug("res " + hdr + ": " + val);
283 				
284 				h++;
285 				hdr = huc.getHeaderFieldKey(h);
286 				val = huc.getHeaderField(h);
287 			}
288 		} else {
289 			// TODO - is it a bug in Jetty that I have to start my loop at 1 ? or that key[0]==null ?
290 			// Try this inside Tomcat...
291 			String key;
292 			for (int i=1; (key=huc.getHeaderFieldKey(i))!=null; i++) {
293 				key=key.toLowerCase();
294 				String val=huc.getHeaderField(i);
295 				if (val!=null && !_DontProxyHeaders.contains(key)) {
296 					res.addHeader(key, val);
297 				}
298 			}
299 		}
300 		
301 		// do we need another Via header in the response...
302 		
303 		// server->client
304 		int server2ClientTotal=0;
305 		{
306 			if (fromServer!=null) {
307 				try {
308 					OutputStream toClient=res.getOutputStream();// IOException
309 					server2ClientTotal+=copy(fromServer, toClient, 8192);// IOException
310 				} catch (IOException e) {
311 					if (_log.isWarnEnabled()) _log.warn("proxying problem", e);
312 					throw new IrrecoverableException("problem proxying server response back to client", e);
313 				} finally {
314 					try {
315 						fromServer.close();
316 					} catch (IOException e) {
317 						// well - we did our best...
318 						_log.warn("problem closing server response stream", e);
319 					}
320 				}
321 			}
322 		}
323 		
324 		huc.disconnect();
325 		
326 		long endTime=System.currentTimeMillis();
327 		long elapsed=endTime-startTime;
328 		if (_log.isDebugEnabled()) _log.debug("in:"+client2ServerTotal+", out:"+server2ClientTotal+", status:"+code+", time:"+elapsed+", url:"+url);
329 	}
330 	
331 }