1:
37:
38:
39: package ;
40:
41: import ;
42: import ;
43:
44: import ;
45: import ;
46: import ;
47: import ;
48: import ;
49: import ;
50: import ;
51: import ;
52: import ;
53: import ;
54: import ;
55: import ;
56: import ;
57: import ;
58: import ;
59: import ;
60:
61:
66: public class Request
67: {
68:
69:
72: protected final HTTPConnection connection;
73:
74:
77: protected final String method;
78:
79:
84: protected final String path;
85:
86:
89: protected final Headers requestHeaders;
90:
91:
94: protected RequestBodyWriter requestBodyWriter;
95:
96:
99: protected Map responseHeaderHandlers;
100:
101:
104: protected Authenticator authenticator;
105:
106:
109: private boolean dispatched;
110:
111:
117: protected Request(HTTPConnection connection, String method,
118: String path)
119: {
120: this.connection = connection;
121: this.method = method;
122: this.path = path;
123: requestHeaders = new Headers();
124: responseHeaderHandlers = new HashMap();
125: }
126:
127:
131: public HTTPConnection getConnection()
132: {
133: return connection;
134: }
135:
136:
140: public String getMethod()
141: {
142: return method;
143: }
144:
145:
149: public String getPath()
150: {
151: return path;
152: }
153:
154:
158: public String getRequestURI()
159: {
160: return connection.getURI() + path;
161: }
162:
163:
166: public Headers getHeaders()
167: {
168: return requestHeaders;
169: }
170:
171:
175: public String getHeader(String name)
176: {
177: return requestHeaders.getValue(name);
178: }
179:
180:
184: public int getIntHeader(String name)
185: {
186: return requestHeaders.getIntValue(name);
187: }
188:
189:
193: public Date getDateHeader(String name)
194: {
195: return requestHeaders.getDateValue(name);
196: }
197:
198:
203: public void setHeader(String name, String value)
204: {
205: requestHeaders.put(name, value);
206: }
207:
208:
212: public void setRequestBody(byte[] requestBody)
213: {
214: setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody));
215: }
216:
217:
221: public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter)
222: {
223: this.requestBodyWriter = requestBodyWriter;
224: }
225:
226:
231: public void setResponseHeaderHandler(String name,
232: ResponseHeaderHandler handler)
233: {
234: responseHeaderHandlers.put(name, handler);
235: }
236:
237:
242: public void setAuthenticator(Authenticator authenticator)
243: {
244: this.authenticator = authenticator;
245: }
246:
247:
254: public Response dispatch()
255: throws IOException
256: {
257: if (dispatched)
258: {
259: throw new ProtocolException("request already dispatched");
260: }
261: final String CRLF = "\r\n";
262: final String HEADER_SEP = ": ";
263: final String US_ASCII = "US-ASCII";
264: final String version = connection.getVersion();
265: Response response;
266: int contentLength = -1;
267: boolean retry = false;
268: int attempts = 0;
269: boolean expectingContinue = false;
270: if (requestBodyWriter != null)
271: {
272: contentLength = requestBodyWriter.getContentLength();
273: String expect = getHeader("Expect");
274: if (expect != null && expect.equals("100-continue"))
275: {
276: expectingContinue = true;
277: }
278: else
279: {
280: setHeader("Content-Length", Integer.toString(contentLength));
281: }
282: }
283:
284: try
285: {
286:
287: do
288: {
289: retry = false;
290:
291:
292: OutputStream out = connection.getOutputStream();
293:
294:
295: String requestUri = path;
296: if (connection.isUsingProxy() &&
297: !"*".equals(requestUri) &&
298: !"CONNECT".equals(method))
299: {
300: requestUri = getRequestURI();
301: }
302: String line = method + ' ' + requestUri + ' ' + version + CRLF;
303: out.write(line.getBytes(US_ASCII));
304:
305: for (Iterator i = requestHeaders.iterator(); i.hasNext(); )
306: {
307: Headers.HeaderElement elt = (Headers.HeaderElement)i.next();
308: line = elt.name + HEADER_SEP + elt.value + CRLF;
309: out.write(line.getBytes(US_ASCII));
310: }
311: out.write(CRLF.getBytes(US_ASCII));
312:
313: if (requestBodyWriter != null && !expectingContinue)
314: {
315: byte[] buffer = new byte[4096];
316: int len;
317: int count = 0;
318:
319: requestBodyWriter.reset();
320: do
321: {
322: len = requestBodyWriter.write(buffer);
323: if (len > 0)
324: {
325: out.write(buffer, 0, len);
326: }
327: count += len;
328: }
329: while (len > -1 && count < contentLength);
330: }
331: out.flush();
332:
333: while(true)
334: {
335: response = readResponse(connection.getInputStream());
336: int sc = response.getCode();
337: if (sc == 401 && authenticator != null)
338: {
339: if (authenticate(response, attempts++))
340: {
341: retry = true;
342: }
343: }
344: else if (sc == 100)
345: {
346: if (expectingContinue)
347: {
348: requestHeaders.remove("Expect");
349: setHeader("Content-Length",
350: Integer.toString(contentLength));
351: expectingContinue = false;
352: retry = true;
353: }
354: else
355: {
356:
357:
358:
359:
360:
361: continue;
362: }
363: }
364: break;
365: }
366: }
367: while (retry);
368: }
369: catch (IOException e)
370: {
371: connection.close();
372: throw e;
373: }
374: return response;
375: }
376:
377: Response readResponse(InputStream in)
378: throws IOException
379: {
380: String line;
381: int len;
382:
383:
384: LineInputStream lis = new LineInputStream(in);
385:
386: line = lis.readLine();
387: if (line == null)
388: {
389: throw new ProtocolException("Peer closed connection");
390: }
391: if (!line.startsWith("HTTP/"))
392: {
393: throw new ProtocolException(line);
394: }
395: len = line.length();
396: int start = 5, end = 6;
397: while (line.charAt(end) != '.')
398: {
399: end++;
400: }
401: int majorVersion = Integer.parseInt(line.substring(start, end));
402: start = end + 1;
403: end = start + 1;
404: while (line.charAt(end) != ' ')
405: {
406: end++;
407: }
408: int minorVersion = Integer.parseInt(line.substring(start, end));
409: start = end + 1;
410: end = start + 3;
411: int code = Integer.parseInt(line.substring(start, end));
412: String message = line.substring(end + 1, len - 1);
413:
414: Headers responseHeaders = new Headers();
415: responseHeaders.parse(lis);
416: notifyHeaderHandlers(responseHeaders);
417: InputStream body = null;
418:
419: switch (code)
420: {
421: case 100:
422: case 204:
423: case 205:
424: case 304:
425: break;
426: default:
427: body = createResponseBodyStream(responseHeaders, majorVersion,
428: minorVersion, in);
429: }
430:
431:
432: Response ret = new Response(majorVersion, minorVersion, code,
433: message, responseHeaders, body);
434: return ret;
435: }
436:
437: void notifyHeaderHandlers(Headers headers)
438: {
439: for (Iterator i = headers.iterator(); i.hasNext(); )
440: {
441: Headers.HeaderElement entry = (Headers.HeaderElement) i.next();
442:
443: if ("Set-Cookie".equalsIgnoreCase(entry.name))
444: handleSetCookie(entry.value);
445:
446: ResponseHeaderHandler handler =
447: (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name);
448: if (handler != null)
449: handler.setValue(entry.value);
450: }
451: }
452:
453: private InputStream createResponseBodyStream(Headers responseHeaders,
454: int majorVersion,
455: int minorVersion,
456: InputStream in)
457: throws IOException
458: {
459: long contentLength = -1;
460: Headers trailer = null;
461:
462:
463: boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
464: "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) ||
465: (connection.majorVersion == 1 && connection.minorVersion == 0) ||
466: (majorVersion == 1 && minorVersion == 0);
467:
468: String transferCoding = responseHeaders.getValue("Transfer-Encoding");
469: if ("chunked".equalsIgnoreCase(transferCoding))
470: {
471: in = new LimitedLengthInputStream(in, -1, false, connection, doClose);
472:
473: in = new ChunkedInputStream(in, responseHeaders);
474: }
475: else
476: {
477: contentLength = responseHeaders.getLongValue("Content-Length");
478:
479: if (contentLength < 0)
480: doClose = true;
481:
482: in = new LimitedLengthInputStream(in, contentLength,
483: contentLength >= 0,
484: connection, doClose);
485: }
486: String contentCoding = responseHeaders.getValue("Content-Encoding");
487: if (contentCoding != null && !"identity".equals(contentCoding))
488: {
489: if ("gzip".equals(contentCoding))
490: {
491: in = new GZIPInputStream(in);
492: }
493: else if ("deflate".equals(contentCoding))
494: {
495: in = new InflaterInputStream(in);
496: }
497: else
498: {
499: throw new ProtocolException("Unsupported Content-Encoding: " +
500: contentCoding);
501: }
502:
503:
504: responseHeaders.remove("Content-Encoding");
505: }
506: return in;
507: }
508:
509: boolean authenticate(Response response, int attempts)
510: throws IOException
511: {
512: String challenge = response.getHeader("WWW-Authenticate");
513: if (challenge == null)
514: {
515: challenge = response.getHeader("Proxy-Authenticate");
516: }
517: int si = challenge.indexOf(' ');
518: String scheme = (si == -1) ? challenge : challenge.substring(0, si);
519: if ("Basic".equalsIgnoreCase(scheme))
520: {
521: Properties params = parseAuthParams(challenge.substring(si + 1));
522: String realm = params.getProperty("realm");
523: Credentials creds = authenticator.getCredentials(realm, attempts);
524: String userPass = creds.getUsername() + ':' + creds.getPassword();
525: byte[] b_userPass = userPass.getBytes("US-ASCII");
526: byte[] b_encoded = BASE64.encode(b_userPass);
527: String authorization =
528: scheme + " " + new String(b_encoded, "US-ASCII");
529: setHeader("Authorization", authorization);
530: return true;
531: }
532: else if ("Digest".equalsIgnoreCase(scheme))
533: {
534: Properties params = parseAuthParams(challenge.substring(si + 1));
535: String realm = params.getProperty("realm");
536: String nonce = params.getProperty("nonce");
537: String qop = params.getProperty("qop");
538: String algorithm = params.getProperty("algorithm");
539: String digestUri = getRequestURI();
540: Credentials creds = authenticator.getCredentials(realm, attempts);
541: String username = creds.getUsername();
542: String password = creds.getPassword();
543: connection.incrementNonce(nonce);
544: try
545: {
546: MessageDigest md5 = MessageDigest.getInstance("MD5");
547: final byte[] COLON = { 0x3a };
548:
549:
550: md5.reset();
551: md5.update(username.getBytes("US-ASCII"));
552: md5.update(COLON);
553: md5.update(realm.getBytes("US-ASCII"));
554: md5.update(COLON);
555: md5.update(password.getBytes("US-ASCII"));
556: byte[] ha1 = md5.digest();
557: if ("md5-sess".equals(algorithm))
558: {
559: byte[] cnonce = generateNonce();
560: md5.reset();
561: md5.update(ha1);
562: md5.update(COLON);
563: md5.update(nonce.getBytes("US-ASCII"));
564: md5.update(COLON);
565: md5.update(cnonce);
566: ha1 = md5.digest();
567: }
568: String ha1Hex = toHexString(ha1);
569:
570:
571: md5.reset();
572: md5.update(method.getBytes("US-ASCII"));
573: md5.update(COLON);
574: md5.update(digestUri.getBytes("US-ASCII"));
575: if ("auth-int".equals(qop))
576: {
577: byte[] hEntity = null;
578: md5.update(COLON);
579: md5.update(hEntity);
580: }
581: byte[] ha2 = md5.digest();
582: String ha2Hex = toHexString(ha2);
583:
584:
585: md5.reset();
586: md5.update(ha1Hex.getBytes("US-ASCII"));
587: md5.update(COLON);
588: md5.update(nonce.getBytes("US-ASCII"));
589: if ("auth".equals(qop) || "auth-int".equals(qop))
590: {
591: String nc = getNonceCount(nonce);
592: byte[] cnonce = generateNonce();
593: md5.update(COLON);
594: md5.update(nc.getBytes("US-ASCII"));
595: md5.update(COLON);
596: md5.update(cnonce);
597: md5.update(COLON);
598: md5.update(qop.getBytes("US-ASCII"));
599: }
600: md5.update(COLON);
601: md5.update(ha2Hex.getBytes("US-ASCII"));
602: String digestResponse = toHexString(md5.digest());
603:
604: String authorization = scheme +
605: " username=\"" + username + "\"" +
606: " realm=\"" + realm + "\"" +
607: " nonce=\"" + nonce + "\"" +
608: " uri=\"" + digestUri + "\"" +
609: " response=\"" + digestResponse + "\"";
610: setHeader("Authorization", authorization);
611: return true;
612: }
613: catch (NoSuchAlgorithmException e)
614: {
615: return false;
616: }
617: }
618:
619: return false;
620: }
621:
622: Properties parseAuthParams(String text)
623: {
624: int len = text.length();
625: String key = null;
626: StringBuilder buf = new StringBuilder();
627: Properties ret = new Properties();
628: boolean inQuote = false;
629: for (int i = 0; i < len; i++)
630: {
631: char c = text.charAt(i);
632: if (c == '"')
633: {
634: inQuote = !inQuote;
635: }
636: else if (c == '=' && key == null)
637: {
638: key = buf.toString().trim();
639: buf.setLength(0);
640: }
641: else if (c == ' ' && !inQuote)
642: {
643: String value = unquote(buf.toString().trim());
644: ret.put(key, value);
645: key = null;
646: buf.setLength(0);
647: }
648: else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
649: {
650: buf.append(c);
651: }
652: }
653: if (key != null)
654: {
655: String value = unquote(buf.toString().trim());
656: ret.put(key, value);
657: }
658: return ret;
659: }
660:
661: String unquote(String text)
662: {
663: int len = text.length();
664: if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
665: {
666: return text.substring(1, len - 1);
667: }
668: return text;
669: }
670:
671:
675: String getNonceCount(String nonce)
676: {
677: int nc = connection.getNonceCount(nonce);
678: String hex = Integer.toHexString(nc);
679: StringBuilder buf = new StringBuilder();
680: for (int i = 8 - hex.length(); i > 0; i--)
681: {
682: buf.append('0');
683: }
684: buf.append(hex);
685: return buf.toString();
686: }
687:
688:
691: byte[] nonce;
692:
693:
696: byte[] generateNonce()
697: throws IOException, NoSuchAlgorithmException
698: {
699: if (nonce == null)
700: {
701: long time = System.currentTimeMillis();
702: MessageDigest md5 = MessageDigest.getInstance("MD5");
703: md5.update(Long.toString(time).getBytes("US-ASCII"));
704: nonce = md5.digest();
705: }
706: return nonce;
707: }
708:
709: String toHexString(byte[] bytes)
710: {
711: char[] ret = new char[bytes.length * 2];
712: for (int i = 0, j = 0; i < bytes.length; i++)
713: {
714: int c =(int) bytes[i];
715: if (c < 0)
716: {
717: c += 0x100;
718: }
719: ret[j++] = Character.forDigit(c / 0x10, 0x10);
720: ret[j++] = Character.forDigit(c % 0x10, 0x10);
721: }
722: return new String(ret);
723: }
724:
725:
728: void handleSetCookie(String text)
729: {
730: CookieManager cookieManager = connection.getCookieManager();
731: if (cookieManager == null)
732: {
733: return;
734: }
735: String name = null;
736: String value = null;
737: String comment = null;
738: String domain = connection.getHostName();
739: String path = this.path;
740: int lsi = path.lastIndexOf('/');
741: if (lsi != -1)
742: {
743: path = path.substring(0, lsi);
744: }
745: boolean secure = false;
746: Date expires = null;
747:
748: int len = text.length();
749: String attr = null;
750: StringBuilder buf = new StringBuilder();
751: boolean inQuote = false;
752: for (int i = 0; i <= len; i++)
753: {
754: char c =(i == len) ? '\u0000' : text.charAt(i);
755: if (c == '"')
756: {
757: inQuote = !inQuote;
758: }
759: else if (!inQuote)
760: {
761: if (c == '=' && attr == null)
762: {
763: attr = buf.toString().trim();
764: buf.setLength(0);
765: }
766: else if (c == ';' || i == len || c == ',')
767: {
768: String val = unquote(buf.toString().trim());
769: if (name == null)
770: {
771: name = attr;
772: value = val;
773: }
774: else if ("Comment".equalsIgnoreCase(attr))
775: {
776: comment = val;
777: }
778: else if ("Domain".equalsIgnoreCase(attr))
779: {
780: domain = val;
781: }
782: else if ("Path".equalsIgnoreCase(attr))
783: {
784: path = val;
785: }
786: else if ("Secure".equalsIgnoreCase(val))
787: {
788: secure = true;
789: }
790: else if ("Max-Age".equalsIgnoreCase(attr))
791: {
792: int delta = Integer.parseInt(val);
793: Calendar cal = Calendar.getInstance();
794: cal.setTimeInMillis(System.currentTimeMillis());
795: cal.add(Calendar.SECOND, delta);
796: expires = cal.getTime();
797: }
798: else if ("Expires".equalsIgnoreCase(attr))
799: {
800: DateFormat dateFormat = new HTTPDateFormat();
801: try
802: {
803: expires = dateFormat.parse(val);
804: }
805: catch (ParseException e)
806: {
807:
808:
809:
810: buf.append(c);
811: continue;
812: }
813: }
814: attr = null;
815: buf.setLength(0);
816:
817: if (i == len || c == ',')
818: {
819: Cookie cookie = new Cookie(name, value, comment, domain,
820: path, secure, expires);
821: cookieManager.setCookie(cookie);
822: }
823: if (c == ',')
824: {
825:
826: name = null;
827: value = null;
828: comment = null;
829: domain = connection.getHostName();
830: path = this.path;
831: if (lsi != -1)
832: {
833: path = path.substring(0, lsi);
834: }
835: secure = false;
836: expires = null;
837: }
838: }
839: else
840: {
841: buf.append(c);
842: }
843: }
844: else
845: {
846: buf.append(c);
847: }
848: }
849: }
850:
851: }