/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sun.jini.jeri.internal.http; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.util.StringTokenizer; /** * Class for reading HTTP messages. Each instance reads a single HTTP message. * * @author Sun Microsystems, Inc. * */ class MessageReader { /* state values */ private static final int START = 0; private static final int HEADER = 1; private static final int CONTENT = 2; private static final int DONE = 3; private static final byte[] sink = new byte[256]; private final InputStream in; private final boolean noContent; private int state = START; private StartLine sline; private InputStream cin; /** * Creates new reader on top of given input stream. If noContent is true, * incoming message is assumed to be bodiless (e.g., a HEAD response). */ MessageReader(InputStream in, boolean noContent) { this.in = in; this.noContent = noContent; } /** * Reads in HTTP message start line. */ StartLine readStartLine() throws IOException { updateState(START, HEADER); sline = new StartLine(in); return sline; } /** * Reads in HTTP message header. */ Header readHeader() throws IOException { updateState(HEADER, CONTENT); Header header = new Header(in); if (!noContent && contentIndicated(sline, header)) { String clen; if (header.containsValue("Transfer-Encoding", "chunked", true)) { cin = new ChunkedInputStream(); } else if ((clen = header.getField("Content-Length")) != null) { int len; try { len = Integer.parseInt(clen); } catch (Exception ex) { throw new HttpParseException("invalid content length"); } if (len < 0) { throw new HttpParseException("invalid content length"); } cin = new BoundedInputStream(len); } else if (sline.isRequest) { throw new HttpParseException("request length undeclared"); } else { cin = in; } } else { cin = new BoundedInputStream(0); } return header; } /** * Reads message content. */ int readContent(byte[] b, int off, int len) throws IOException { updateState(CONTENT, CONTENT); return cin.read(b, off, len); } /** * Returns count of available message content. */ int availableContent() throws IOException { updateState(CONTENT, CONTENT); return cin.available(); } /** * Reads in message trailer after consuming any unread content data. * Returns null if message doesn't have a trailer. */ Header readTrailer() throws IOException { updateState(CONTENT, CONTENT); while (cin.read(sink) != -1) { } updateState(CONTENT, DONE); if (cin instanceof ChunkedInputStream) { Header trailer = new Header(in); return (trailer.size() > 0) ? trailer : null; } else { return null; } } /** * Reads and returns next line from stream, or null if at end of stream. * Expects ASCII lines terminated by the HTTP end-of-line sequence "\r\n". */ static String readLine(InputStream in) throws IOException { // REMIND: use Charset? int c = in.read(); if (c == -1) { return null; } StringBuffer sbuf = new StringBuffer(); do { if (c == '\r') { if ((c = in.read()) == '\n') { break; } sbuf.append('\r'); } else { sbuf.append((char) c); c = in.read(); } } while (c != -1); return sbuf.toString(); } private void updateState(int oldState, int newState) { if (state != oldState) { throw new IllegalStateException(); } state = newState; } /** * Returns true if given start line and header indicate a content body. */ private static boolean contentIndicated(StartLine sline, Header header) { if (!sline.isRequest && ((sline.status / 100) == 1 || sline.status == HttpURLConnection.HTTP_NO_CONTENT || sline.status == HttpURLConnection.HTTP_NOT_MODIFIED)) { return false; } if (header.getField("Transfer-Encoding") != null || header.getField("Content-Length") != null) { return true; } return !sline.isRequest; } /** * Input stream for reading bounded content data. */ private class BoundedInputStream extends InputStream { private int bound; BoundedInputStream(int bound) { this.bound = bound; } public int read() throws IOException { byte[] b = new byte[1]; return (read(b, 0, 1) != -1) ? b[0] & 0xFF : -1; } public int read(byte[] b, int off, int len) throws IOException { if (bound == 0) { return -1; } else { int n = in.read(b, off, Math.min(bound, len)); if (n != -1) { bound -= n; } return n; } } public int available() throws IOException { return Math.min(bound, in.available()); } } /** * Input stream for reading chunked content data. */ private class ChunkedInputStream extends InputStream { private byte[] buf; private int pos = 0; private int lim = 0; ChunkedInputStream() {} public int read() throws IOException { byte[] b = new byte[1]; return (read(b, 0, 1) != -1) ? b[0] & 0xFF : -1; } public int read(byte[] b, int off, int len) throws IOException { while (pos >= lim) { refill(); } if (pos < 0) { return -1; } int n = Math.min(lim - pos, len); System.arraycopy(buf, pos, b, off, n); pos += n; return n; } public int available() throws IOException { while (pos >= lim) { refill(); } return (pos >= 0) ? (lim - pos) : 0; } private void refill() throws IOException { int newlim = 0; try { String line = readLine(in); StringTokenizer tok = new StringTokenizer(line, " ;\t"); newlim = Integer.parseInt(tok.nextToken(), 16); } catch (Exception ex) { throw new HttpParseException("error parsing chunk length"); } if (newlim < 0) { throw new HttpParseException("illegal chunk length"); } else if (newlim == 0) { pos = -1; } else { // REMIND: place upper limit on chunk length? if (buf == null || newlim > buf.length) { buf = new byte[newlim]; } for (int i = 0; i < newlim; ) { int n = in.read(buf, i, newlim - i); if (n < 0) { throw new EOFException("incomplete chunk"); } i += n; } String blank = readLine(in); if (blank == null || blank.length() > 0) { throw new HttpParseException("illegal chunk tail"); } pos = 0; lim = newlim; } } } }