001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.vfs;
031:
032: import com.caucho.util.Alarm;
033: import com.caucho.util.CharBuffer;
034: import com.caucho.util.Log;
035:
036: import javax.net.ssl.SSLContext;
037: import javax.net.ssl.SSLSocketFactory;
038: import java.io.IOException;
039: import java.io.InputStream;
040: import java.io.OutputStream;
041: import java.net.ConnectException;
042: import java.net.Socket;
043: import java.net.SocketException;
044: import java.util.HashMap;
045: import java.util.Iterator;
046: import java.util.logging.Level;
047: import java.util.logging.Logger;
048:
049: /**
050: * Underlying stream handling HTTP requests.
051: */
052: class HttpStream extends StreamImpl {
053: private static final Logger log = Log.open(HttpStream.class);
054: // reserved headers that should not be passed to the HTTP server
055: private static HashMap<String, String> _reserved;
056:
057: private static final Object LOCK = new Object();
058:
059: // Saved keepalive stream for a new request.
060: private static HttpStream _savedStream;
061: // Time the stream was saved
062: private static long _saveTime;
063:
064: private long _socketTimeout = 30000L;
065:
066: private boolean _isSSL;
067:
068: private Socket _s;
069: private InputStream _is;
070: private OutputStream _os;
071: private ReadStream _rs;
072: private WriteStream _ws;
073:
074: // The server's host name
075: private String _host;
076: // The server's port
077: private int _port;
078:
079: private String _virtualHost;
080:
081: // the method
082: private String _method;
083: // true for a HEAD stream
084: private boolean _isHead;
085: // true for a POST stream
086: private boolean _isPost;
087:
088: // buffer containing the POST data
089: private MemoryStream _tempStream;
090:
091: // true if keepalive is allowed
092: private boolean _isKeepalive = true;
093: // true after the request has been sent
094: private boolean _didGet;
095: // content length from the returned response
096: private int _contentLength;
097: // true if the response was chunked
098: private boolean _isChunked;
099: // length of the current chunk, -1 on eof
100: private int _chunkLength;
101: // the request is done
102: private boolean _isRequestDone;
103:
104: private HashMap<String, Object> _attributes;
105:
106: // Used to read unread bytes on recycle.
107: private byte[] _tempBuffer;
108:
109: /**
110: * Create a new HTTP stream.
111: */
112: private HttpStream(Path path, String host, int port, Socket s)
113: throws IOException {
114: _s = s;
115:
116: _host = host;
117: _port = port;
118:
119: _is = _s.getInputStream();
120: _os = _s.getOutputStream();
121:
122: _ws = VfsStream.openWrite(_os);
123: _rs = VfsStream.openRead(_is, _ws);
124:
125: _attributes = new HashMap<String, Object>();
126:
127: init(path);
128: }
129:
130: /**
131: * Opens a new HTTP stream for reading, i.e. a GET request.
132: *
133: * @param path the URL for the stream
134: *
135: * @return the opened stream
136: */
137: static HttpStreamWrapper openRead(HttpPath path) throws IOException {
138: HttpStream stream = createStream(path);
139: stream._isPost = false;
140:
141: return new HttpStreamWrapper(stream);
142: }
143:
144: /**
145: * Opens a new HTTP stream for reading and writing, i.e. a POST request.
146: *
147: * @param path the URL for the stream
148: *
149: * @return the opened stream
150: */
151: static HttpStreamWrapper openReadWrite(HttpPath path)
152: throws IOException {
153: HttpStream stream = createStream(path);
154: stream._isPost = true;
155:
156: return new HttpStreamWrapper(stream);
157: }
158:
159: /**
160: * Creates a new HTTP stream. If there is a saved connection to
161: * the same host, use it.
162: *
163: * @param path the URL for the stream
164: *
165: * @return the opened stream
166: */
167: static private HttpStream createStream(HttpPath path)
168: throws IOException {
169: String host = path.getHost();
170: int port = path.getPort();
171:
172: HttpStream stream = null;
173: long streamTime = 0;
174: synchronized (LOCK) {
175: if (_savedStream != null
176: && host.equals(_savedStream.getHost())
177: && port == _savedStream.getPort()) {
178: stream = _savedStream;
179: streamTime = _saveTime;
180: _savedStream = null;
181: }
182: }
183:
184: if (stream == null) {
185: }
186: // if the stream is still valid, use it
187: else if (Alarm.getCurrentTime() < streamTime + 5000) {
188: stream.init(path);
189: return stream;
190: }
191: // if the stream has timed out, close it
192: else {
193: try {
194: stream._isKeepalive = false;
195: stream.close();
196: } catch (IOException e) {
197: log.log(Level.FINE, e.toString(), e);
198: }
199: }
200:
201: Socket s;
202:
203: try {
204: s = new Socket(host, port);
205:
206: if (path instanceof HttpsPath) {
207: SSLContext context = SSLContext.getInstance("TLS");
208:
209: javax.net.ssl.TrustManager tm = new javax.net.ssl.X509TrustManager() {
210: public java.security.cert.X509Certificate[] getAcceptedIssuers() {
211: return null;
212: }
213:
214: public void checkClientTrusted(
215: java.security.cert.X509Certificate[] cert,
216: String foo) {
217: }
218:
219: public void checkServerTrusted(
220: java.security.cert.X509Certificate[] cert,
221: String foo) {
222: }
223: };
224:
225: context.init(null,
226: new javax.net.ssl.TrustManager[] { tm }, null);
227: SSLSocketFactory factory = context.getSocketFactory();
228:
229: s = factory.createSocket(s, host, port, true);
230: }
231: } catch (ConnectException e) {
232: throw new ConnectException(path.getURL() + ": "
233: + e.getMessage());
234: } catch (Exception e) {
235: throw new ConnectException(path.getURL() + ": "
236: + e.toString());
237: }
238:
239: int socketTimeout = 300 * 1000;
240:
241: try {
242: s.setSoTimeout(socketTimeout);
243: } catch (Exception e) {
244: }
245:
246: return new HttpStream(path, host, port, s);
247: }
248:
249: /**
250: * Initializes the stream for the next request.
251: */
252: private void init(Path path) {
253: _contentLength = -1;
254: _isChunked = false;
255: _isRequestDone = false;
256: _didGet = false;
257: _isPost = false;
258: _isHead = false;
259: _method = null;
260: _attributes.clear();
261:
262: setPath(path);
263:
264: if (path instanceof HttpPath)
265: _virtualHost = ((HttpPath) path).getVirtualHost();
266: }
267:
268: /**
269: * Set if this should be an SSL connection.
270: */
271: public void setSSL(boolean isSSL) {
272: _isSSL = isSSL;
273: }
274:
275: /**
276: * Set if this should be an SSL connection.
277: */
278: public boolean isSSL() {
279: return _isSSL;
280: }
281:
282: /**
283: * Sets the method
284: */
285: public void setMethod(String method) {
286: _method = method;
287: }
288:
289: /**
290: * Sets true if we're only interested in the head.
291: */
292: public void setHead(boolean isHead) {
293: _isHead = isHead;
294: }
295:
296: /**
297: * Returns the stream's host.
298: */
299: public String getHost() {
300: return _host;
301: }
302:
303: /**
304: * Returns the stream's port.
305: */
306: public int getPort() {
307: return _port;
308: }
309:
310: /**
311: * Returns a header from the response returned from the HTTP server.
312: *
313: * @param name name of the header
314: * @return the header value.
315: */
316: public Object getAttribute(String name) throws IOException {
317: if (!_didGet)
318: getConnInput();
319:
320: return _attributes.get(name.toLowerCase());
321: }
322:
323: /**
324: * Returns an iterator of the returned header names.
325: */
326: public Iterator getAttributeNames() throws IOException {
327: if (!_didGet)
328: getConnInput();
329:
330: return _attributes.keySet().iterator();
331: }
332:
333: /**
334: * Sets a header for the request.
335: */
336: public void setAttribute(String name, Object value) {
337: if (name.equals("method"))
338: setMethod((String) value);
339: else if (name.equals("socket-timeout")) {
340: if (value instanceof Integer) {
341: int socketTimeout = ((Integer) value).intValue();
342:
343: if (socketTimeout > 0) {
344: try {
345: if (_s != null)
346: _s.setSoTimeout(socketTimeout);
347: } catch (Exception e) {
348:
349: }
350: }
351: }
352: } else
353: _attributes.put(name.toLowerCase(), value);
354: }
355:
356: /**
357: * Remove a header for the request.
358: */
359: public void removeAttribute(String name) {
360: _attributes.remove(name.toLowerCase());
361: }
362:
363: /**
364: * Sets the timeout.
365: */
366: public void setSocketTimeout(long timeout) throws SocketException {
367: if (_s != null)
368: _s.setSoTimeout((int) timeout);
369: }
370:
371: /**
372: * The stream is always writable (?)
373: */
374: public boolean canWrite() {
375: return true;
376: }
377:
378: /**
379: * Writes a buffer to the underlying stream.
380: *
381: * @param buffer the byte array to write.
382: * @param offset the offset into the byte array.
383: * @param length the number of bytes to write.
384: * @param isEnd true when the write is flushing a close.
385: */
386: public void write(byte[] buf, int offset, int length, boolean isEnd)
387: throws IOException {
388: if (!_isPost)
389: return;
390:
391: if (_tempStream == null)
392: _tempStream = new MemoryStream();
393:
394: _tempStream.write(buf, offset, length, isEnd);
395: }
396:
397: /**
398: * The stream is readable.
399: */
400: public boolean canRead() {
401: return true;
402: }
403:
404: /**
405: * Read data from the connection. If the request hasn't yet been sent
406: * to the server, send it.
407: */
408: public int read(byte[] buf, int offset, int length)
409: throws IOException {
410: try {
411: return readInt(buf, offset, length);
412: } catch (IOException e) {
413: _isKeepalive = false;
414: throw e;
415: } catch (RuntimeException e) {
416: _isKeepalive = false;
417: throw e;
418: }
419: }
420:
421: /**
422: * Read data from the connection. If the request hasn't yet been sent
423: * to the server, send it.
424: */
425: public int readInt(byte[] buf, int offset, int length)
426: throws IOException {
427: if (!_didGet)
428: getConnInput();
429:
430: if (_isRequestDone)
431: return -1;
432:
433: try {
434: int len = length;
435:
436: if (_isChunked) {
437: if (_chunkLength == 0) {
438: int ch;
439:
440: for (ch = _rs.read(); ch >= 0
441: && (ch == '\r' || ch == '\n' || ch == ' '); ch = _rs
442: .read()) {
443: }
444:
445: for (; ch >= 0 && ch != '\n'; ch = _rs.read()) {
446: if (ch >= '0' && ch <= '9')
447: _chunkLength = 16 * _chunkLength + ch - '0';
448: else if (ch >= 'a' && ch <= 'f')
449: _chunkLength = 16 * _chunkLength + ch - 'a'
450: + 10;
451: else if (ch >= 'A' && ch <= 'F')
452: _chunkLength = 16 * _chunkLength + ch - 'A'
453: + 10;
454: }
455:
456: if (_chunkLength == 0) {
457: _isRequestDone = true;
458: return -1;
459: }
460: } else if (_chunkLength < 0)
461: return -1;
462:
463: if (_chunkLength < len)
464: len = _chunkLength;
465: } else if (_contentLength < 0) {
466: } else if (_contentLength == 0) {
467: _isRequestDone = true;
468: return -1;
469: } else if (_contentLength < len)
470: len = _contentLength;
471:
472: len = _rs.read(buf, offset, len);
473:
474: if (len < 0) {
475: } else if (_isChunked)
476: _chunkLength -= len;
477: else if (_contentLength > 0)
478: _contentLength -= len;
479:
480: return len;
481: } catch (IOException e) {
482: _isKeepalive = false;
483: throw e;
484: } catch (RuntimeException e) {
485: _isKeepalive = false;
486: throw e;
487: }
488: }
489:
490: /**
491: * Sends the request and initializes the response.
492: */
493: private void getConnInput() throws IOException {
494: if (_didGet)
495: return;
496:
497: try {
498: getConnInputImpl();
499: } catch (IOException e) {
500: _isKeepalive = false;
501: throw e;
502: } catch (RuntimeException e) {
503: _isKeepalive = false;
504: throw e;
505: }
506: }
507:
508: /**
509: * Send the request to the server, wait for the response and parse
510: * the headers.
511: */
512: private void getConnInputImpl() throws IOException {
513: if (_didGet)
514: return;
515:
516: _didGet = true;
517:
518: if (_method != null) {
519: _ws.print(_method);
520: _ws.print(' ');
521: } else if (_isPost)
522: _ws.print("POST ");
523: else if (_isHead)
524: _ws.print("HEAD ");
525: else
526: _ws.print("GET ");
527:
528: // Not splitting query? Also fullpath?
529: _ws.print(_path.getPath());
530:
531: if (_path.getQuery() != null) {
532: _ws.print("?");
533: _ws.print(_path.getQuery());
534: }
535: _ws.print(" HTTP/1.1\r\n");
536: _ws.print("Host: ");
537: if (_virtualHost != null)
538: _ws.print(_virtualHost);
539: else {
540: _ws.print(_path.getHost());
541: if (_path.getPort() != 80) {
542: _ws.print(":");
543: _ws.print(String.valueOf(_path.getPort()));
544: }
545: }
546: _ws.print("\r\n");
547:
548: Object userAgent = getAttribute("User-Agent");
549: if (userAgent == null)
550: _ws
551: .print("User-Agent: Mozilla/4.0 (compatible; Resin 1.0; JDK)\r\n");
552: else
553: _ws.print("User-Agent: " + userAgent + "\r\n");
554: Iterator iter = getAttributeNames();
555: while (iter.hasNext()) {
556: String name = (String) iter.next();
557: if (_reserved.get(name.toLowerCase()) == null)
558: _ws.print(name + ": " + getAttribute(name) + "\r\n");
559: }
560: if (!_isKeepalive)
561: _ws.print("Connection: close\r\n");
562:
563: if (_isPost) {
564: int length = 0;
565: if (_tempStream != null)
566: length = _tempStream.getLength();
567: _ws.print("Content-Length: ");
568: _ws.print(length);
569: _ws.print("\r\n");
570: }
571: _ws.print("\r\n");
572:
573: if (_isPost) {
574: MemoryStream tempStream = _tempStream;
575: _tempStream = null;
576: if (tempStream != null) {
577: tempStream.writeToStream(_ws);
578: tempStream.destroy();
579: }
580: }
581:
582: _attributes.clear();
583:
584: parseHeaders();
585:
586: if (_isHead)
587: _isRequestDone = true;
588: }
589:
590: /**
591: * Parse the headers returned from the server.
592: */
593: private void parseHeaders() throws IOException {
594: CharBuffer line = new CharBuffer();
595:
596: // Skip blank lines
597: int count = 0;
598: do {
599: line.clear();
600: if (!_rs.readln(line)) {
601: _isKeepalive = false;
602: return;
603: }
604: } while (line.length() == 0 && ++count < 10);
605:
606: if (line.length() == 0) {
607: _isKeepalive = false;
608: return;
609: }
610:
611: if (line.startsWith("HTTP/1.1 100")) {
612: count = 100;
613: do {
614: line.clear();
615: if (!_rs.readln(line)) {
616: _isKeepalive = false;
617: return;
618: }
619: } while (line.length() != 0 && count-- > 0);
620:
621: count = 100;
622: do {
623: line.clear();
624: if (!_rs.readln(line)) {
625: _isKeepalive = false;
626: return;
627: }
628: } while (line.length() == 0 && count-- > 0);
629: }
630:
631: if (line.length() == 0) {
632: _isKeepalive = false;
633: return;
634: }
635:
636: int i = 0;
637: for (i = 0; i < line.length() && line.charAt(i) != ' '; i++) {
638: }
639:
640: for (; i < line.length() && line.charAt(i) == ' '; i++) {
641: }
642:
643: int status = 0;
644: for (; i < line.length(); i++) {
645: char ch = line.charAt(i);
646: if (ch >= '0' && ch <= '9')
647: status = 10 * status + ch - '0';
648: else
649: break;
650: }
651:
652: if (status != 200)
653: _isKeepalive = false;
654: else if (!line.startsWith("HTTP/1.1 "))
655: _isKeepalive = false;
656:
657: _attributes.put("status", String.valueOf(status));
658:
659: CharBuffer key = new CharBuffer();
660: while (true) {
661: line.clear();
662: if (!_rs.readln(line) || line.length() == 0)
663: break;
664:
665: int lineLength = line.length();
666:
667: for (i = 0; i < lineLength
668: && Character.isWhitespace(line.charAt(i)); i++) {
669: }
670:
671: key.clear();
672: for (; i < lineLength
673: && !Character.isWhitespace(line.charAt(i))
674: && line.charAt(i) != ':'; i++) {
675: key.append((char) line.charAt(i));
676: }
677:
678: for (; i < lineLength
679: && Character.isWhitespace(line.charAt(i)); i++) {
680: }
681:
682: if (key.length() == 0 || lineLength <= i
683: || line.charAt(i) != ':')
684: continue;
685:
686: for (i++; i < lineLength
687: && Character.isWhitespace(line.charAt(i)); i++) {
688: }
689:
690: key.toLowerCase();
691: String value = line.substring(i);
692:
693: if (log.isLoggable(Level.FINE))
694: log.fine(key + ": " + value);
695:
696: if (key.matchesIgnoreCase("content-length")) {
697: _contentLength = Integer.parseInt(value);
698: } else if (key.matchesIgnoreCase("connection")
699: && value.equalsIgnoreCase("close")) {
700: _isKeepalive = false;
701: } else if (key.matchesIgnoreCase("transfer-encoding")
702: && value.equalsIgnoreCase("chunked")) {
703:
704: _isChunked = true;
705: _chunkLength = 0;
706: }
707:
708: _attributes.put(key.toLowerCase().toString(), value);
709: }
710: }
711:
712: /**
713: * Returns the bytes still available.
714: */
715: public int getAvailable() throws IOException {
716: if (!_didGet)
717: getConnInput();
718:
719: if (_contentLength > 0)
720: return _contentLength;
721: else
722: return _rs.getAvailable();
723: }
724:
725: /**
726: * Close the connection.
727: */
728: public void close() throws IOException {
729: if (_isKeepalive) {
730: // If recycling, read any unread data
731: if (!_didGet)
732: getConnInput();
733:
734: if (!_isRequestDone) {
735: if (_tempBuffer == null)
736: _tempBuffer = new byte[256];
737:
738: try {
739: while (read(_tempBuffer, 0, _tempBuffer.length) > 0) {
740: }
741: } catch (IOException e) {
742: _isKeepalive = false;
743: }
744: }
745: }
746:
747: if (com.caucho.util.Alarm.isTest())
748: _isKeepalive = false; // XXX:
749:
750: if (_isKeepalive) {
751: HttpStream oldSaved;
752: long now = Alarm.getCurrentTime();
753: synchronized (LOCK) {
754: oldSaved = _savedStream;
755: _savedStream = this ;
756: _saveTime = now;
757: }
758:
759: if (oldSaved != null && oldSaved != this ) {
760: oldSaved._isKeepalive = false;
761: oldSaved.close();
762: }
763:
764: return;
765: }
766:
767: try {
768: try {
769: if (_ws != null)
770: _ws.close();
771: } catch (Throwable e) {
772: }
773: _ws = null;
774:
775: try {
776: if (_rs != null)
777: _rs.close();
778: } catch (Throwable e) {
779: }
780: _rs = null;
781:
782: try {
783: if (_os != null)
784: _os.close();
785: } catch (Throwable e) {
786: }
787: _os = null;
788:
789: try {
790: if (_is != null)
791: _is.close();
792: } catch (Throwable e) {
793: }
794: _is = null;
795: } finally {
796: if (_s != null)
797: _s.close();
798: _s = null;
799: }
800: }
801:
802: static {
803: _reserved = new HashMap<String, String>();
804: _reserved.put("user-agent", "");
805: _reserved.put("content-length", "");
806: _reserved.put("content-encoding", "");
807: _reserved.put("connection", "");
808: _reserved.put("host", "");
809: }
810: }
|