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}