View Javadoc

1   /*
2    * @(#)RedirectionModule.java				0.3-3E 06/05/2001
3    *
4    *  This file is part of the HTTPClient package
5    *  Copyright (C) 1996-2001 Ronald Tschalär
6    *
7    *  This library is free software; you can redistribute it and/or
8    *  modify it under the terms of the GNU Lesser General Public
9    *  License as published by the Free Software Foundation; either
10   *  version 2 of the License, or (at your option) any later version.
11   *
12   *  This library is distributed in the hope that it will be useful,
13   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   *  Lesser General Public License for more details.
16   *
17   *  You should have received a copy of the GNU Lesser General Public
18   *  License along with this library; if not, write to the Free
19   *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
20   *  MA 02111-1307, USA
21   *
22   *  For questions, suggestions, bug-reports, enhancement-requests etc.
23   *  I may be contacted at:
24   *
25   *  ronald@innovation.ch
26   *
27   *  The HTTPClient's home page is located at:
28   *
29   *  http://www.innovation.ch/java/HTTPClient/
30   *
31   * This file contains modifications for use with "The Grinder"
32   * (http://grinder.sourceforge.net) under the terms of the LGPL. They
33   * are marked below with the comment "GRINDER MODIFICATION".
34   *
35   */
36  
37  package HTTPClient;
38  
39  import java.net.ProtocolException;
40  import java.io.IOException;
41  import java.util.Hashtable;
42  
43  
44  /**
45   * This module handles the redirection status codes 301, 302, 303, 305, 306
46   * and 307.
47   *
48   * @version	0.3-3E  06/05/2001
49   * @author	Ronald Tschalär
50   */
51  class RedirectionModule implements HTTPClientModule
52  {
53      /** a list of permanent redirections (301) */
54      private static Hashtable perm_redir_cntxt_list = new Hashtable();
55  
56      /** a list of deferred redirections (used with Response.retryRequest()) */
57      private static Hashtable deferred_redir_list = new Hashtable();
58  
59      /** the level of redirection */
60      private int level;
61  
62      /** the url used in the last redirection */
63      private URI lastURI;
64  
65      /** used for deferred redirection retries */
66      private boolean new_con;
67  
68      /** used for deferred redirection retries */
69      private Request saved_req;
70  
71  
72      // Constructors
73  
74      /**
75       * Start with level 0.
76       */
77      RedirectionModule()
78      {
79  	level     = 0;
80  	lastURI   = null;
81  	saved_req = null;
82      }
83  
84  
85      // Methods
86  
87      /**
88       * Invoked by the HTTPClient.
89       */
90      public int requestHandler(Request req, Response[] resp)
91      {
92  	HTTPConnection con = req.getConnection();
93  	URI new_loc,
94  	    cur_loc;
95  
96  
97  	// check for retries
98  
99  	HttpOutputStream out = req.getStream();
100 	if (out != null  &&  deferred_redir_list.get(out) != null)
101 	{
102 	    copyFrom((RedirectionModule) deferred_redir_list.remove(out));
103 	    req.copyFrom(saved_req);
104 
105 	    if (new_con)
106 		return REQ_NEWCON_RST;
107 	    else
108 		return REQ_RESTART;
109 	}
110 
111 
112 	// handle permanent redirections
113 
114 	try
115 	{
116 	    cur_loc = new URI(new URI(con.getProtocol(), con.getHost(), con.getPort(), null),
117 			      req.getRequestURI());
118 	}
119 	catch (ParseException pe)
120 	{
121 	    throw new Error("HTTPClient Internal Error: unexpected exception '"
122 			    + pe + "'");
123 	}
124 
125 
126 	// handle permanent redirections
127 
128 	Hashtable perm_redir_list = Util.getList(perm_redir_cntxt_list,
129 					    req.getConnection().getContext());
130 	if ((new_loc = (URI) perm_redir_list.get(cur_loc)) != null)
131 	{
132 	    /* copy query if present in old url but not in new url. This
133 	     * isn't strictly conforming, but some scripts fail to properly
134 	     * propagate the query string to the Location header.
135 	     *
136 	     * Unfortunately it looks like we're fucked either way: some
137 	     * scripts fail if you don't propagate the query string, some
138 	     * fail if you do... God, don't you just love it when people
139 	     * can't read a spec? Anway, since we can't get it right for
140 	     * all scripts we opt to follow the spec.
141 	    String nres    = new_loc.getPathAndQuery(),
142 		   oquery  = Util.getQuery(req.getRequestURI()),
143 		   nquery  = Util.getQuery(nres);
144 	    if (nquery == null  &&  oquery != null)
145 		nres += "?" + oquery;
146 	     */
147 	    String nres = new_loc.getPathAndQuery();
148 	    req.setRequestURI(nres);
149 
150 	    try
151 		{ lastURI = new URI(new_loc, nres); }
152 	    catch (ParseException pe)
153 		{ }
154 
155 	    Log.write(Log.MODS, "RdirM: matched request in permanent " +
156 				"redirection list - redoing request to " +
157 				lastURI.toExternalForm());
158 
159 	    if (!con.isCompatibleWith(new_loc))
160 	    {
161 		try
162 		    { con = new HTTPConnection(new_loc); }
163 		catch (Exception e)
164 		{
165 		    throw new Error("HTTPClient Internal Error: unexpected " +
166 				    "exception '" + e + "'");
167 		}
168 
169 		con.setSSLSocketFactory(req.getConnection().getSSLSocketFactory());
170 		con.setContext(req.getConnection().getContext());
171 		req.setConnection(con);
172 		return REQ_NEWCON_RST;
173 	    }
174 	    else
175 	    {
176 		return REQ_RESTART;
177 	    }
178 	}
179 
180 	return REQ_CONTINUE;
181     }
182 
183 
184     /**
185      * Invoked by the HTTPClient.
186      */
187     public void responsePhase1Handler(Response resp, RoRequest req)
188 	    throws IOException
189     {
190 	int sts  = resp.getStatusCode();
191 	if (sts < 301  ||  sts > 307  ||  sts == 304)
192 	{
193 	    if (lastURI != null)		// it's been redirected
194 		resp.setEffectiveURI(lastURI);
195 	}
196     }
197 
198 
199     /**
200      * Invoked by the HTTPClient.
201      */
202     public int responsePhase2Handler(Response resp, Request req)
203 	    throws IOException
204     {
205 	/* handle various response status codes until satisfied */
206 
207 	int sts  = resp.getStatusCode();
208 	switch(sts)
209 	{
210 	    case 302: // General (temporary) Redirection (handle like 303)
211 
212 		/* Note we only do this munging for POST and PUT. For GET it's
213 		 * not necessary; for HEAD we probably want to do another HEAD.
214 		 * For all others (i.e. methods from WebDAV, IPP, etc) it's
215 		 * somewhat unclear - servers supporting those should really
216 		 * return a 307 or 303, but some don't (guess who...), so we
217 		 * just don't touch those.
218 		 */
219 		if (req.getMethod().equals("POST")  ||
220 		    req.getMethod().equals("PUT"))
221 		{
222 		    Log.write(Log.MODS, "RdirM: Received status: " + sts +
223 					" " + resp.getReasonLine() +
224 					" - treating as 303");
225 
226 		    sts = 303;
227 		}
228 
229 	    case 301: // Moved Permanently
230 	    case 303: // See Other (use GET)
231 	    case 307: // Moved Temporarily (we mean it!)
232 
233 		Log.write(Log.MODS, "RdirM: Handling status: " + sts +
234 				    " " + resp.getReasonLine());
235 
236 		// the spec says automatic redirection may only be done if
237 		// the second request is a HEAD or GET.
238 		if (!req.getMethod().equals("GET")  &&
239 		    !req.getMethod().equals("HEAD")  &&
240 		    sts != 303)
241 		{
242 		    Log.write(Log.MODS, "RdirM: not redirected because " +
243 					"method is neither HEAD nor GET");
244 
245 		    if (sts == 301  &&  resp.getHeader("Location") != null)
246 			update_perm_redir_list(req,
247 				    resLocHdr(resp.getHeader("Location"), req));
248 
249 		    resp.setEffectiveURI(lastURI);
250 		    return RSP_CONTINUE;
251 		}
252 
253 	    case 305: // Use Proxy
254 	    case 306: // Switch Proxy
255 
256 		if (sts == 305  ||  sts == 306)
257 		    Log.write(Log.MODS, "RdirM: Handling status: " + sts +
258 				        " " + resp.getReasonLine());
259 
260 		// Don't accept 305 from a proxy
261 		if (sts == 305  &&  req.getConnection().getProxyHost() != null)
262 		{
263 		    Log.write(Log.MODS, "RdirM: 305 ignored because " +
264 					"a proxy is already in use");
265 
266 		    resp.setEffectiveURI(lastURI);
267 		    return RSP_CONTINUE;
268 		}
269 
270 
271 		/* the level is a primitive way of preventing infinite
272 		 * redirections. RFC-2068 set the max to 5, but RFC-2616
273 		 * has loosened this. Since some sites (notably M$) need
274 		 * more levels, this is now set to the (arbitrary) value
275 		 * of 15 (god only knows why they need to do even 5
276 		 * redirections...).
277 		 */
278 		if (level >= 15  ||  resp.getHeader("Location") == null)
279 		{
280 		    if (level >= 15)
281 			Log.write(Log.MODS, "RdirM: not redirected because "+
282 					    "of too many levels of redirection");
283 		    else
284 			Log.write(Log.MODS, "RdirM: not redirected because "+
285 					    "no Location header was present");
286 
287 		    resp.setEffectiveURI(lastURI);
288 		    return RSP_CONTINUE;
289 		}
290 		level++;
291 
292 		URI loc = resLocHdr(resp.getHeader("Location"), req);
293 
294 		HTTPConnection mvd;
295 		String nres;
296 		new_con = false;
297 
298 		if (sts == 305)
299 		{
300 		    mvd = new HTTPConnection(req.getConnection().getProtocol(),
301 					     req.getConnection().getHost(),
302 					     req.getConnection().getPort());
303 		    mvd.setCurrentProxy(loc.getHost(), loc.getPort());
304 		    mvd.setSSLSocketFactory(req.getConnection().getSSLSocketFactory());
305 		    mvd.setContext(req.getConnection().getContext());
306 		    new_con = true;
307 
308 		    nres = req.getRequestURI();
309 
310 		    /* There was some discussion about this, and especially
311 		     * Foteos Macrides (Lynx) said a 305 should also imply
312 		     * a change to GET (for security reasons) - see the thread
313 		     * starting at
314 		     * http://www.ics.uci.edu/pub/ietf/http/hypermail/1997q4/0351.html
315 		     * However, this is not in the latest draft, but since I
316 		     * agree with Foteos we do it anyway...
317 		     */
318 		    req.setMethod("GET");
319 		    req.setData(null);
320 		    req.setStream(null);
321 		}
322 		else if (sts == 306)
323 		{
324 		    // We'll have to wait for Josh to create a new spec here.
325 		    return RSP_CONTINUE;
326 		}
327 		else
328 		{
329 		    if (req.getConnection().isCompatibleWith(loc))
330 		    {
331 			mvd  = req.getConnection();
332 			nres = loc.getPathAndQuery();
333 		    }
334 		    else
335 		    {
336 			try
337 			{
338 			    mvd  = new HTTPConnection(loc);
339 			    nres = loc.getPathAndQuery();
340 			}
341 			catch (Exception e)
342 			{
343 			    if (req.getConnection().getProxyHost() == null  ||
344 				!loc.getScheme().equalsIgnoreCase("ftp"))
345 				return RSP_CONTINUE;
346 
347 			    // We're using a proxy and the protocol is ftp -
348 			    // maybe the proxy will also proxy ftp...
349 			    mvd  = new HTTPConnection("http",
350 					    req.getConnection().getProxyHost(),
351 					    req.getConnection().getProxyPort());
352 			    mvd.setCurrentProxy(null, 0);
353 			    nres = loc.toExternalForm();
354 			}
355 
356 			mvd.setSSLSocketFactory(req.getConnection().getSSLSocketFactory());
357 			mvd.setContext(req.getConnection().getContext());
358 
359             /* GRINDER MODIFICATION++ */
360             mvd.setCheckCertificates(
361               req.getConnection().getCheckCertificates());
362             mvd.setTestConnectionHealthWithBlockingRead(
363               req.getConnection().getTestConnectionHealthWithBlockingRead());
364 		    mvd.setTimeAuthority(req.getConnection().getTimeAuthority());
365             /* --GRINDER MODIFICATION */
366 
367 			new_con = true;
368 		    }
369 
370 		    /* copy query if present in old url but not in new url.
371 		     * This isn't strictly conforming, but some scripts fail
372 		     * to propagate the query properly to the Location
373 		     * header.
374 		     *
375 		     * See comment on line 126.
376 		    String oquery  = Util.getQuery(req.getRequestURI()),
377 			   nquery  = Util.getQuery(nres);
378 		    if (nquery == null  &&  oquery != null)
379 			nres += "?" + oquery;
380 		     */
381 
382 		    if (sts == 303)
383 		    {
384 			// 303 means "use GET"
385 
386 			if (!req.getMethod().equals("HEAD"))
387 			    req.setMethod("GET");
388 			req.setData(null);
389 			req.setStream(null);
390 		    }
391 		    else
392 		    {
393 			// If they used an output stream then they'll have
394 			// to do the resend themselves
395 			if (req.getStream() != null)
396 			{
397 			    if (!HTTPConnection.deferStreamed)
398 			    {
399 				Log.write(Log.MODS, "RdirM: status " + sts +
400 						    " not handled - request " +
401 						    "has an output stream");
402 				return RSP_CONTINUE;
403 			    }
404 
405 			    saved_req = (Request) req.clone();
406 			    deferred_redir_list.put(req.getStream(), this);
407 			    req.getStream().reset();
408 			    resp.setRetryRequest(true);
409 			}
410 
411 			if (sts == 301)
412 			{
413 			    // update permanent redirection list
414 			    try
415 			    {
416 				update_perm_redir_list(req, new URI(loc, nres));
417 			    }
418 			    catch (ParseException pe)
419 			    {
420 				throw new Error("HTTPClient Internal Error: " +
421 						"unexpected exception '" + pe +
422 						"'");
423 			    }
424 			}
425 		    }
426 
427 		    // Adjust Referer, if present
428 		    NVPair[] hdrs = req.getHeaders();
429 		    for (int idx=0; idx<hdrs.length; idx++)
430 			if (hdrs[idx].getName().equalsIgnoreCase("Referer"))
431 			{
432 			    HTTPConnection con = req.getConnection();
433 			    hdrs[idx] =
434 				new NVPair("Referer", con+req.getRequestURI());
435 			    break;
436 			}
437 		}
438 
439 		req.setConnection(mvd);
440 		req.setRequestURI(nres);
441 
442 		try { resp.getInputStream().close(); }
443 		catch (IOException ioe) { }
444 
445 		if (sts != 305  &&  sts != 306)
446 		{
447 		    try
448 			{ lastURI = new URI(loc, nres); }
449 		    catch (ParseException pe)
450 			{ /* ??? */ }
451 
452 		    Log.write(Log.MODS, "RdirM: request redirected to " +
453 					lastURI.toExternalForm() +
454 					" using method " + req.getMethod());
455 		}
456 		else
457 		{
458 		    Log.write(Log.MODS, "RdirM: resending request using " +
459 					"proxy " + mvd.getProxyHost() +
460 					":" + mvd.getProxyPort());
461 		}
462 
463 		if (req.getStream() != null)
464 		    return RSP_CONTINUE;
465 		else if (new_con)
466 		    return RSP_NEWCON_REQ;
467 		else
468 		    return RSP_REQUEST;
469 
470 	    default:
471 
472 		return RSP_CONTINUE;
473 	}
474     }
475 
476 
477     /**
478      * Invoked by the HTTPClient.
479      */
480     public void responsePhase3Handler(Response resp, RoRequest req)
481     {
482     }
483 
484 
485     /**
486      * Invoked by the HTTPClient.
487      */
488     public void trailerHandler(Response resp, RoRequest req)
489     {
490     }
491 
492 
493     /**
494      * Update the permanent redirection list.
495      *
496      * @param the original request
497      * @param the new location
498      */
499     private static void update_perm_redir_list(RoRequest req, URI new_loc)
500     {
501 	HTTPConnection con = req.getConnection();
502 	URI cur_loc = null;
503 	try
504 	{
505 	    cur_loc = new URI(new URI(con.getProtocol(), con.getHost(), con.getPort(), null),
506 			      req.getRequestURI());
507 	}
508 	catch (ParseException pe)
509 	    { }
510 
511 	if (!cur_loc.equals(new_loc))
512 	{
513 	    Hashtable perm_redir_list =
514 			Util.getList(perm_redir_cntxt_list, con.getContext());
515 	    perm_redir_list.put(cur_loc, new_loc);
516 	}
517     }
518 
519 
520     /**
521      * The Location header field must be an absolute URI, but too many broken
522      * servers use relative URIs. So, we always resolve relative to the
523      * full request URI.
524      *
525      * @param  loc the Location header field
526      * @param  req the Request to resolve relative URI's relative to
527      * @return an absolute URI corresponding to the Location header field
528      * @exception ProtocolException if the Location header field is completely
529      *                            unparseable
530      */
531     private URI resLocHdr(String loc, RoRequest req)  throws ProtocolException
532     {
533 	try
534 	{
535 	    URI base = new URI(req.getConnection().getProtocol(),
536 			       req.getConnection().getHost(),
537 			       req.getConnection().getPort(), null);
538 	    base = new URI(base, req.getRequestURI());
539 	    URI res = new URI(base, loc);
540 	    if (res.getHost() == null)
541 		throw new ProtocolException("Malformed URL in Location header: `" + loc +
542 					    "' - missing host field");
543 	    return res;
544 	}
545 	catch (ParseException pe)
546 	{
547 	    throw new ProtocolException("Malformed URL in Location header: `" + loc +
548 					"' - exception was: " + pe.getMessage());
549 	}
550     }
551 
552 
553     private void copyFrom(RedirectionModule other)
554     {
555 	this.level     = other.level;
556 	this.lastURI   = other.lastURI;
557 	this.saved_req = other.saved_req;
558     }
559 }