package wjhk.jupload2.upload.helper; import java.io.IOException; import java.io.PushbackInputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import wjhk.jupload2.exception.JUploadEOFException; import wjhk.jupload2.exception.JUploadException; import wjhk.jupload2.exception.JUploadIOException; import wjhk.jupload2.policies.UploadPolicy; /** * A helper, to read the response coming from the server. * * @author etienne_sf */ public class HTTPInputStreamReader { // ////////////////////////////////////////////////////////////////////////////// // //////////////////// Main attributes // ////////////////////////////////////////////////////////////////////////////// /** * The current upload policy, always useful. */ private UploadPolicy uploadPolicy = null; private HTTPConnectionHelper httpConnectionHelper = null; /** * Contains the HTTP response bytearrayResponseBody, that is: the server * response, without the headers. */ String stringResponseBody = null; private byte[] bytearrayResponseBody = new byte[0]; /** * The headers of the HTTP response. */ String responseHeaders = null; /** * The status message from the first line of the response (e.g. "200 OK"). */ String responseMsg = null; // //////////////////////////////////////////////////////////////////////////////////// // /////////////////// ATTRIBUTE CONTAINING DATA COMING FROM THE RESPONSE // //////////////////////////////////////////////////////////////////////////////////// private CookieJar cookies = null; boolean gotClose = false; private boolean gotChunked = false; private boolean gotContentLength = false; private int clen = 0; /** * The server HTTP response. Should be 200, in case of success. */ private int httpStatusCode = 0; private String line = ""; private String charset = "ISO-8859-1"; // //////////////////////////////////////////////////////////////////////////////////// // /////////////////// CONSTANTS USED TO CONTROL THE HTTP INTPUT // //////////////////////////////////////////////////////////////////////////////////// private final static int CHUNKBUF_SIZE = 4096; private final byte chunkbuf[] = new byte[CHUNKBUF_SIZE]; private final static Pattern pChunked = Pattern.compile( "^Transfer-Encoding:\\s+chunked", Pattern.CASE_INSENSITIVE); private final static Pattern pClose = Pattern.compile( "^Connection:\\s+close", Pattern.CASE_INSENSITIVE); private final static Pattern pProxyClose = Pattern.compile( "^Proxy-Connection:\\s+close", Pattern.CASE_INSENSITIVE); private final static Pattern pHttpStatus = Pattern .compile("^HTTP/\\d\\.\\d\\s+((\\d+)\\s+.*)$"); private final static Pattern pContentLen = Pattern.compile( "^Content-Length:\\s+(\\d+)$", Pattern.CASE_INSENSITIVE); private final static Pattern pContentTypeCs = Pattern.compile( "^Content-Type:\\s+.*;\\s*charset=([^;\\s]+).*$", Pattern.CASE_INSENSITIVE); private final static Pattern pSetCookie = Pattern.compile( "^Set-Cookie:\\s+(.*)$", Pattern.CASE_INSENSITIVE); /** * The standard constructor: does nothing ! Oh yes, it initialize some * attribute from the given parameter... :-) * * @param httpConnectionHelper The connection helper, associated with this * instance. * @param uploadPolicy The current upload policy. */ public HTTPInputStreamReader(HTTPConnectionHelper httpConnectionHelper, UploadPolicy uploadPolicy) { this.httpConnectionHelper = httpConnectionHelper; this.uploadPolicy = uploadPolicy; this.cookies = new CookieJar(uploadPolicy); } /** * Return the last read http response (200, in case of success). * * @return The last read http response */ public int gethttpStatusCode() { return this.httpStatusCode; } /** * Get the last response bytearrayResponseBody. * * @return The last read response bytearrayResponseBody. */ public String getResponseBody() { return this.stringResponseBody; } /** * Get the last response bytearrayResponseBody. This byte array should be * decoded by using the {@link #getResponseCharset()}. * * @return The last read response bytearrayResponseBody. * */ public byte[] getResponseBodyAsByteArray() { return this.bytearrayResponseBody; } /** * Get the charset that should be used to decode the last response, when * using the {@link #getResponseBodyAsByteArray()} method. * * @return The last read response bytearrayResponseBody. */ public String getResponseCharset() { return this.charset; } /** * Get the headers of the HTTP response. * * @return The HTTP headers. */ public String getResponseHeaders() { return this.responseHeaders; } /** * Get the last response message. * * @return The response message from the first line of the response (e.g. * "200 OK"). */ public String getResponseMsg() { return this.responseMsg; } /** * The main method: reads the response in the input stream. * * @return The response status (e.g.: 200 if everything was ok) * @throws JUploadException */ public int readHttpResponse() throws JUploadException { PushbackInputStream httpDataIn = this.httpConnectionHelper .getInputStream(); try { // We first read the headers, readHeaders(httpDataIn); // then the bytearrayResponseBody. // If we're in a HEAD request ... we're not interested in the // bytearrayResponseBody! if (this.httpConnectionHelper.getMethod().equals("HEAD")) { this.uploadPolicy .displayDebug( "This is a HEAD request: we don't care about the bytearrayResponseBody", 70); this.stringResponseBody = ""; } else { readBody(httpDataIn); } } catch (JUploadException e) { throw e; } catch (Exception e) { throw new JUploadException(e); } return this.httpStatusCode; } // ////////////////////////////////////////////////////////////////////////////////////// // //////////////////// Various utilities // ////////////////////////////////////////////////////////////////////////////////////// /** * Concatenates two byte arrays. * * @param buf1 The first array * @param buf2 The second array * @return A byte array, containing buf2 appended to buf2 */ static byte[] byteAppend(byte[] buf1, byte[] buf2) { byte[] ret = new byte[buf1.length + buf2.length]; System.arraycopy(buf1, 0, ret, 0, buf1.length); System.arraycopy(buf2, 0, ret, buf1.length, buf2.length); return ret; } /** * Concatenates two byte arrays. * * @param buf1 The first array * @param buf2 The second array * @param len Number of bytes to copy from buf2 * @return A byte array, containing buf2 appended to buf2 */ static byte[] byteAppend(byte[] buf1, byte[] buf2, int len) { if (len > buf2.length) len = buf2.length; byte[] ret = new byte[buf1.length + len]; System.arraycopy(buf1, 0, ret, 0, buf1.length); System.arraycopy(buf2, 0, ret, buf1.length, len); return ret; } /** * Similar like BufferedInputStream#readLine() but operates on raw bytes. * Line-Ending is always "\r\n". * * @param inputStream * * @param charset The input charset of the stream. * @param includeCR Set to true, if the terminating CR/LF should be included * in the returned byte array. * @return The line, encoded from the input stream with the given charset * @throws IOException * @throws JUploadException */ public static String readLine(PushbackInputStream inputStream, String charset, boolean includeCR) throws IOException, JUploadException { byte[] line = readLine(inputStream, includeCR); return (null == line) ? null : new String(line, charset); } /** * Similar like BufferedInputStream#readLine() but operates on raw bytes. * According to RFC 2616, and of line may be CR (13), LF (10) or CRLF. * Line-Ending is always "\r\n" in header, but not in text bodies. * Update done by TedA (sourceforge account: tedaaa). Allows to manage * response from web server that send LF instead of CRLF ! Here is a part of * the RFC: "we recommend that applications, when parsing such headers, * recognize a single LF as a line terminator and ignore the leading * CR".
* Corrected again to manage line finished by CR only. This is not allowed * in headers, but this method is also used to read lines in the * bytearrayResponseBody. * * @param inputStream * * @param includeCR Set to true, if the terminating CR/LF should be included * in the returned byte array. In this case, CR/LF is always * returned to the caller, whether the input stream got CR, LF or * CRLF. * @return The byte array from the input stream, with or without a trailing * CRLF * @throws IOException * @throws JUploadException */ public static byte[] readLine(PushbackInputStream inputStream, boolean includeCR) throws IOException, JUploadException { final byte EOS = -1; final byte CR = 13; final byte LF = 10; int len = 0; int buflen = 128; // average line length byte[] buf = new byte[buflen]; byte[] ret = null; int b; boolean lineRead = false; while (!lineRead) { try { b = inputStream.read(); } catch (IOException ioe) { throw new JUploadIOException(ioe.getClass().getName() + ": " + ioe.getMessage() + " (while reading server response )", ioe); } catch (Exception e) { throw new JUploadException(e.getClass().getName() + ": " + e.getMessage() + " (while reading server response )", e); } switch (b) { case EOS: // We've finished reading the stream, and so the line is // finished too. if (len == 0) { return null; } lineRead = true; break; /* * if (len > 0) { ret = new byte[len]; System.arraycopy(buf, 0, * ret, 0, len); return ret; } return null; */ case LF: // We found the end of the current line. lineRead = true; break; case CR: // We got a CR. It can be the end of line. // Is it followed by a LF ? (not mandatory in RFC 2616) b = inputStream.read(); if (b != LF) { // The end of line was a simple LF: the next one blongs // to the next line. inputStream.unread(b); } lineRead = true; break; default: buf[len++] = (byte) b; // If the buffer is too small, we let enough space to add CR // and LF, in case of ... if (len + 2 >= buflen) { buflen *= 2; byte[] tmp = new byte[buflen]; System.arraycopy(buf, 0, tmp, 0, len); buf = tmp; } } } // while // Let's go back to before any CR and LF. while (len > 0 && (buf[len] == CR || buf[len] == LF)) { len -= 1; } // Ok, now len indicates the end of the actual line. // Should we add a proper CRLF, or nothing ? if (includeCR) { // We have enough space to add these two characters (see the default // here above) buf[len++] = CR; buf[len++] = LF; } if (len > 0) { ret = new byte[len]; if (len > 0) System.arraycopy(buf, 0, ret, 0, len); } else { // line feed for empty line between headers and // bytearrayResponseBody, or within the // bytearrayResponseBody. ret = new byte[0]; } return ret; } /** * Read the headers from the given input stream. * * @param httpDataIn The http input stream * @throws IOException * @throws JUploadException */ private void readHeaders(PushbackInputStream httpDataIn) throws IOException, JUploadException { StringBuffer sbHeaders = new StringBuffer(); // Headers are US-ASCII (See RFC 2616, Section 2.2) String tmp; // We must be reading the first line of the HTTP header. this.uploadPolicy.displayDebug( "-------- Response Headers Start --------", 80); do { tmp = readLine(httpDataIn, "US-ASCII", false); if (null == tmp) { throw new JUploadEOFException(this.uploadPolicy, "reading headers"); } if (this.httpStatusCode == 0) { Matcher m = pHttpStatus.matcher(tmp); if (m.matches()) { this.httpStatusCode = Integer.parseInt(m.group(2)); this.responseMsg = m.group(1); } else { // The status line must be the first line of the // response. (See RFC 2616, Section 6.1) so this // is an error. // We first display the wrong line. this.uploadPolicy.displayDebug("First line of response: '" + tmp + "'", 80); // Then, we throw the exception. throw new JUploadException( "HTTP response did not begin with status line."); } } // Handle folded headers (RFC 2616, Section 2.2). This is // handled after the status line, because that line may // not be folded (RFC 2616, Section 6.1). if (tmp.startsWith(" ") || tmp.startsWith("\t")) this.line += " " + tmp.trim(); else this.line = tmp; // The read line is now correctly formatted. this.uploadPolicy.displayDebug(this.line, 80); sbHeaders.append(tmp).append("\n"); if (pClose.matcher(this.line).matches()) this.gotClose = true; if (pProxyClose.matcher(this.line).matches()) this.gotClose = true; if (pChunked.matcher(this.line).matches()) this.gotChunked = true; Matcher m = pContentLen.matcher(this.line); if (m.matches()) { this.gotContentLength = true; this.clen = Integer.parseInt(m.group(1)); } m = pContentTypeCs.matcher(this.line); if (m.matches()) this.charset = m.group(1); m = pSetCookie.matcher(this.line); if (m.matches()) { this.uploadPolicy.displayDebug( "Calling this.cookies.parseCookieHeader, with parameter: " + m.group(1), 80); this.cookies.parseCookieHeader(m.group(1)); this.uploadPolicy.displayDebug("Cookie header parsed.", 80); } // RFC 2616, Section 6. Body is separated by the header with an // empty line: so end of headers is an empty line. } while (this.line.length() > 0); this.responseHeaders = sbHeaders.toString(); this.uploadPolicy.displayDebug( "--------- Response Headers End ---------", 80); }// readHeaders() /** * Read the bytearrayResponseBody from the given input stream. * * @param httpDataIn The http input stream * @throws IOException * @throws JUploadException * @throws JUploadException */ private void readBody(PushbackInputStream httpDataIn) throws IOException, JUploadException { // && is evaluated from left to right so !stop must come first! while ((!this.gotContentLength) || (this.clen > 0)) { if (this.gotChunked) { // Read the chunk header. // This is US-ASCII! (See RFC 2616, Section 2.2) this.line = readLine(httpDataIn, "US-ASCII", false); if (null == this.line) throw new JUploadEOFException(this.uploadPolicy, "reading HTTP Body, HTTP chunked mode (1)"); // Handle a single chunk of the response // We cut off possible chunk extensions and ignore them. // The length is hex-encoded (RFC 2616, Section 3.6.1) int len = Integer.parseInt(this.line.replaceFirst(";.*", "") .trim(), 16); this.uploadPolicy.displayDebug("Chunk: " + this.line + " dec: " + len, 70); if (len == 0) { // RFC 2616, Section 3.6.1: A length of 0 denotes // the last chunk of the bytearrayResponseBody. // This code wrong if the server sends chunks with trailers! // (trailers are HTTP Headers that are send *after* the // bytearrayResponseBody. These are announced // in the regular HTTP header "Trailer". // Fritz: Never seen them so far ... // TODO: Implement trailer-handling. break; } // Loop over the chunk (len == length of the chunk) while (len > 0) { int rlen = (len > CHUNKBUF_SIZE) ? CHUNKBUF_SIZE : len; int ofs = 0; if (rlen > 0) { while (ofs < rlen) { int res = httpDataIn.read(this.chunkbuf, ofs, rlen - ofs); if (res < 0) throw new JUploadEOFException( this.uploadPolicy, "reading body, HTTP chunk mode (2)"); len -= res; ofs += res; } if (ofs < rlen) throw new JUploadException("short read"); if (rlen < CHUNKBUF_SIZE) this.bytearrayResponseBody = byteAppend( this.bytearrayResponseBody, this.chunkbuf, rlen); else this.bytearrayResponseBody = byteAppend( this.bytearrayResponseBody, this.chunkbuf); } } // Got the whole chunk, read the trailing CRLF. readLine(httpDataIn, false); } else { // Not chunked. Use either content-length (if available) // or read until EOF. if (this.gotContentLength) { // Got a Content-Length. Read exactly that amount of // bytes. while (this.clen > 0) { int rlen = (this.clen > CHUNKBUF_SIZE) ? CHUNKBUF_SIZE : this.clen; int ofs = 0; if (rlen > 0) { while (ofs < rlen) { int res = httpDataIn.read(this.chunkbuf, ofs, rlen - ofs); if (res < 0) throw new JUploadEOFException( this.uploadPolicy, "reading HTTP bytearrayResponseBody, not chunked mode"); this.clen -= res; ofs += res; } if (ofs < rlen) throw new JUploadException("short read"); if (rlen < CHUNKBUF_SIZE) this.bytearrayResponseBody = byteAppend( this.bytearrayResponseBody, this.chunkbuf, rlen); else this.bytearrayResponseBody = byteAppend( this.bytearrayResponseBody, this.chunkbuf); } } } else { // No Content-length available, read until EOF // while (true) { byte[] lbuf = readLine(httpDataIn, true); if (null == lbuf) break; this.bytearrayResponseBody = byteAppend( this.bytearrayResponseBody, lbuf); } break; } } } // while // Convert the whole bytearrayResponseBody according to the charset. // The default for charset ISO-8859-1, but overridden by // the charset attribute of the Content-Type header (if any). // See RFC 2616, Sections 3.4.1 and 3.7.1. this.stringResponseBody = new String(this.bytearrayResponseBody, this.charset); // At the higher debug level, we display the response. this.uploadPolicy.displayDebug("-------- Response Body Start --------", 99); this.uploadPolicy.displayDebug(this.stringResponseBody, 99); this.uploadPolicy.displayDebug("-------- Response Body End --------", 99); }// readBody }