001/**
002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC
003 * and GZip files. (http://jwat.org/)
004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/)
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.jwat.common;
019
020import java.io.IOException;
021import java.io.PushbackInputStream;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027
028/**
029 * This class represents a recognized HTTP response or request header with optional payload.
030 *
031 * @author lbihanic, selghissassi, nicl
032 */
033public class HttpHeader extends PayloadWithHeaderAbstract {
034
035    /** Response header type. */
036    public static final int HT_RESPONSE = 1;
037
038    /** Request header type. */
039    public static final int HT_REQUEST = 2;
040
041    /** HTTP scheme. */
042    public static final String PROTOCOL_HTTP = "http";
043
044    /** HTTPS scheme. */
045    public static final String PROTOCOL_HTTPS = "https";
046
047    /** HTTP Version suffix. */
048    protected static final String HTTP_VERSION_SUFFIX = "HTTP/";
049
050    /** Content-type header name. */
051    protected static final String CONTENT_TYPE = "Content-Type".toUpperCase();
052
053    /** Header type of this object. */
054    public int headerType;
055
056    /*
057     * Request-Line.
058     */
059
060    /** HTTP Method. */
061    public String method;
062
063    /** HTTP Request-URI. */
064    public String requestUri;
065
066    /*
067     * Http-Version.
068     */
069
070    /** HTTP-Version. */
071    public String httpVersion;
072
073    /** HTTP-Version major. */
074    public Integer httpVersionMajor;
075
076    /** HTTP-Version minor. */
077    public Integer httpVersionMinor;
078
079    /*
080     * Status-Line.
081     */
082
083    /** HTTP Status-Code. */
084    public String statusCodeStr;
085
086    /** HTTP Status-Code. */
087    public Integer statusCode;
088
089    /** HTTP Reason-Phrase. */
090    public String reasonPhrase;
091
092    /*
093     * Header-Fields.
094     */
095
096    /** List of parsed header fields. */
097    protected List<HeaderLine> headerList = new LinkedList<HeaderLine>();
098
099    /** Map of parsed header fields. */
100    protected Map<String, HeaderLine> headerMap = new HashMap<String, HeaderLine>();
101
102    /** HTTP content Content-type. */
103    public String contentType;
104
105    /**
106     * Non public constructor.
107     */
108    protected HttpHeader() {
109    }
110
111    /**
112     * Boolean indicating whether a protocol is supported by this
113     * payload inspector.
114     * @param protocol protocol name
115     * @return true/false if the protocol is supported or not.
116     */
117    public static boolean isSupported(String protocol) {
118        return ((PROTOCOL_HTTP.equalsIgnoreCase(protocol)
119                || PROTOCOL_HTTPS.equalsIgnoreCase(protocol)));
120    }
121
122    /**
123     * Reads the HTTP protocol response or request and returns it as an object.
124     * It is important to understand that the maximum size of a parsed header
125     * is equal to the size of the PushbackInputStream's buffer!
126     * @param headerType HTTP header type to parse, request or response
127     * @param pbin payload input stream
128     * @param length payload length
129     * @param digestAlgorithm digest algorithm to use on payload or null if we
130     * don't want a digest of the payload
131     * @return <code>HttpResponse</code> based on the http headers
132     * @throws IOException if an error occur while processing http header.
133     */
134    public static HttpHeader processPayload(int headerType, ByteCountingPushBackInputStream pbin,
135                    long length, String digestAlgorithm) throws IOException {
136        if (headerType != HT_RESPONSE && headerType != HT_REQUEST) {
137            throw new IllegalArgumentException(
138                    "Invalid 'headerType' argument: " + headerType);
139        }
140        if (pbin == null) {
141            throw new IllegalArgumentException(
142                    "The inputstream 'pbin' is null");
143        }
144        if (length < 0) {
145            throw new IllegalArgumentException(
146                    "The 'length' is less than zero: " + length);
147        }
148        HttpHeader hh = new HttpHeader();
149        hh.headerType = headerType;
150        hh.in_pb = pbin;
151        hh.totalLength = length;
152        hh.digestAlgorithm = digestAlgorithm;
153        hh.diagnostics = new Diagnostics();
154        hh.initProcess();
155        return hh;
156    }
157
158    @Override
159    protected boolean readHeader(MaxLengthRecordingInputStream in, long payloadLength)
160                            throws IOException {
161        // TODO too low!
162        PushbackInputStream pbin = new PushbackInputStream(in, in_pb.getPushbackSize());
163        HeaderLineReader hlr = HeaderLineReader.getHeaderLineReader();
164        hlr.bNameValue = false;
165        hlr.encoding = HeaderLineReader.ENC_ISO8859_1;
166        hlr.bLWS = false;
167        hlr.bQuotedText = false;
168        hlr.bEncodedWords = false;
169        boolean bValidHttpHeader = false;
170        HeaderLine line = hlr.readLine(pbin);
171        int bfErrors = 0;
172        if (!hlr.bEof && line.type == HeaderLine.HLT_LINE && line.line != null && line.line.length() > 0) {
173            bfErrors = (line.bfErrors & ~HeaderLineReader.E_BIT_INVALID_SEPARATOR_CHAR);
174            if (headerType == HT_RESPONSE) {
175                bValidHttpHeader = isHttpStatusLineValid(line.line);
176            } else if (headerType == HT_REQUEST) {
177                bValidHttpHeader = isHttpRequestLineValid(line.line);
178            } else {
179                throw new IllegalStateException("Invalid headerType!");
180            }
181        }
182        hlr.bNameValue = true;
183        hlr.bLWS = true;
184        hlr.bQuotedText = true;
185        hlr.bEncodedWords = true;
186        HeaderLine tmpLine;
187        boolean bLoop = bValidHttpHeader;
188        while (bLoop) {
189            line = hlr.readLine(pbin);
190            bfErrors |= line.bfErrors;
191            if (!hlr.bEof) {
192                switch (line.type) {
193                case HeaderLine.HLT_HEADERLINE:
194                    //System.out.println(line.name);
195                    //System.out.println(line.value);
196                    if (CONTENT_TYPE.equals(line.name.toUpperCase())) {
197                        contentType = line.value;
198                    }
199                    // A HeaderLine object contains a list of additional lines.
200                    tmpLine = headerMap.get(line.name.toLowerCase());
201                    if (tmpLine == null) {
202                        headerMap.put(line.name.toLowerCase(), line);
203                    } else {
204                        tmpLine.lines.add(line);
205                    }
206                    headerList.add(line);
207                    break;
208                case HeaderLine.HLT_LINE:
209                    if (line.line.length() == 0) {
210                        bLoop = false;
211                    } else {
212                        // Errors reported by bfErrors.
213                    }
214                    break;
215                }
216            } else {
217                // Accept truncated http header if it is the length of the payload.
218                if ((bfErrors & HeaderLineReader.E_BIT_EOF) == 0 || in.record.size() != payloadLength) {
219                    /*
220                    System.out.println("Epic fail!");
221                    System.out.println(Integer.toBinaryString(hlr.bfErrors));
222                    System.out.println(new String(in.getRecording()));
223                    */
224                    bValidHttpHeader = false;
225                }
226                bLoop = false;
227            }
228        }
229        HeaderLineReader.report_error(bfErrors, diagnostics);
230        if (bValidHttpHeader) {
231            this.payloadLength = payloadLength - in.record.size();
232        }
233        return bValidHttpHeader;
234    }
235
236    /**
237     * Checks a HTTP Response Status-Line for validity.
238     * @param statusLine the Status-Line of the HTTP Response
239     * @return true/false based on whether the Status-Line is valid or not.
240     */
241    protected boolean isHttpStatusLineValid(String statusLine) {
242        int idx;
243        int prevIdx;
244        boolean bIsHttpStatusLineValid = (statusLine != null) && (statusLine.length() > 0);
245        if (bIsHttpStatusLineValid) {
246            idx = statusLine.indexOf(' ');
247            if (idx > 0) {
248                bIsHttpStatusLineValid = isHttpVersionValid(statusLine.substring(0, idx));
249            } else {
250                if (idx == -1) {
251                    httpVersion = statusLine;
252                }
253                bIsHttpStatusLineValid = false;
254            }
255            if (bIsHttpStatusLineValid) {
256                prevIdx = ++idx;
257                idx = statusLine.indexOf(' ', idx);
258                if (idx == -1) {
259                    idx = statusLine.length();
260                }
261                if (idx > prevIdx) {
262                    statusCodeStr = statusLine.substring(prevIdx, idx);
263                    try {
264                        statusCode = Integer.parseInt(statusCodeStr);
265                        if (statusCode < 100 || statusCode > 999) {
266                            bIsHttpStatusLineValid = false;
267                        }
268                    } catch(NumberFormatException e) {
269                        bIsHttpStatusLineValid = false;
270                    }
271                } else {
272                    bIsHttpStatusLineValid = false;
273                }
274                if (bIsHttpStatusLineValid) {
275                    if (idx < statusLine.length()) {
276                        ++idx;
277                        reasonPhrase = statusLine.substring(idx);
278                    }
279                }
280            }
281        }
282        return bIsHttpStatusLineValid;
283    }
284
285    /**
286     * Checks a HTTP-Version string for validity.
287     * @param versionString the HTTP-Version of the HTTP header
288     * @return true/false based on whether the HTTP-Version is valid or not.
289     */
290    protected boolean isHttpVersionValid(String versionString) {
291        int idx;
292        httpVersion = versionString;
293        boolean bIsHttpVersionValid = versionString.startsWith(HTTP_VERSION_SUFFIX);
294        if (bIsHttpVersionValid) {
295            idx = versionString.indexOf('.', HTTP_VERSION_SUFFIX.length());
296            if (idx > 0) {
297                try {
298                    httpVersionMajor = Integer.parseInt(versionString.substring(HTTP_VERSION_SUFFIX.length(), idx));
299                    if (httpVersionMajor < 0) {
300                        bIsHttpVersionValid = false;
301                    }
302                } catch (NumberFormatException e) {
303                    bIsHttpVersionValid = false;
304                }
305                try {
306                    httpVersionMinor = Integer.parseInt(versionString.substring(idx + 1));
307                    if (httpVersionMinor < 0) {
308                        bIsHttpVersionValid = false;
309                    }
310                } catch (NumberFormatException e) {
311                    bIsHttpVersionValid = false;
312                }
313            } else {
314                bIsHttpVersionValid = false;
315            }
316        } else if (versionString.equals("HTTP")) {
317            // IA-local change: be lenient with broken version field.
318            // See https://webarchive.jira.com/browse/WWM-160
319            httpVersionMajor = 1;
320            httpVersionMinor = 0;
321            bIsHttpVersionValid = true;
322        }
323        return bIsHttpVersionValid;
324    }
325
326    /**
327     * Checks a HTTP Request Request-Line for validity.
328     * @param requestLine the Request-Line of the HTTP Request
329     * @return true/false based on whether the Request-Line is valid or not.
330     */
331    protected boolean isHttpRequestLineValid(String requestLine) {
332        int idx;
333        int prevIdx;
334        boolean bIsHttpRequestLineValid = (requestLine != null) && (requestLine.length() > 0);
335        if (bIsHttpRequestLineValid) {
336            idx = requestLine.indexOf(' ');
337            if (idx > 0) {
338                method = requestLine.substring(0, idx);
339            } else {
340                if (idx == -1) {
341                    method = requestLine;
342                }
343                bIsHttpRequestLineValid = false;
344            }
345            if (bIsHttpRequestLineValid) {
346                prevIdx = ++idx;
347                idx = requestLine.indexOf(' ', idx);
348                if (idx > prevIdx) {
349                    requestUri = requestLine.substring(prevIdx, idx);
350                } else {
351                    if (idx == -1) {
352                        requestUri = requestLine.substring(prevIdx);
353                    }
354                    bIsHttpRequestLineValid = false;
355                }
356                if (bIsHttpRequestLineValid) {
357                    ++idx;
358                    bIsHttpRequestLineValid = isHttpVersionValid(requestLine.substring(idx));
359                }
360            }
361        }
362        return bIsHttpRequestLineValid;
363    }
364
365    @Override
366    public List<HeaderLine> getHeaderList() {
367        return Collections.unmodifiableList(headerList);
368    }
369
370    @Override
371    public HeaderLine getHeader(String field) {
372        if (field != null && field.length() > 0) {
373            return headerMap.get(field.toLowerCase());
374        } else {
375            return null;
376        }
377    }
378
379    /**
380     * Result-Code string getter
381     * @return the ResultCode
382     */
383    public String getProtocolStatusCodeStr() {
384        return statusCodeStr;
385    }
386
387    /**
388     * Result-Code integer getter
389     * @return the ResultCode
390     */
391    public Integer getProtocolStatusCode() {
392        return statusCode;
393    }
394
395    /**
396     * protocolVersion getter
397     * @return the protocolVersion
398     */
399    public String getProtocolVersion() {
400        return httpVersion;
401    }
402
403    /**
404     * Content-Type getter
405     * @return the Content-Type
406     */
407    public String getProtocolContentType() {
408        return contentType;
409    }
410
411    @Override
412    public String toString() {
413        StringBuilder builder = new StringBuilder(256);
414        builder.append("\nHttpHeader : [\n");
415        if (statusCode != null) {
416            builder.append(", HttpResultCode: ")
417                .append(statusCode);
418        }
419        if (httpVersion != null) {
420            builder.append(", HttpProtocolVersion: ")
421                .append(httpVersion);
422        }
423        if (contentType != null) {
424            builder.append(", HttpContentType: ")
425                .append(contentType);
426        }
427        builder.append("]\n");
428        return builder.toString();
429    }
430
431}