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