/**************************************************************************** * * * cryptlib HTTP Read Routines * * Copyright Peter Gutmann 1998-2006 * * * ****************************************************************************/ #include #include #if defined( INC_ALL ) #include "crypt.h" #include "http.h" #include "misc_rw.h" #else #include "crypt.h" #include "io/http.h" #include "misc/misc_rw.h" #endif /* Compiler-specific includes */ #ifdef USE_HTTP /* The various HTTP header types that we can process */ typedef enum { HTTP_HEADER_NONE, HTTP_HEADER_HOST, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_TRANSFER_ENCODING, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_CONTENT_TRANSFER_ENCODING, HTTP_HEADER_TRAILER, HTTP_HEADER_CONNECTION, HTTP_HEADER_WARNING, HTTP_HEADER_LOCATION, HTTP_HEADER_EXPECT, HTTP_HEADER_LAST } HTTP_HEADER_TYPE; /* HTTP header parsing information. Note that the first letter of the header string must be uppercase for the case-insensitive quick match */ typedef struct { const char FAR_BSS *headerString;/* Header string */ const int headerStringLen; /* Length of header string */ const HTTP_HEADER_TYPE headerType; /* Type corresponding to header string */ } HTTP_HEADER_PARSE_INFO; static const HTTP_HEADER_PARSE_INFO FAR_BSS httpHeaderParseInfo[] = { { "Host:", 5, HTTP_HEADER_HOST }, { "Content-Length:", 15, HTTP_HEADER_CONTENT_LENGTH }, { "Content-Type:", 13, HTTP_HEADER_CONTENT_TYPE }, { "Transfer-Encoding:", 18, HTTP_HEADER_TRANSFER_ENCODING }, { "Content-Encoding:", 17, HTTP_HEADER_CONTENT_ENCODING }, { "Content-Transfer-Encoding:", 26, HTTP_HEADER_CONTENT_TRANSFER_ENCODING }, { "Trailer:", 8, HTTP_HEADER_TRAILER }, { "Connection:", 11, HTTP_HEADER_CONNECTION }, { "NnCoection:", 11, HTTP_HEADER_CONNECTION }, { "Cneonction:", 11, HTTP_HEADER_CONNECTION }, /* The bizarre spellings are for buggy NetApp NetCache servers, which unfortunately are widespread enough that we need to provide special-case handling for them. For the second mis-spelling we have to capitalise the first letter for our use since we compare the uppercase form for a quick match */ { "Warning:", 8, HTTP_HEADER_WARNING }, { "Location:", 9, HTTP_HEADER_LOCATION }, { "Expect:", 7, HTTP_HEADER_EXPECT }, { NULL, 0, HTTP_HEADER_NONE } }; /* HTTP error/warning messages. The mapped status for 30x redirects is somewhat special-case, see the comment in readResponseHeader() for details. This table also contains known non-HTTP codes in the expectation that, when used as a general-purpose substrate, it'll be pressed into use in all sorts of situations */ typedef struct { const int httpStatus; /* Numeric status value */ const char FAR_BSS *httpStatusString; /* String status value */ const char FAR_BSS *httpErrorString; /* Text description of status */ const int status; /* Equivalent cryptlib status */ } HTTP_STATUS_INFO; static const HTTP_STATUS_INFO FAR_BSS httpStatusInfo[] = { { 100, "100", "Continue", OK_SPECIAL }, { 101, "101", "Switching Protocols", CRYPT_ERROR_READ }, { 110, "110", "Warning: Response is stale", CRYPT_OK }, { 111, "111", "Warning: Revalidation failed", CRYPT_OK }, { 112, "112", "Warning: Disconnected operation", CRYPT_OK }, { 113, "113", "Warning: Heuristic expiration", CRYPT_OK }, { 199, "199", "Warning: Miscellaneous warning", CRYPT_OK }, { 200, "200", "OK", CRYPT_OK }, { 201, "201", "Created", CRYPT_ERROR_READ }, { 202, "202", "Accepted", CRYPT_ERROR_READ }, { 203, "203", "Non-Authoritative Information", CRYPT_OK }, { 204, "204", "No Content", CRYPT_ERROR_READ }, { 205, "205", "Reset Content", CRYPT_ERROR_READ }, { 206, "206", "Partial Content", CRYPT_ERROR_READ }, { 214, "214", "Warning: Transformation applied", CRYPT_OK }, { 250, "250", "RTSP: Low on Storage Space", CRYPT_OK }, { 299, "299", "Warning: Miscellaneous persistent warning", CRYPT_OK }, { 300, "300", "Multiple Choices", CRYPT_ERROR_READ }, { 301, "301", "Moved Permanently", OK_SPECIAL }, { 302, "302", "Moved Temporarily/Found", OK_SPECIAL }, { 303, "303", "See Other", CRYPT_ERROR_READ }, { 304, "304", "Not Modified", CRYPT_ERROR_READ }, { 305, "305", "Use Proxy", CRYPT_ERROR_READ }, { 306, "306", "Unused/obsolete", CRYPT_ERROR_READ }, { 307, "307", "Temporary Redirect", OK_SPECIAL }, { 400, "400", "Bad Request", CRYPT_ERROR_READ }, { 401, "401", "Unauthorized", CRYPT_ERROR_PERMISSION }, { 402, "402", "Payment Required", CRYPT_ERROR_READ }, { 403, "403", "Forbidden", CRYPT_ERROR_PERMISSION }, { 404, "404", "Not Found", CRYPT_ERROR_NOTFOUND }, { 405, "405", "Method Not Allowed", CRYPT_ERROR_NOTAVAIL }, { 406, "406", "Not Acceptable", CRYPT_ERROR_PERMISSION }, { 407, "407", "Proxy Authentication Required", CRYPT_ERROR_PERMISSION }, { 408, "408", "Request Time-out", CRYPT_ERROR_READ }, { 409, "409", "Conflict", CRYPT_ERROR_READ }, { 410, "410", "Gone", CRYPT_ERROR_NOTFOUND }, { 411, "411", "Length Required", CRYPT_ERROR_READ }, { 412, "412", "Precondition Failed", CRYPT_ERROR_READ }, { 413, "413", "Request Entity too Large", CRYPT_ERROR_OVERFLOW }, { 414, "414", "Request-URI too Large", CRYPT_ERROR_OVERFLOW }, { 415, "415", "Unsupported Media Type", CRYPT_ERROR_READ }, { 416, "416", "Requested range not satisfiable", CRYPT_ERROR_READ }, { 417, "417", "Expectation Failed", CRYPT_ERROR_READ }, { 426, "426", "Upgrade Required", CRYPT_ERROR_READ }, { 451, "451", "RTSP: Parameter not Understood", CRYPT_ERROR_BADDATA }, { 452, "452", "RTSP: Conference not Found", CRYPT_ERROR_NOTFOUND }, { 453, "453", "RTSP: Not enough Bandwidth", CRYPT_ERROR_NOTAVAIL }, { 454, "454", "RTSP: Session not Found", CRYPT_ERROR_NOTFOUND }, { 455, "455", "RTSP: Method not Valid in this State", CRYPT_ERROR_NOTAVAIL }, { 456, "456", "RTSP: Header Field not Valid for Resource", CRYPT_ERROR_NOTAVAIL }, { 457, "457", "RTSP: Invalid Range", CRYPT_ERROR_READ }, { 458, "458", "RTSP: Parameter is Read-Only", CRYPT_ERROR_PERMISSION }, { 459, "459", "RTSP: Aggregate Operation not Allowed", CRYPT_ERROR_PERMISSION }, { 460, "460", "RTSP: Only Aggregate Operation Allowed", CRYPT_ERROR_PERMISSION }, { 461, "461", "RTSP: Unsupported Transport", CRYPT_ERROR_NOTAVAIL }, { 462, "462", "RTSP: Destination Unreachable", CRYPT_ERROR_OPEN }, { 500, "500", "Internal Server Error", CRYPT_ERROR_READ }, { 501, "501", "Not Implemented", CRYPT_ERROR_NOTAVAIL }, { 502, "502", "Bad Gateway", CRYPT_ERROR_READ }, { 503, "503", "Service Unavailable", CRYPT_ERROR_NOTAVAIL }, { 504, "504", "Gateway Time-out", CRYPT_ERROR_TIMEOUT }, { 505, "505", "HTTP Version not supported", CRYPT_ERROR_READ }, { 510, "510", "HTTP-Ext: Not Extended", CRYPT_ERROR_READ }, { 551, "551", "RTSP: Option not supported", CRYPT_ERROR_READ }, { 0, NULL, "Unrecognised HTTP status condition", CRYPT_ERROR_READ }, { 0, NULL, "Unrecognised HTTP status condition", CRYPT_ERROR_READ } }; /* HTTP header parsing information as used by readHeaderLines() */ typedef struct { /* Returned status information: The body content-length, the HTTP error status (if there is one), and general flags information. The flags parameter is used as both an input and an output parameter */ int contentLength; /* HTTP body content length */ int httpStatus; /* HTTP error status, if an HTTP error occurs */ int flags; /* General flags */ /* Range-checking information: The minimum and maximum allowable content-length value */ int minContentLength, maxContentLength; } HTTP_HEADER_INFO; #define initHeaderInfo( headerInfo, minLength, maxLength, hdrFlags ) \ memset( headerInfo, 0, sizeof( HTTP_HEADER_INFO ) ); \ ( headerInfo )->flags = ( hdrFlags ); \ ( headerInfo )->minContentLength = ( minLength ); \ ( headerInfo )->maxContentLength = ( maxLength ); /**************************************************************************** * * * Utility Functions * * * ****************************************************************************/ /* Callback function used by readTextLine() to read characters from a stream. When reading text data over a network we don't know how much more data is to come so we have to read a byte at a time looking for an EOL. In addition we can't use the simple optimisation of reading two bytes at a time because some servers only send a LF even though the spec requires a CRLF. This is horribly inefficient but is pretty much eliminated through the use of opportunistic read-ahead buffering */ static int readCharFunction( void *streamPtr ) { STREAM *stream = streamPtr; BYTE ch; int status; status = stream->bufferedTransportReadFunction( stream, &ch, 1, TRANSPORT_FLAG_NONE ); return( cryptStatusError( status ) ? status : ch ); } /* Skip whitespace/non-whitespace in a line of text. We only need to check for spaces as whitespace since it's been canonicalised when it was read */ static int skipWhitespace( const char *data, const int dataLength ) { int i; assert( isReadPtr( data, dataLength ) ); for( i = 0; i < dataLength && data[ i ] == ' '; i++ ); return( ( i < dataLength ) ? i : CRYPT_ERROR ); } static int skipNonWhitespace( const char *data, const int dataLength ) { int i; assert( isReadPtr( data, dataLength ) ); /* This differs slightly from skipWhitespace() in that EOL is also counted as whitespace, so there's never an error condition unless we don't find anything at all */ for( i = 0; i < dataLength && data[ i ] != ' '; i++ ); return( i > 0 ? i : CRYPT_ERROR ); } /* Decode a hex nibble */ static int getNibble( const char srcCh ) { int ch; ch = toLower( srcCh ); if( !isXDigit( ch ) ) return( CRYPT_ERROR_BADDATA ); return( ( ch <= '9' ) ? ch - '0' : ch - ( 'a' - 10 ) ); } /* Decode a string as per RFC 1866 (although the list of characters that need to be escaped is itself given in RFC 2396). Characters that are permitted/not permitted are: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ x..x.xx....x...xxxxxxxxxxxx.xxxxx Because of this it's easier to check for the most likely permitted characters (alphanumerics), and then to check for any special-case chars */ static int decodeRFC1866( char *buffer, const int bufSize ) { int srcIndex = 0, destIndex = 0; assert( isWritePtr( buffer, bufSize ) ); while( srcIndex < bufSize ) { int chLo, chHi, ch; /* If it's not an escape, just copy it straight over. The input has already been sanitised when it was read so there's no need to perform another check here */ if( buffer[ srcIndex ] != '%' ) { buffer[ destIndex++ ] = buffer[ srcIndex++ ]; continue; } srcIndex++; /* Skip '%' */ /* Decode the escaped character */ if( bufSize - srcIndex < 2 ) return( CRYPT_ERROR_BADDATA ); chHi = getNibble( buffer[ srcIndex++ ] ); chLo = getNibble( buffer[ srcIndex++ ] ); if( cryptStatusError( chHi ) || cryptStatusError( chLo ) ) return( CRYPT_ERROR_BADDATA ); ch = ( chHi << 4 ) | chLo; if( !isPrint( ch ) ) /* It's a special-case/control character of some kind, report it as an error. This gets rid of things like nulls (treated as string terminators by some functions) and CR/LF line terminators, which can be embedded into strings to turn a single line of supplied text into multi-line responses containing user-controlled type : value pairs (in other words they allow user data to be injected into the control channel) */ return( CRYPT_ERROR_BADDATA ); buffer[ destIndex++ ] = ch; } /* If we've processed an escape sequence (causing the data to change size), tell the caller the new length, otherwise tell them that nothing's changed */ return( ( destIndex < srcIndex ) ? destIndex : OK_SPECIAL ); } /* Convert a hex ASCII string used with chunked encoding into a numeric value */ static int getChunkLength( const char *data, const int dataLength ) { int i, chunkLength = 0, length = dataLength; assert( isReadPtr( data, dataLength ) ); /* Chunk size information can have extensions tacked onto it following a ';', strip these before we start */ for( i = 0; i < length; i++ ) { if( data[ i ] == ';' ) { /* Move back to the end of the string that precedes the ';' */ while( i > 0 && data[ i - 1 ] == ' ' ) i--; length = i; /* Adjust length and force loop exit */ } } /* The other side shouldn't be sending us more than 64K of data, given that what we're expecting is a short PKI message */ if( length < 1 || length > 4 ) return( CRYPT_ERROR_BADDATA ); /* Walk down the string converting hex characters into their numeric values */ for( i = 0; i < length; i++ ) { const int ch = getNibble( data[ i ] ); if( cryptStatusError( ch ) ) return( CRYPT_ERROR_BADDATA ); chunkLength = ( chunkLength << 4 ) | ch; } if( chunkLength < 0 || chunkLength > MAX_INTLENGTH ) return( CRYPT_ERROR_BADDATA ); return( chunkLength ); } /* Convert a decimal ASCII string in the range 'd' ... 'ddddddd' into a numeric value */ static int getNumericValue( const char *data, const int dataLength ) { char numericBuffer[ 8 + 8 ]; int value; assert( isReadPtr( data, dataLength ) ); if( dataLength < 1 || dataLength > 7 ) return( CRYPT_ERROR_BADDATA ); memcpy( numericBuffer, data, dataLength ); numericBuffer[ dataLength ] = '\0'; value = atoi( numericBuffer ); return( ( value <= 0 || value > MAX_INTLENGTH ) ? \ CRYPT_ERROR_BADDATA : value ); } /* Exit with extended error information after a readTextLine() call */ static int retTextLineError( STREAM *stream, const int status, const int textLineError, const char *format, const int value ) { /* If the extended error information came up from a lower level than readCharFunction(), pass it on up to the caller */ if( !textLineError ) return( status ); /* Extend the readTextLine()-level error information with higher-level detail. This allows us to provide a more useful error report ("Problem with line x") than just the rather low-level view provided by readTextLine() ("Invalid character 0x8F at position 12") */ retExtStr( STREAM_ERRINFO, status, stream->errorInfo->errorString, format, value ); } /* Send an HTTP error message. This function is somewhat unusually placed with the read functions because it's used by both read and write code but needs access to the HTTP status decoding table, which is part of the read code */ int sendHTTPError( STREAM *stream, char *headerBuffer, const int headerBufMaxLen, const int httpStatus ) { STREAM headerStream; const char *statusString = "400"; const char *errorString = "Bad Request"; int length, i; assert( isWritePtr( stream, sizeof( STREAM ) ) ); assert( isWritePtr( headerBuffer, headerBufMaxLen ) ); assert( headerBufMaxLen >= 256 ); /* Find the HTTP error string that corresponds to the HTTP status value */ for( i = 0; httpStatusInfo[ i ].httpStatus > 0 && \ httpStatusInfo[ i ].httpStatus != httpStatus && \ i < FAILSAFE_ARRAYSIZE( httpStatusInfo, HTTP_STATUS_INFO ); i++ ); if( i >= FAILSAFE_ARRAYSIZE( httpStatusInfo, HTTP_STATUS_INFO ) ) retIntError(); if( httpStatusInfo[ i ].httpStatus ) { statusString = httpStatusInfo[ i ].httpStatusString; errorString = httpStatusInfo[ i ].httpErrorString; } /* Send the error message to the peer */ sMemOpen( &headerStream, headerBuffer, headerBufMaxLen ); swrite( &headerStream, isHTTP10( stream ) ? "HTTP/1.0 " : \ "HTTP/1.1 ", 9 ); swrite( &headerStream, statusString, strlen( statusString ) ); sputc( &headerStream, ' ' ); swrite( &headerStream, errorString, strlen( errorString ) ); swrite( &headerStream, "\r\n\r\n", 4 ); assert( sStatusOK( &headerStream ) ); length = stell( &headerStream ); sMemDisconnect( &headerStream ); return( sendHTTPData( stream, headerBuffer, length, TRANSPORT_FLAG_FLUSH ) ); } /**************************************************************************** * * * URI Parsing Functions * * * ****************************************************************************/ /* Information needed to parse a URI sub-segment: The character that ends a segment and an optional alternative segment-end character, and the minimum and maximum permitted segment size */ typedef struct { const char segmentEndChar, altSegmentEndChar; const int segmentMinLength, segmentMaxLength; } URI_PARSE_INFO; /* Get the length of a sub-segment of a URI */ static int getUriSegmentLength( const char *data, const int dataLength, const URI_PARSE_INFO *uriParseInfo, BOOLEAN *altDelimiterFound ) { const int maxLength = min( dataLength, uriParseInfo->segmentMaxLength ); int i; assert( isReadPtr( data, dataLength ) ); assert( isReadPtr( uriParseInfo, sizeof( URI_PARSE_INFO ) ) ); assert( uriParseInfo->segmentMinLength >= 0 && \ uriParseInfo->segmentMinLength < \ uriParseInfo->segmentMaxLength && \ uriParseInfo->segmentMaxLength <= 1024 ); assert( ( uriParseInfo->altSegmentEndChar == '\0' && \ altDelimiterFound == NULL ) || \ ( uriParseInfo->altSegmentEndChar > '\0' && \ isWritePtr( altDelimiterFound, sizeof( BOOLEAN ) ) ) ); /* Clear return value */ if( altDelimiterFound != NULL ) *altDelimiterFound = FALSE; /* Parse the current query sub-segment */ for( i = 0; i < maxLength; i++ ) { if( data[ i ] == uriParseInfo->segmentEndChar ) break; if( uriParseInfo->altSegmentEndChar > '\0' && \ data[ i ] == uriParseInfo->altSegmentEndChar ) { *altDelimiterFound = TRUE; break; } } /* Make sure that we both got enough data and that we didn't run out of data */ if( i < uriParseInfo->segmentMinLength || i >= maxLength ) return( CRYPT_ERROR_BADDATA ); return( i ); } /* Parse a URI of the form "* '?' attribute '=' value [ '&' ... ] ", returning the parsed form to the caller. This function needs to return two length values since it decodes the URI string according to RFC 1866, which means that its length can change. So as its standard return value it returns the number of chars consumed, but it also returns the new length of the input as a by-reference parameter */ static int parseUriInfo( char *data, const int dataInLength, int *dataOutLength, HTTP_URI_INFO *uriInfo ) { static const URI_PARSE_INFO locationParseInfo = \ { '?', '\0', 1, CRYPT_MAX_TEXTSIZE }; static const URI_PARSE_INFO attributeParseInfo = \ { '=', '\0', 3, CRYPT_MAX_TEXTSIZE }; static const URI_PARSE_INFO valueParseInfo = \ { ' ', '&', 3, CRYPT_MAX_TEXTSIZE }; static const URI_PARSE_INFO extraParseInfo = \ { ' ', '\0', 1, CRYPT_MAX_TEXTSIZE }; BOOLEAN altDelimiterFound; const char *bufPtr = data; int length = dataInLength, segmentLength, parsedLength, i, status; assert( isWritePtr( data, dataInLength ) ); assert( isWritePtr( dataOutLength, sizeof( int ) ) ); assert( isWritePtr( uriInfo, sizeof( HTTP_URI_INFO ) ) ); /* Clear return values */ memset( uriInfo, 0, sizeof( HTTP_URI_INFO ) ); *dataOutLength = 0; /* Decode the URI text. Since there can be multiple nested levels of encoding, we keep iteratively decoding in-place until either decodeRFC1866() cries Uncle or we hit the sanity-check limit */ for( i = 0; i < FAILSAFE_ITERATIONS_SMALL; i++ ) { status = decodeRFC1866( data, length ); if( cryptStatusError( status ) ) { if( status == OK_SPECIAL ) /* There's been no further change in the data, exit */ break; return( CRYPT_ERROR_BADDATA ); } length = status; /* Record the new length of the decoded data */ } if( i >= FAILSAFE_ITERATIONS_SMALL ) { /* Sanity-check limit exceeded. This could be either data error or an internal error, since we can't automatically tell which we report it as a data error */ return( CRYPT_ERROR_BADDATA ); } *dataOutLength = length; /* We need to get at least 'x?xxx=xxx' */ if( length < 9 ) return( CRYPT_ERROR_BADDATA ); /* Parse a URI of the form "* '?' attribute '=' value [ '&' ... ] ". The URI is followed by the HTTP ID, so we know that it always has to end on a space; running out of input is an error */ segmentLength = getUriSegmentLength( bufPtr, length, &locationParseInfo, NULL ); if( cryptStatusError( segmentLength ) ) return( segmentLength ); memcpy( uriInfo->location, bufPtr, segmentLength ); uriInfo->locationLen = segmentLength; bufPtr += segmentLength + 1; /* Skip delimiter */ length -= segmentLength + 1; parsedLength = segmentLength + 1; segmentLength = getUriSegmentLength( bufPtr, length, &attributeParseInfo, NULL ); if( cryptStatusError( segmentLength ) ) return( segmentLength ); memcpy( uriInfo->attribute, bufPtr, segmentLength ); uriInfo->attributeLen = segmentLength; bufPtr += segmentLength + 1; /* Skip delimiter */ length -= segmentLength + 1; parsedLength += segmentLength + 1; segmentLength = getUriSegmentLength( bufPtr, length, &valueParseInfo, &altDelimiterFound ); if( cryptStatusError( segmentLength ) ) return( segmentLength ); memcpy( uriInfo->value, bufPtr, segmentLength ); uriInfo->valueLen = segmentLength; bufPtr += segmentLength + 1; /* Skip delimiter */ length -= segmentLength + 1; parsedLength += segmentLength + 1; if( altDelimiterFound ) { segmentLength = getUriSegmentLength( bufPtr, length, &extraParseInfo, NULL ); if( cryptStatusError( segmentLength ) ) return( segmentLength ); parsedLength += segmentLength + 1; } return( parsedLength ); } /* Check an "HTTP 1.x" ID string. No PKI client should be sending us an 0.9 ID, so we only allow 1.x */ static int checkHTTPID( const char *data, const int dataLength, STREAM *stream ) { assert( isReadPtr( data, dataLength ) ); assert( isWritePtr( stream, sizeof( STREAM ) ) ); if( dataLength < 8 || strCompare( data, "HTTP/1.", 7 ) ) return( CRYPT_ERROR_BADDATA ); if( data[ 7 ] == '0' ) stream->flags |= STREAM_NFLAG_HTTP10; else if( data[ 7 ] != '1' ) return( CRYPT_ERROR_BADDATA ); return( 8 ); } /**************************************************************************** * * * HTTP Header Processing * * * ****************************************************************************/ /* Read an HTTP status code. Some status values are warnings only and don't return an error status */ static int readHTTPStatus( const char *data, const int dataLength, int *httpStatus, void *errorInfo ) { const HTTP_STATUS_INFO *httpStatusPtr; const BOOLEAN isResponseStatus = ( httpStatus != NULL ) ? TRUE : FALSE; char thirdChar; int value, valueLength, remainderLength, offset, i; assert( isReadPtr( data, dataLength ) ); assert( httpStatus == NULL || \ isWritePtr( httpStatus, sizeof( int ) ) ); assert( isWritePtr( errorInfo, sizeof( STREAM ) ) ); /* Clear return value */ if( httpStatus != NULL ) *httpStatus = CRYPT_OK; /* Process the numeric HTTP status code and translate it into a cryptlib equivalent. Most of the HTTP codes don't have any meaning in a cryptlib context, so they're mapped to a generic CRYPT_ERROR_READ by the HTTP status decoding table */ if( dataLength < 3 || !isDigit( *data ) ) retExt( STREAM_ERRINFO_VOID, CRYPT_ERROR_BADDATA, "Invalid/missing HTTP %sstatus code", isResponseStatus ? "response " : "" ); thirdChar = data[ 2 ]; for( i = 0; httpStatusInfo[ i ].httpStatus != 0 && \ i < FAILSAFE_ARRAYSIZE( httpStatusInfo, HTTP_STATUS_INFO ); i++ ) { /* We check the third digit (the one most likely to be different) for a mismatch to avoid a large number of calls to the string- compare function */ if( httpStatusInfo[ i ].httpStatusString[ 2 ] == thirdChar && \ !strCompare( data, httpStatusInfo[ i ].httpStatusString, 3 ) ) break; } if( i >= FAILSAFE_ARRAYSIZE( httpStatusInfo, HTTP_STATUS_INFO ) ) retIntError(); httpStatusPtr = &httpStatusInfo[ i ]; /* Find the end of the numeric value. Note that this only finds the end of the value for a correctly-formatted string (for example it will report '123xyz' as having a length of 6), since it's up to getNumericValue() to check the validity of the numeric value. All that we're doing here is telling getNumericValue() which part of the data it has to check */ valueLength = skipNonWhitespace( data, dataLength ); if( valueLength <= 0 ) retExt( STREAM_ERRINFO_VOID, CRYPT_ERROR_BADDATA, "Missing HTTP %sstatus code", isResponseStatus ? "response " : "" ); /* Process the numeric status code */ value = getNumericValue( data, valueLength ); if( cryptStatusError( value ) ) retExt( STREAM_ERRINFO_VOID, CRYPT_ERROR_BADDATA, "Invalid/missing HTTP %sstatus code", isResponseStatus ? "response " : "" ); if( httpStatus != NULL ) *httpStatus = value; /* If we're doing a status read from something in a header line rather than an HTTP response (for example a Warning line, which only requires a status code but no status message), we're done */ if( !isResponseStatus ) return( CRYPT_OK ); /* We're doing a status read from an HTTP response, make sure that there's status text present alongside the status code */ remainderLength = dataLength - valueLength; if( remainderLength <= 0 || \ ( offset = skipWhitespace( data + valueLength, remainderLength ) ) < 0 || \ dataLength - offset < 1 ) retExt( STREAM_ERRINFO_VOID, CRYPT_ERROR_BADDATA, "Missing HTTP response status text" ); /* If it's a special-case condition such as a redirect, tell the caller to handle it specially */ if( httpStatusPtr->status == OK_SPECIAL ) return( OK_SPECIAL ); /* If it's an error condition, return extended error info (from the information we have, not from any externally-supplied message) */ if( httpStatusPtr->status != CRYPT_OK ) { assert( httpStatusPtr->httpStatusString != NULL ); /* Catch oddball errors in debug version */ retExt( STREAM_ERRINFO_VOID, httpStatusPtr->status, "HTTP response status: %s", httpStatusPtr->httpErrorString ); } return( CRYPT_OK ); } /* Process an HTTP header line looking for anything that we can handle */ static int processHeaderLine( const char *data, const int dataLength, HTTP_HEADER_TYPE *headerType, void *errorInfo, const int errorLineNo ) { const HTTP_HEADER_PARSE_INFO *headerParseInfoPtr = NULL; const char firstChar = toUpper( *data ); int processedLength, dataLeft, i; assert( isReadPtr( data, dataLength ) ); assert( isWritePtr( headerType, sizeof( HTTP_HEADER_TYPE ) ) ); assert( isWritePtr( errorInfo, sizeof( STREAM ) ) ); assert( errorLineNo > 0 && errorLineNo < 1000 ); /* Clear return value */ *headerType = HTTP_HEADER_NONE; /* Look for a header line that we recognise */ for( i = 0; httpHeaderParseInfo[ i ].headerString != NULL && \ i < FAILSAFE_ARRAYSIZE( httpHeaderParseInfo, HTTP_HEADER_PARSE_INFO ); i++ ) { if( httpHeaderParseInfo[ i ].headerString[ 0 ] == firstChar && \ dataLength >= httpHeaderParseInfo[ i ].headerStringLen && \ !strCompare( data, httpHeaderParseInfo[ i ].headerString, \ httpHeaderParseInfo[ i ].headerStringLen ) ) { headerParseInfoPtr = &httpHeaderParseInfo[ i ]; break; } } if( i >= FAILSAFE_ARRAYSIZE( httpHeaderParseInfo, HTTP_HEADER_PARSE_INFO ) ) retIntError(); if( headerParseInfoPtr == NULL ) /* It's nothing that we can handle, exit */ return( 0 ); processedLength = headerParseInfoPtr->headerStringLen; /* Make sure that there's an attribute value present */ dataLeft = dataLength - processedLength; if( dataLeft > 0 ) { const int extraLength = \ skipWhitespace( data + processedLength, dataLeft ); if( extraLength > 0 ) { /* We skipped some whitespace before the attribute value, adjust the consumed/remaining byte counts */ dataLeft -= extraLength; processedLength += extraLength; } else /* If there was a problem, make sure that we fail the following check */ if( extraLength < 0 ) dataLeft = CRYPT_ERROR; } if( dataLeft < 1 ) retExt( STREAM_ERRINFO_VOID, CRYPT_ERROR_BADDATA, "Missing HTTP header value for '%s' token, line %d", headerParseInfoPtr->headerString, errorLineNo ); /* Tell the caller what we found */ *headerType = headerParseInfoPtr->headerType; return( processedLength ); } /* Read the first line in an HTTP response header */ int readFirstHeaderLine( STREAM *stream, char *dataBuffer, const int dataMaxLength, int *httpStatus ) { BOOLEAN textDataError; int length, processedLength, dataLeft; assert( isWritePtr( stream, sizeof( STREAM ) ) ); assert( isWritePtr( dataBuffer, dataMaxLength ) ); assert( isWritePtr( httpStatus, sizeof( int ) ) ); /* Clear return value */ *httpStatus = CRYPT_OK; /* Read the header and check for an HTTP ID */ length = readTextLine( readCharFunction, stream, dataBuffer, dataMaxLength, &textDataError ); if( cryptStatusError( length ) || length < 8 ) return( retTextLineError( stream, length, textDataError, "Invalid HTTP header line 1: ", 0 ) ); processedLength = checkHTTPID( dataBuffer, length, stream ); if( cryptStatusError( processedLength ) ) retExt( STREAM_ERRINFO, cryptStatusError( processedLength ) ? \ processedLength : CRYPT_ERROR_BADDATA, \ "Invalid HTTP ID/version" ); dataLeft = length - processedLength; /* Skip the whitespace between the HTTP ID and status info */ if( dataLeft > 0 ) { const int extraLength = \ skipWhitespace( dataBuffer + processedLength, dataLeft ); if( extraLength > 0 ) { /* We skipped some whitespace before the HTTP status info, adjust the consumed/remaining byte counts */ dataLeft -= extraLength; processedLength += extraLength; } else /* If there was a problem, make sure that we fail the following check */ if( extraLength < 0 ) dataLeft = CRYPT_ERROR; } if( dataLeft < 1 ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Missing HTTP status code, line 1" ); /* Read the HTTP status info */ return( readHTTPStatus( dataBuffer + processedLength, dataLeft, httpStatus, stream ) ); } /* Read the remaining HTTP header lines after the first one */ int readHeaderLines( STREAM *stream, char *lineBuffer, const int lineBufMaxLen, HTTP_HEADER_INFO *headerInfo ) { BOOLEAN seenHost = FALSE, seenLength = FALSE; int contentLength = 0, lineCount, status; assert( isWritePtr( stream, sizeof( STREAM ) ) ); assert( isWritePtr( lineBuffer, lineBufMaxLen ) ); assert( isWritePtr( headerInfo, sizeof( HTTP_HEADER_INFO ) ) ); /* Read each line in the header checking for any fields that we need to handle. We check for a couple of basic problems with the header to avoid malformed-header attacks, for example an attacker could send a request with two 'Content-Length:' headers, one of which covers the entire message body and the other which indicates that there's a second request that begins halfway through the message body. Some proxies/caches will take the first length, some the second, if the proxy is expected to check/rewrite the request as it passes through then the single/dual-message issue can be used to bypass the checking on the tunnelled second message. Because of this we only allow a single Host: and Content-Length: header, and disallow a chunked encoding in combination with a content-length (Apache does some really strange things with chunked encodings). We can't be too finicky with the checking though or we'll end up rejecting non- malicious requests from some of the broken HTTP implementations out there */ for( lineCount = 0; lineCount < FAILSAFE_ITERATIONS_MED; lineCount++ ) { HTTP_HEADER_TYPE headerType; BOOLEAN textDataError; char *lineBufPtr; int lineLength; lineLength = readTextLine( readCharFunction, stream, lineBuffer, lineBufMaxLen, &textDataError ); if( cryptStatusError( lineLength ) ) return( retTextLineError( stream, lineLength, textDataError, "Invalid HTTP header line %d: ", lineCount + 2 ) ); if( lineLength <= 0 ) /* End of input, exit */ break; status = processHeaderLine( lineBuffer, lineLength, &headerType, stream, lineCount + 2 ); if( cryptStatusError( status ) ) return( status ); lineBufPtr = lineBuffer + status; lineLength -= status; assert( lineLength > 0 ); /* Guaranteed by processHeaderLine() */ switch( headerType ) { case HTTP_HEADER_HOST: /* Make sure that it's a non-duplicate, and remember that we've seen a Host: line, to meet the HTTP 1.1 requirements */ if( seenHost ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Duplicate HTTP 'Host:' header, line %d", lineCount + 2 ); seenHost = TRUE; break; case HTTP_HEADER_CONTENT_LENGTH: /* Make sure that it's a non-duplicate and get the content length. At this point all that we do is perform a general sanity check that the length looks OK, a specific check against the caller-supplied minimum/maximum allowable length is performed later since the content length may also be provided as a chunked encoding length, which we can't check until we've processed all of the header lines */ if( seenLength ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Duplicate HTTP 'Content-Length:' header, " "line %d", lineCount + 2 ); contentLength = getNumericValue( lineBufPtr, lineLength ); if( cryptStatusError( contentLength ) ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP content length, line %d", lineCount + 2 ); seenLength = TRUE; break; case HTTP_HEADER_CONTENT_TYPE: { int contentTypeLen, slashPos = CRYPT_ERROR; /* Sometimes if there's an error it'll be returned as content at the HTTP level rather than at the tunnelled-over-HTTP protocol level. The easiest way to check for this would be to make sure that the content-type matches the expected type and report anything else as an error. Unfortunately due to the hit-and-miss handling of content- types by PKI software using HTTP as a substrate it's not safe to do this, so we have to default to allow-all rather than deny-all, treating only straight text as a problem type. Unfortunately there are also apps out there that send their PKI messages marked as plain text, so this isn't 100% foolproof. This is particularly problematic for web browsers, where so many servers were misconfigured to return pretty much anything as text/plain that Microsoft added content-type guessing code to MSIE to make web pages served from misconfigured servers work (you can see this by serving a JPEG file as text/plain, MSIE will display it as a JPEG while Mozilla/Firefox/ Opera/etc will display it as text or prompt for a helper app to handle it). Since this content-type guessing is a potential security hole, MS finally made it configurable in Windows XP SP2, but it's still enabled by default even there. In practice however errors-via-HTTP is more common than certs-via-text. We try and detect the cert-as-plain-text special-case at a later point when we've got the message body available. Since we're now looking at the content-type line (even if we don't really process it in any way), we perform at least a minimal validity check for * '/' * [ ';*' ] */ for( contentTypeLen = 0; contentTypeLen < lineLength && \ lineBufPtr[ contentTypeLen ] != ';'; contentTypeLen++ ) { if( lineBufPtr[ contentTypeLen ] == '/' ) slashPos = contentTypeLen; } if( contentTypeLen < 5 || slashPos < 0 ) { /* We need to have at least 'xx/yy' present */ retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP content type '%s', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } if( slashPos >= 4 && \ !strCompare( lineBufPtr, "text/", 5 ) ) headerInfo->flags |= HTTP_FLAG_TEXTMSG; break; } case HTTP_HEADER_TRANSFER_ENCODING: if( lineLength < 7 || \ strCompare( lineBufPtr, "Chunked", 7 ) ) { retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP transfer encoding method " "'%s', expected 'Chunked', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } /* If it's a chunked encoding, the length is part of the data and must be read later */ if( seenLength ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Duplicate HTTP 'Content-Length:' header, " "line %d", lineCount + 2 ); headerInfo->flags |= HTTP_FLAG_CHUNKED; seenLength = TRUE; break; case HTTP_HEADER_CONTENT_ENCODING: /* We can't handle any type of content encoding (e.g. gzip, compress, deflate, mpeg4, interpretive dance) except the no-op identity encoding */ if( lineLength < 8 || \ strCompare( lineBufPtr, "Identity", 8 ) ) { headerInfo->httpStatus = 415; /* Unsupp.media type */ retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP content encoding method " "'%s', expected 'Identity', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } break; case HTTP_HEADER_CONTENT_TRANSFER_ENCODING: /* HTTP uses Transfer-Encoding, not the MIME Content- Transfer-Encoding types such as base64 or quoted- printable. If any implementations erroneously use a C-T-E, we make sure that it's something that we can handle */ if( lineLength < 6 || \ ( strCompare( lineBufPtr, "Identity", 8 ) && \ strCompare( lineBufPtr, "Binary", 6 ) ) ) { headerInfo->httpStatus = 415; /* Unsupp.media type */ retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP content transfer encoding method " "'%s', expected 'Identity' or 'Binary', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } break; case HTTP_HEADER_TRAILER: /* The body is followed by trailer lines, used with chunked encodings where some header lines can't be produced until the entire body has been generated. This wasn't added until RFC 2616, since many implementations are based on RFC 2068 and don't produce this header we don't do anything with it. The trailer can be auto-detected anyway, it's only present to tell the receiver to perform certain actions such as creating an MD5 hash of the data as it arrives */ headerInfo->flags |= HTTP_FLAG_TRAILER; break; case HTTP_HEADER_CONNECTION: /* If the other side has indicated that it's going to close the connection, remember that the stream is now no longer usable */ if( lineLength >= 5 && \ !strCompare( lineBufPtr, "Close", 5 ) ) sioctl( stream, STREAM_IOCTL_CONNSTATE, NULL, FALSE ); break; case HTTP_HEADER_WARNING: /* Read the HTTP status info from the warning. readHTTPStatus() will process the error status in the warning line, but since we're passing in a NULL pointer for the status info it'll only report an error in the warning content itself, it won't return the processed warning status as an error */ status = readHTTPStatus( lineBufPtr, lineLength, NULL, stream ); if( cryptStatusError( status ) ) { retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP warning information '%s', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } break; case HTTP_HEADER_LOCATION: { #if defined( __WIN32__ ) && !defined( NDEBUG ) && 1 URL_INFO urlInfo; #endif /* Win32 debug build only */ /* Make sure that we've been given an HTTP URL as the redirect location. We need to do this because sNetParseURL() will accept a wide range of URL types while we only allow "http://*" */ if( lineLength < 10 || \ strCompare( lineBufPtr, "http://", 7 ) ) { retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP redirect location '%s', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } /* Process the redirect location */ #if defined( __WIN32__ ) && !defined( NDEBUG ) && 1 /* We don't try and parse the URL because we don't do redirects yet so there's no need to expose ourselves to possibly maliciously-created URLs from external sources */ status = sNetParseURL( &urlInfo, lineBufPtr, lineLength, URL_TYPE_HTTP ); if( cryptStatusError( status ) ) { retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP redirect location '%s', line %d", sanitiseString( lineBufPtr, \ min( lineLength, \ CRYPT_MAX_TEXTSIZE ), lineLength ), lineCount + 2 ); } #endif /* Win32 debug build only */ break; } case HTTP_HEADER_EXPECT: /* If the other side wants the go-ahead to continue, give it to them. We do this automatically because we're merely using HTTP as a substrate, the real decision will be made at the higher-level protocol layer */ if( lineLength >= 12 && \ !strCompare( lineBufPtr, "100-Continue", 12 ) ) sendHTTPError( stream, lineBufPtr, lineBufMaxLen, 100 ); break; case HTTP_HEADER_NONE: /* It's something that we don't know/care about, skip it */ break; default: assert( NOTREACHED ); } } if( lineCount >= FAILSAFE_ITERATIONS_MED ) retExt( STREAM_ERRINFO, CRYPT_ERROR_OVERFLOW, "Too many HTTP header lines" ); /* If this is an tunnel being opened via an HTTP proxy, we're done */ if( !( stream->flags & STREAM_NFLAG_ISSERVER ) && \ ( stream->flags & STREAM_NFLAG_HTTPTUNNEL ) ) return( CRYPT_OK ); /* If it's a chunked encoding for which the length is kludged on before the data as a hex string, decode the length value */ if( headerInfo->flags & HTTP_FLAG_CHUNKED ) { BOOLEAN textDataError; const int lineLength = readTextLine( readCharFunction, stream, lineBuffer, lineBufMaxLen, &textDataError ); if( cryptStatusError( lineLength ) ) return( retTextLineError( stream, lineLength, textDataError, "Invalid HTTP chunked encoding " "header line %d: ", lineCount + 2 ) ); if( lineLength <= 0 ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Missing HTTP chunk length, line %d", lineCount + 2 ); status = contentLength = getChunkLength( lineBuffer, lineLength ); if( cryptStatusError( status ) ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid length for HTTP chunked encoding, line %d", lineCount + 2 ); } /* If this is a no-op read (for example lines following an error or 100 Continue response), all that we're interested in is draining the input, so we don't check any further */ if( headerInfo->flags & HTTP_FLAG_NOOP ) return( CRYPT_OK ); /* If we're a server talking HTTP 1.1 and we haven't seen a "Host:" header from the client, return an error */ if( ( stream->flags & STREAM_NFLAG_ISSERVER ) && \ !isHTTP10( stream ) && !seenHost ) { headerInfo->httpStatus = 400; /* Bad request */ retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Missing HTTP 'Host:' header" ); } /* If it's an idempotent read there's no length, just a GET request, so we can exit now */ if( stream->flags & STREAM_NFLAG_IDEMPOTENT ) { if( seenLength ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Unexpected %d bytes HTTP body content received in " "idempotent read", contentLength ); return( CRYPT_OK ); } /* Make sure that we've been given a length. In theory a server could indicate the length implicitly by closing the connection once it's sent the last byte, but this isn't allowed for PKI messages. The client can't use this option either since that would make it impossible for us to send back the response */ if( !seenLength ) { headerInfo->httpStatus = 411; /* Length required */ retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Missing HTTP length" ); } /* Make sure that the length is sensible */ if( contentLength < headerInfo->minContentLength || \ contentLength > headerInfo->maxContentLength ) { retExt( STREAM_ERRINFO, ( contentLength < headerInfo->minContentLength ) ? \ CRYPT_ERROR_UNDERFLOW : CRYPT_ERROR_OVERFLOW, "Invalid HTTP content length %d bytes, expected " "%d...%d bytes", contentLength, headerInfo->minContentLength, headerInfo->maxContentLength ); } headerInfo->contentLength = contentLength; return( CRYPT_OK ); } /* Read the HTTP trailer lines that follow chunked data: CRLF "0" CRLF trailer-lines* CRLF */ static int readTrailerLines( STREAM *stream, char *lineBuffer, const int lineBufMaxLen ) { HTTP_HEADER_INFO headerInfo; BOOLEAN textDataError; int readLength, status; assert( isWritePtr( stream, sizeof( STREAM ) ) ); assert( isReadPtr( lineBuffer, lineBufMaxLen ) ); /* Read the blank line and chunk length */ status = readTextLine( readCharFunction, stream, lineBuffer, lineBufMaxLen, &textDataError ); if( !cryptStatusError( status ) ) status = readLength = readTextLine( readCharFunction, stream, lineBuffer, lineBufMaxLen, &textDataError ); if( cryptStatusError( status ) ) return( retTextLineError( stream, status, textDataError, "Invalid HTTP chunked trailer line: ", 0 ) ); /* Make sure that there are no more chunks to follow */ status = getChunkLength( lineBuffer, readLength ); if( status != 0 ) retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Unexpected additional data following HTTP chunked data" ); /* Read any remaining trailer lines */ initHeaderInfo( &headerInfo, 0, 0, HTTP_FLAG_NOOP ); return( readHeaderLines( stream, lineBuffer, lineBufMaxLen, &headerInfo ) ); } /**************************************************************************** * * * Read Request Header * * * ****************************************************************************/ /* Read an HTTP request header */ static int readRequestHeader( STREAM *stream, char *lineBuffer, const int lineBufSize, int *contentLength, const int contentMaxLen, int *flags, HTTP_URI_INFO *uriInfo ) { HTTP_HEADER_INFO headerInfo; BOOLEAN textDataError; const BOOLEAN idempotentRead = \ ( stream->flags & STREAM_NFLAG_IDEMPOTENT ) ? TRUE : FALSE; const char *reqName = idempotentRead ? "GET " : "POST "; const int reqNameLen = idempotentRead ? 4 : 5; char *bufPtr; int length, offset, status; assert( isWritePtr( stream, sizeof( STREAM ) ) && \ stream->flags & STREAM_NFLAG_ISSERVER ); assert( isWritePtr( lineBuffer, lineBufSize ) ); assert( isWritePtr( contentLength, sizeof( int ) ) ); assert( isWritePtr( flags, sizeof( int ) ) ); assert( ( idempotentRead && \ isWritePtr( uriInfo, sizeof( HTTP_URI_INFO ) ) ) || \ ( !idempotentRead && uriInfo == NULL ) ); /* Clear return value */ *contentLength = CRYPT_ERROR; /* Read the header and check for "POST/GET x HTTP/1.x". In theory this could be a bit risky because the original CERN server required an extra (spurious) CRLF after a POST, so that various early clients sent an extra CRLF that isn't included in the Content-Length header and ends up preceding the start of the next load of data. We don't check for this because it only applies to very old pure-HTTP (rather than HTTP-as-a-transport-layer) clients, which are unlikely to be hitting a PKI responder */ status = length = readTextLine( readCharFunction, stream, lineBuffer, lineBufSize, &textDataError ); if( cryptStatusError( status ) ) { /* If it's an HTTP-level error (e.g. line too long), send back an HTTP-level error response */ if( status != CRYPT_ERROR_COMPLETE ) sendHTTPError( stream, lineBuffer, lineBufSize, ( status == CRYPT_ERROR_OVERFLOW ) ? 414 : 400 ); return( retTextLineError( stream, status, textDataError, "Invalid HTTP request header line 1:", 0 ) ); } if( length < reqNameLen || \ strCompare( lineBuffer, reqName, reqNameLen ) ) { char reqNameBuffer[ 8 + 8 ]; /* Return the extended error information. Note that we don't use sanitiseString() here because it's a static string that we supply, however we have to copy it into a temporary buffer so that we can strip the space character at the end */ sendHTTPError( stream, lineBuffer, lineBufSize, 501 ); memcpy( reqNameBuffer, reqName, reqNameLen ); reqNameBuffer[ reqNameLen - 1 ] = '\0'; /* Strip trailing space */ retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP request type, expected '%s'", sanitiseString( reqNameBuffer, reqNameLen - 1, reqNameLen - 1 ) ); } bufPtr = lineBuffer + reqNameLen; length -= reqNameLen; /* Process the ' '* * ' '* and check for the HTTP ID */ if( length <= 0 || ( offset = skipWhitespace( bufPtr, length ) ) < 0 ) { sendHTTPError( stream, lineBuffer, lineBufSize, 400 ); retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Missing HTTP request URI" ); } bufPtr += offset; length -= offset; if( idempotentRead ) { /* If it's an indempotent read the client is sending a GET rather than submitting a POST, process the request details. This performs in-place decoding of (possibly encoded) data, so it returns two length values, the new length after the in-place decoding has occurred, and the offset of the next character of data as usual */ offset = parseUriInfo( bufPtr, length, &length, uriInfo ); if( cryptStatusError( offset ) ) { sendHTTPError( stream, lineBuffer, lineBufSize, 400 ); retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP GET request URI" ); } bufPtr += offset; length -= offset; } else { /* For non-idempotent queries we don't care what the location is since it's not relevant for anything, so we just skip the URI. This also avoids complications with absolute vs. relative URLs, character encoding/escape sequences, and so on */ while( length > 0 && *bufPtr != ' ' ) { bufPtr++; length--; } } if( length <= 0 || ( offset = skipWhitespace( bufPtr, length ) ) < 0 ) { sendHTTPError( stream, lineBuffer, lineBufSize, 400 ); retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Missing HTTP request ID/version" ); } bufPtr += offset; length -= offset; if( length <= 0 || \ cryptStatusError( checkHTTPID( bufPtr, length, stream ) ) ) { sendHTTPError( stream, lineBuffer, lineBufSize, 505 ); retExt( STREAM_ERRINFO, CRYPT_ERROR_BADDATA, "Invalid HTTP request ID/version" ); } /* Process the remaining header lines. ~32 bytes is the minimum-size object that can be returned from any HTTP-based message which is exchanged by cryptlib, this being a TSP request */ initHeaderInfo( &headerInfo, 32, contentMaxLen, *flags ); status = readHeaderLines( stream, lineBuffer, lineBufSize, &headerInfo ); if( cryptStatusError( status ) ) { /* We always (try and) send an HTTP error response once we get to this stage since chances are that it'll be a problem with an HTTP header rather than a low-level network read problem */ sendHTTPError( stream, lineBuffer, lineBufSize, headerInfo.httpStatus ); return( status ); } /* If it's an idempotent read, the content is the request header and not the body, since there isn't one */ if( idempotentRead ) headerInfo.contentLength = sizeof( HTTP_URI_INFO ); /* Copy any status info back to the caller */ *contentLength = headerInfo.contentLength; *flags = headerInfo.flags; return( CRYPT_OK ); } /**************************************************************************** * * * Read Response Header * * * ****************************************************************************/ /* Read an HTTP response header */ static int readResponseHeader( STREAM *stream, char *lineBuffer, const int lineBufSize, int *contentLength, const int contentMaxLen, int *flags ) { int repeatCount, status; assert( isWritePtr( stream, sizeof( STREAM ) ) ); assert( isWritePtr( lineBuffer, lineBufSize ) ); assert( isWritePtr( contentLength, sizeof( int ) ) ); assert( isWritePtr( flags, sizeof( int ) ) ); /* Clear return value */ *contentLength = CRYPT_ERROR; /* If it's a stateless HTTP read, we need to first send the initiating HTTP fetch request before we can read anything back */ if( stream->protocol == STREAM_PROTOCOL_HTTP ) { assert( stream->contentTypeLen <= 0 ); status = writeRequestHeader( stream, 0 ); if( cryptStatusError( status ) ) return( status ); } /* Read the returned response header from the server, taking various special-case conditions into account. In theory we could also handle the 503 "Retry-After" status, but there's no sensible reason why anyone should send us this, and even if they do it'll screw up a lot of the PKI protocols, which have timeliness constraints built in */ for( repeatCount = 0; repeatCount < FAILSAFE_ITERATIONS_SMALL; \ repeatCount++ ) { HTTP_HEADER_INFO headerInfo; BOOLEAN needsSpecialHandling = FALSE; int httpStatus; /* Read the response header */ status = readFirstHeaderLine( stream, lineBuffer, lineBufSize, &httpStatus ); if( cryptStatusError( status ) ) { if( status != OK_SPECIAL ) return( status ); /* It's a special-case header (e.g. a 100 Continue), turn the read into a no-op read that drains the input to get to the real data */ *flags |= HTTP_FLAG_NOOP; needsSpecialHandling = TRUE; } /* Process the remaining header lines. 5 bytes is the minimum-size object that can be returned from any HTTP-based message which is exchanged by cryptlib, this being an OCSP response containing a single-byte status value, i.e. SEQUENCE { ENUM x } */ initHeaderInfo( &headerInfo, 5, contentMaxLen, *flags ); status = readHeaderLines( stream, lineBuffer, lineBufSize, &headerInfo ); if( cryptStatusError( status ) ) return( status ); /* Copy any status info back to the caller */ *flags = headerInfo.flags & ~HTTP_FLAG_NOOP; *contentLength = headerInfo.contentLength; /* If it's not something like a redirect that needs special-case handling, we're done */ if( !needsSpecialHandling ) return( CRYPT_OK ); assert( httpStatus == 100 || httpStatus == 301 || \ httpStatus == 302 || httpStatus == 307 ); /* If we got a 100 Continue response, try for another header that follows the first one */ if( httpStatus == 100 ) continue; /* If we got a 301, 302, or 307 Redirect then in theory we should proceed roughly as per the code below, however in practice it's not nearly as simple as this, because what we're in effect doing is taking a stream and replacing it with a completely new stream (different host/abs-path/query info, new socket with optional proxy handling, etc etc). One way to do this would be to read the new location into the current stream buffer and pass it back with a special status telling the stream-level code to create a new stream, clean up the old one, and perform a deep copy of the new stream over to the old one. We'll leave this for a time when it's really needed. In addition the semantics of the following pseudocode don't quite match those of RFC 2616 because of the HTTP-as-a-substrate use rather than direct use in a browser. Specifically, anything other than a GET for a 302 or 307 isn't supposed to perform an automatic redirect without asking the user, because of concerns that it'll change the semantics of the request. However, since we're not an interactive web browser there's no way that we can ask a user for redirect permission, and in any case since we're merely using HTTP as a substrate for a cryptographically protected PKI message (and specifically assuming that the HTTP layer is completely insecure), any problems will be caught by the crypto protocol layer */ #if 0 if( !*location ) return( CRYPT_ERROR_READ ); stream->closeSocketFunction( stream ); clFree( "readResponseHeader", stream->host ); stream->host = NULL; status = parseLocation( stream, location ); if( cryptStatusError( status ) ) return( CRYPT_ERROR_READ ); #endif /* 0 */ retExt( STREAM_ERRINFO, CRYPT_ERROR_READ, "Unable to process HTTP %d redirect", httpStatus ); } /* We used up our maximum number of retries, bail out */ retExt( STREAM_ERRINFO, CRYPT_ERROR_READ, "HTTP retry/redirection loop detected" ); } /**************************************************************************** * * * HTTP Access Functions * * * ****************************************************************************/ /* Read data from an HTTP stream */ static int readFunction( STREAM *stream, void *buffer, int length ) { char headerBuffer[ HTTP_LINEBUF_SIZE + 8 ]; void *bufPtr = buffer; int flags = HTTP_FLAG_NONE, contentLength, readLength, status; assert( isWritePtr( stream, sizeof( STREAM ) ) ); assert( isWritePtr( buffer, length ) ); assert( !( stream->flags & STREAM_NFLAG_IDEMPOTENT ) || \ ( length == sizeof( HTTP_URI_INFO ) ) ); /* Read the HTTP packet header */ if( stream->flags & STREAM_NFLAG_ISSERVER ) { /* If we're performing an idempotent read then the content is the URI info present in the header rather than any body content, so we pass in the read buffer to return the URI data */ status = readRequestHeader( stream, headerBuffer, HTTP_LINEBUF_SIZE, &contentLength, length, &flags, ( stream->flags & STREAM_NFLAG_IDEMPOTENT ) ? \ ( HTTP_URI_INFO * ) buffer : NULL ); } else { /* If the buffer is dynamically allocated then we allow an effectively arbitrary content length, otherwise it has to fit into the fixed-size read buffer. Unfortunately since CRLs can reach > 100MB in size it's not really possible to provide any sensible limit on the length for dynamic-buffer reads, however to avoid DoS issues we limit it to 8MB until someone complains that they can't read the 150MB CRLs that their CA is issuing */ status = readResponseHeader( stream, headerBuffer, HTTP_LINEBUF_SIZE, &contentLength, ( stream->callbackFunction != NULL ) ? \ min( MAX_INTLENGTH, 8388608L ) : \ length, &flags ); } if( cryptStatusError( status ) ) return( status ); /* Adjust the read buffer size if necessary. This adjustment only occurs on the client side, which needs to be able to handle arbitrary- length responses from the server */ if( contentLength > length ) { /* This situation can only occur if there's a buffer-adjust callback present, in which case we try and increase the buffer size to handle the extra data */ if( stream->callbackFunction == NULL || \ stream->callbackParams == NULL ) retIntError(); /* Safety check */ status = stream->callbackFunction( stream->callbackParams, &bufPtr, contentLength ); if( cryptStatusError( status ) ) return( status ); assert( isWritePtr( bufPtr, contentLength ) ); } /* If it's an idempotent read, all of the information was contained in the header and we're done */ if( stream->flags & STREAM_NFLAG_IDEMPOTENT ) return( contentLength ); /* Read the payload data from the client/server */ readLength = status = \ stream->bufferedTransportReadFunction( stream, bufPtr, contentLength, TRANSPORT_FLAG_NONE ); if( cryptStatusError( status ) ) return( status ); if( readLength < contentLength ) { /* We timed out before reading all of the data. Usually this will be reported as a CRYPT_ERROR_TIMEOUT by the lower-level read routines, however due to the multiple layers of I/O and special case timeout handling when (for example) a cryptlib transport session is layered over the network I/O layer, we perform an explicit check here to make sure that we got everything */ retExt( STREAM_ERRINFO, CRYPT_ERROR_TIMEOUT, "HTTP read timed out before all data could be read, only " "got %d of %d bytes", readLength, contentLength ); } /* If it's a plain-text error message, return it to the caller */ if( flags & HTTP_FLAG_TEXTMSG ) { BYTE *byteBufPtr = bufPtr; /* Usually a body returned as plain text is an error message that (for some reason) is sent as content rather than an HTTP error, however in some unusual cases the content will be the requested object marked as plain text. This only seems to occur with straight HTTP fetches from misconfigured servers rather than when HTTP is being used as a tunnelling mechanism for a PKI protocol, so we can filter this by requiring that the fetch is a straight HTTP fetch (not a request/response PKI protocol fetch), that the request is over a minimum size (most error messages are quite short), and that the first bytes match what would be seen in a PKI object such as a cert or CRL */ if( stream->protocol != STREAM_PROTOCOL_HTTP || \ contentLength < 256 || ( byteBufPtr[ 0 ] != 0x30 ) || \ !( byteBufPtr[ 1 ] & 0x80 ) || \ ( isAlpha( byteBufPtr[ 2 ] ) && isAlpha( byteBufPtr[ 3 ] ) && \ isAlpha( byteBufPtr[ 4 ] ) ) ) { retExt( STREAM_ERRINFO, CRYPT_ERROR_READ, "HTTP server reported: '%s'", sanitiseString( byteBufPtr, \ min( readLength, \ MAX_ERRMSG_SIZE - 32 ), readLength ) ); } } /* If we're reading chunked data, drain the input by processing the trailer. The reason why there can be extra header lines at the end of the chunked data is because it's designed to be an indefinite- length streamable format that doesn't require buffering the entire message before emitting it. Since some header information may not be available until the entire message has been generated, the HTTP spec. makes provisions for adding further header lines as a trailer. In theory we should check for the HTTP_FLAG_TRAILER flag before reading trailer lines rather than just swallowing the last CRLF, however the "Trailer:" header wasn't added until RFC 2616 (RFC 2068 didn't have it) so we can't rely on its presence. Normally we wouldn't have to worry about trailer data, but if it's an HTTP 1.1 persistent connection we need to clear the way for the next lot of data */ if( flags & HTTP_FLAG_CHUNKED ) { status = readTrailerLines( stream, headerBuffer, HTTP_LINEBUF_SIZE ); if( cryptStatusError( status ) ) return( status ); } return( readLength ); } int setStreamLayerHTTP( STREAM *stream ) { assert( isWritePtr( stream, sizeof( STREAM ) ) ); /* Set the access method pointers */ stream->readFunction = readFunction; setStreamLayerHTTPwrite( stream ); /* HTTP provides its own data-size and flow-control indicators so we don't want the higher-level code to try and do this for us */ stream->flags |= STREAM_NFLAG_ENCAPS; return( CRYPT_OK ); } #endif /* USE_HTTP */