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