/** * The idle state is invoked before and after every transaction. Consequently, * it will start a new transaction when data is available and finalise a transaction * which has been processed. * * @param connp * @returns HTP_OK on state change, HTTP_ERROR on error, or HTP_DATA when more data is needed. */ int htp_connp_REQ_IDLE(htp_connp_t * connp) { // If we're here and a transaction object exists that // means we've just completed parsing a request. We need // to run the final hook and start over. if (connp->in_tx != NULL) { // Run hook REQUEST int rc = hook_run_all(connp->cfg->hook_request, connp); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request callback returned error (%d)", rc); return HTP_ERROR; } // Start afresh connp->in_tx = NULL; } // We want to start parsing the next request (and change // the state from IDLE) only if there's at least one // byte of data available. Otherwise we could be creating // new structures even if there's no more data on the // connection. IN_TEST_NEXT_BYTE_OR_RETURN(connp); // Detect pipelining if (list_size(connp->conn->transactions) > connp->out_next_tx_index) { connp->conn->flags |= PIPELINED_CONNECTION; } // Parsing a new request connp->in_tx = htp_tx_create(connp->cfg, CFG_SHARED, connp->conn); if (connp->in_tx == NULL) return HTP_ERROR; connp->in_tx->connp = connp; list_add(connp->conn->transactions, connp->in_tx); connp->in_content_length = -1; connp->in_body_data_left = -1; connp->in_header_line_index = -1; connp->in_header_line_counter = 0; connp->in_chunk_request_index = connp->in_chunk_count; // Run hook TRANSACTION_START int rc = hook_run_all(connp->cfg->hook_transaction_start, connp); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Transaction start callback returned error (%d)", rc); return HTP_ERROR; } // Change state into request line parsing connp->in_state = htp_connp_REQ_LINE; connp->in_tx->progress[0] = TX_PROGRESS_REQ_LINE; return HTP_OK; }
/** * Processes a chunk of data. * * @param connp * @returns HTP_OK on state change, HTTP_ERROR on error, or HTP_DATA when more data is needed. */ int htp_connp_RES_BODY_CHUNKED_DATA(htp_connp_t *connp) { htp_tx_data_t d; d.tx = connp->out_tx; d.data = &connp->out_current_data[connp->out_current_offset]; d.len = 0; for (;;) { OUT_NEXT_BYTE(connp); if (connp->out_next_byte == -1) { if (connp->out_tx->response_content_encoding != COMPRESSION_NONE) { connp->out_decompressor->decompress(connp->out_decompressor, &d); } else { // Send data to callbacks int rc = htp_res_run_hook_body_data(connp, &d); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Response body data callback returned error (%d)", rc); return HTP_ERROR; } } // Ask for more data return HTP_DATA; } else { connp->out_tx->response_message_len++; connp->out_tx->response_entity_len++; connp->out_chunked_length--; d.len++; if (connp->out_chunked_length == 0) { // End of data chunk if (connp->out_tx->response_content_encoding != COMPRESSION_NONE) { connp->out_decompressor->decompress(connp->out_decompressor, &d); } else { // Send data to callbacks int rc = htp_res_run_hook_body_data(connp, &d); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Response body data callback returned error (%d)", rc); return HTP_ERROR; } } connp->out_state = htp_connp_RES_BODY_CHUNKED_DATA_END; return HTP_OK; } } } }
/** * Processes identity request body. * * @param connp * @returns HTP_OK on state change, HTTP_ERROR on error, or HTP_DATA when more data is needed. */ int htp_connp_REQ_BODY_IDENTITY(htp_connp_t *connp) { htp_tx_data_t d; d.tx = connp->in_tx; d.data = &connp->in_current_data[connp->in_current_offset]; d.len = 0; for (;;) { IN_NEXT_BYTE(connp); if (connp->in_next_byte == -1) { // End of chunk if (d.len != 0) { int rc = hook_run_all(connp->cfg->hook_request_body_data, &d); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request body data callback returned error (%d)", rc); return HTP_ERROR; } } // Ask for more data return HTP_DATA; } else { connp->in_tx->request_message_len++; connp->in_tx->request_entity_len++; connp->in_body_data_left--; d.len++; if (connp->in_body_data_left == 0) { // End of body if (d.len != 0) { int rc = hook_run_all(connp->cfg->hook_request_body_data, &d); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request body data callback returned error (%d)", rc); return HTP_ERROR; } } // Done connp->in_state = htp_connp_REQ_IDLE; connp->in_tx->progress[0] = TX_PROGRESS_WAIT; return HTP_OK; } } } return HTP_ERROR; }
htp_status_t htp_tx_state_response_line(htp_tx_t *tx) { if (tx == NULL) return HTP_ERROR; #if 0 // Commented-out until we determine which fields can be // unavailable in real-life. // Unless we're dealing with HTTP/0.9, check that // the minimum amount of data has been provided. if (tx->is_protocol_0_9 != 0) { if ((tx->response_protocol == NULL) || (tx->response_status_number == -1) || (tx->response_message == NULL)) { return HTP_ERROR; } } #endif // Is the response line valid? if ((tx->response_protocol_number == HTP_PROTOCOL_INVALID) || (tx->response_status_number == HTP_STATUS_INVALID) || (tx->response_status_number < HTP_VALID_STATUS_MIN) || (tx->response_status_number > HTP_VALID_STATUS_MAX)) { htp_log(tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Invalid response line."); tx->flags |= HTP_STATUS_LINE_INVALID; } // Run hook HTP_RESPONSE_LINE htp_status_t rc = htp_hook_run_all(tx->connp->cfg->hook_response_line, tx); if (rc != HTP_OK) return rc; return HTP_OK; }
/** * Opens connection. * * @param connp * @param remote_addr Remote address * @param remote_port Remote port * @param local_addr Local address * @param local_port Local port * @param timestamp */ void htp_connp_open(htp_connp_t *connp, const char *remote_addr, int remote_port, const char *local_addr, int local_port, htp_time_t timestamp) { if ((connp->in_status != STREAM_STATE_NEW) || (connp->out_status != STREAM_STATE_NEW)) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Connection is already open"); return; } if (remote_addr != NULL) { connp->conn->remote_addr = strdup(remote_addr); if (connp->conn->remote_addr == NULL) return; } connp->conn->remote_port = remote_port; if (local_addr != NULL) { connp->conn->local_addr = strdup(local_addr); if (connp->conn->local_addr == NULL) { if (connp->conn->remote_addr != NULL) { free(connp->conn->remote_addr); } return; } } connp->conn->local_port = local_port; connp->conn->open_timestamp = timestamp; connp->in_status = STREAM_STATE_OPEN; connp->out_status = STREAM_STATE_OPEN; }
htp_status_t htp_tx_state_request_headers(htp_tx_t *tx) { // Did this request arrive in multiple chunks? if (tx->connp->in_chunk_count != tx->connp->in_chunk_request_index) { tx->flags |= HTP_MULTI_PACKET_HEAD; } // If we're in TX_PROGRESS_REQ_HEADERS that means that this is the // first time we're processing headers in a request. Otherwise, // we're dealing with trailing headers. if (tx->progress > HTP_REQUEST_HEADERS) { // Run hook HTP_REQUEST_TRAILER int rc = htp_hook_run_all(tx->connp->cfg->hook_request_trailer, tx->connp); if (rc != HTP_OK) return rc; // Completed parsing this request; finalize it now. tx->connp->in_state = htp_connp_REQ_FINALIZE; } else if (tx->progress >= HTP_REQUEST_LINE) { // Process request headers int rc = htp_tx_process_request_headers(tx); if (rc != HTP_OK) return rc; tx->connp->in_state = htp_connp_REQ_CONNECT_CHECK; } else { htp_log(tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "[Internal Error] Invalid tx progress: %d", tx->progress); return HTP_ERROR; } return HTP_OK; }
/** * The response idle state will initialize response processing, as well as * finalize each transactions after we are done with it. * * @param[in] connp * @returns HTP_OK on state change, HTP_ERROR on error, or HTP_DATA when more data is needed. */ htp_status_t htp_connp_RES_IDLE(htp_connp_t *connp) { // We want to start parsing the next response (and change // the state from IDLE) only if there's at least one // byte of data available. Otherwise we could be creating // new structures even if there's no more data on the // connection. OUT_TEST_NEXT_BYTE_OR_RETURN(connp); // Parsing a new response // Find the next outgoing transaction connp->out_tx = htp_list_get(connp->conn->transactions, connp->out_next_tx_index); if (connp->out_tx == NULL) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Unable to match response to request"); return HTP_ERROR; } // We've used one transaction connp->out_next_tx_index++; // TODO Detect state mismatch connp->out_content_length = -1; connp->out_body_data_left = -1; int rc = htp_tx_state_response_start(connp->out_tx); if (rc != HTP_OK) return rc; return HTP_OK; }
/** * Initialize gzip decompressor. * * @param connp */ htp_decompressor_t * htp_gzip_decompressor_create(htp_connp_t *connp, int format) { htp_decompressor_gzip_t *drec = calloc(1, sizeof (htp_decompressor_gzip_t)); if (drec == NULL) return NULL; drec->super.decompress = (int (*)(htp_decompressor_t *, htp_tx_data_t *)) htp_gzip_decompressor_decompress; drec->super.destroy = (void (*)(htp_decompressor_t *))htp_gzip_decompressor_destroy; drec->buffer = malloc(GZIP_BUF_SIZE); if (drec->buffer == NULL) { free(drec); return NULL; } int rc = inflateInit2(&drec->stream, GZIP_WINDOW_SIZE); if (rc != Z_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "GZip decompressor: inflateInit2 failed with code %d", rc); inflateEnd(&drec->stream); free(drec->buffer); free(drec); return NULL; } drec->zlib_initialized = 1; drec->stream.avail_out = GZIP_BUF_SIZE; drec->stream.next_out = drec->buffer; if (format == COMPRESSION_DEFLATE) { drec->initialized = 1; } return (htp_decompressor_t *) drec; }
/** * Processes a chunk of data. * * @param connp * @returns HTP_OK on state change, HTTP_ERROR on error, or HTP_DATA when more data is needed. */ int htp_connp_REQ_BODY_CHUNKED_DATA(htp_connp_t *connp) { htp_tx_data_t d; d.tx = connp->in_tx; d.data = &connp->in_current_data[connp->in_current_offset]; d.len = 0; for (;;) { IN_NEXT_BYTE(connp); if (connp->in_next_byte == -1) { // Send data to callbacks int rc = hook_run_all(connp->cfg->hook_request_body_data, &d); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request body data callback returned error (%d)", rc); return HTP_ERROR; } // Ask for more data return HTP_DATA; } else { connp->in_tx->request_message_len++; connp->in_tx->request_entity_len++; connp->in_chunked_length--; d.len++; if (connp->in_chunked_length == 0) { // End of data chunk // Send data to callbacks int rc = hook_run_all(connp->cfg->hook_request_body_data, &d); if (rc != HOOK_OK) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request body data callback returned error (%d)", rc); return HTP_ERROR; } connp->in_state = htp_connp_REQ_BODY_CHUNKED_DATA_END; return HTP_OK; } } } return HTP_ERROR; }
/** * Invoked whenever decompressed response body data becomes available. * * @param d * @return HTP_OK on state change, HTP_ERROR on error. */ static int htp_connp_RES_BODY_DECOMPRESSOR_CALLBACK(htp_tx_data_t *d) { // Invoke all callbacks int rc = htp_res_run_hook_body_data(d->tx->connp, d); if (rc != HTP_OK) { htp_log(d->tx->connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Response body data callback returned error (%d)", rc); return HTP_ERROR; } return HTP_OK; }
htp_status_t htp_tx_res_process_body_data_ex(htp_tx_t *tx, const void *data, size_t len) { if (tx == NULL) return HTP_ERROR; // NULL data is allowed in this private function; it's // used to indicate the end of response body. #ifdef HTP_DEBUG fprint_raw_data(stderr, __FUNCTION__, data, len); #endif htp_tx_data_t d; d.tx = tx; d.data = (unsigned char *) data; d.len = len; // Keep track of body size before decompression. tx->response_message_len += d.len; switch (tx->response_content_encoding_processing) { case HTP_COMPRESSION_GZIP: case HTP_COMPRESSION_DEFLATE: // Send data buffer to the decompressor. tx->connp->out_decompressor->decompress(tx->connp->out_decompressor, &d); if (data == NULL) { // Shut down the decompressor, if we used one. tx->connp->out_decompressor->destroy(tx->connp->out_decompressor); tx->connp->out_decompressor = NULL; } break; case HTP_COMPRESSION_NONE: // When there's no decompression, response_entity_len. // is identical to response_message_len. tx->response_entity_len += d.len; htp_status_t rc = htp_res_run_hook_body_data(tx->connp, &d); if (rc != HTP_OK) return HTP_ERROR; break; default: // Internal error. htp_log(tx->connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "[Internal Error] Invalid tx->response_content_encoding_processing value: %d", tx->response_content_encoding_processing); return HTP_ERROR; break; } return HTP_OK; }
void htp_connp_open(htp_connp_t *connp, const char *client_addr, int client_port, const char *server_addr, int server_port, htp_time_t *timestamp) { // Check connection parser state first. if ((connp->in_status != HTP_STREAM_NEW) || (connp->out_status != HTP_STREAM_NEW)) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Connection is already open"); return; } if (htp_conn_open(connp->conn, client_addr, client_port, server_addr, server_port, timestamp) != HTP_OK) { return; } connp->in_status = HTP_STREAM_OPEN; connp->out_status = HTP_STREAM_OPEN; }
/** * Extracts chunk length. * * @param[in] connp * @returns HTP_OK on state change, HTP_ERROR on error, or HTP_DATA when more data is needed. */ htp_status_t htp_connp_RES_BODY_CHUNKED_LENGTH(htp_connp_t *connp) { for (;;) { OUT_COPY_BYTE_OR_RETURN(connp); // Have we reached the end of the line? if (connp->out_next_byte == LF) { unsigned char *data; size_t len; if (htp_connp_res_consolidate_data(connp, &data, &len) != HTP_OK) { return HTP_ERROR; } connp->out_tx->response_message_len += len; #ifdef HTP_DEBUG fprint_raw_data(stderr, "Chunk length line", data, len); #endif htp_chomp(data, &len); connp->out_chunked_length = htp_parse_chunked_length(data, len); htp_connp_res_clear_buffer(connp); // Handle chunk length if (connp->out_chunked_length > 0) { // More data available connp->out_state = htp_connp_RES_BODY_CHUNKED_DATA; } else if (connp->out_chunked_length == 0) { // End of data connp->out_state = htp_connp_RES_HEADERS; connp->out_tx->response_progress = HTP_RESPONSE_TRAILER; } else { // Invalid chunk length htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Response chunk encoding: Invalid chunk length: %d", connp->out_chunked_length); return HTP_ERROR; } return HTP_OK; } } return HTP_ERROR; }
/** * If there is any data left in the inbound data chunk, this function will preserve * it for later consumption. The maximum amount accepted for buffering is controlled * by htp_config_t::field_limit_hard. * * @param[in] connp * @return HTP_OK, or HTP_ERROR on fatal failure. */ static htp_status_t htp_connp_req_buffer(htp_connp_t *connp) { if (connp->in_current_data == NULL) return HTP_OK; unsigned char *data = connp->in_current_data + connp->in_current_consume_offset; size_t len = connp->in_current_read_offset - connp->in_current_consume_offset; if (len == 0) return HTP_OK; // Check the hard (buffering) limit. size_t newlen = connp->in_buf_size + len; // When calculating the size of the buffer, take into account the // space we're using for the request header buffer. if (connp->in_header != NULL) { newlen += bstr_len(connp->in_header); } if (newlen > connp->in_tx->cfg->field_limit_hard) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request buffer over the limit: size %zd limit %zd.", newlen, connp->in_tx->cfg->field_limit_hard); return HTP_ERROR; } // Copy the data remaining in the buffer. if (connp->in_buf == NULL) { connp->in_buf = malloc(len); if (connp->in_buf == NULL) return HTP_ERROR; memcpy(connp->in_buf, data, len); connp->in_buf_size = len; } else { size_t newsize = connp->in_buf_size + len; unsigned char *newbuf = realloc(connp->in_buf, newsize); if (newbuf == NULL) return HTP_ERROR; connp->in_buf = newbuf; memcpy(connp->in_buf + connp->in_buf_size, data, len); connp->in_buf_size = newsize; } // Reset the consumer position. connp->in_current_consume_offset = connp->in_current_read_offset; return HTP_OK; }
/** * Extracts chunk length. * * @param[in] connp * @returns HTP_OK on state change, HTP_ERROR on error, or HTP_DATA when more data is needed. */ htp_status_t htp_connp_REQ_BODY_CHUNKED_LENGTH(htp_connp_t *connp) { for (;;) { IN_COPY_BYTE_OR_RETURN(connp); // Have we reached the end of the line? if (connp->in_next_byte == LF) { unsigned char *data; size_t len; htp_connp_req_consolidate_data(connp, &data, &len); connp->in_tx->request_message_len += len; #ifdef HTP_DEBUG fprint_raw_data(stderr, "Chunk length line", data, len); #endif htp_chomp(data, &len); connp->in_chunked_length = htp_parse_chunked_length(data, len); htp_connp_req_clear_buffer(connp); // Handle chunk length. if (connp->in_chunked_length > 0) { // More data available. // TODO Add a check (flag) for excessive chunk length. connp->in_state = htp_connp_REQ_BODY_CHUNKED_DATA; } else if (connp->in_chunked_length == 0) { // End of data connp->in_state = htp_connp_REQ_HEADERS; connp->in_tx->request_progress = HTP_REQUEST_TRAILER; } else { // Invalid chunk length. htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request chunk encoding: Invalid chunk length"); return HTP_ERROR; } return HTP_OK; } } return HTP_ERROR; }
htp_status_t htp_tx_req_process_body_data(htp_tx_t *tx, const void *data, size_t len) { // Keep track of the body length. tx->request_entity_len += len; // Send data to the callbacks. htp_tx_data_t d; d.tx = tx; d.data = (unsigned char *) data; d.len = len; int rc = htp_req_run_hook_body_data(tx->connp, &d); if (rc != HTP_OK) { htp_log(tx->connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request body data callback returned error (%d)", rc); return HTP_ERROR; } return HTP_OK; }
/** * If there is any data left in the outbound data chunk, this function will preserve * it for consumption later. The maximum amount accepted for buffering is controlled * by htp_config_t::field_limit_hard. * * @param[in] connp * @return HTP_OK, or HTP_ERROR on fatal failure. */ static htp_status_t htp_connp_res_buffer(htp_connp_t *connp) { unsigned char *data = connp->out_current_data + connp->out_current_consume_offset; size_t len = connp->out_current_read_offset - connp->out_current_consume_offset; // Check the hard (buffering) limit. size_t newlen = connp->out_buf_size + len; if (connp->out_header != NULL) { newlen += bstr_len(connp->out_header); } if (newlen > connp->out_tx->cfg->field_limit_hard) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Response field size over the buffer limit: size %zd limit %zd.", newlen, connp->out_tx->cfg->field_limit_hard); return HTP_ERROR; } // Copy the data remaining in the buffer. if (connp->out_buf == NULL) { connp->out_buf = malloc(len); if (connp->out_buf == NULL) return HTP_ERROR; memcpy(connp->out_buf, data, len); connp->out_buf_size = len; } else { size_t newsize = connp->out_buf_size + len; unsigned char *newbuf = realloc(connp->out_buf, newsize); if (newbuf == NULL) return HTP_ERROR; connp->out_buf = newbuf; memcpy(connp->out_buf + connp->out_buf_size, data, len); connp->out_buf_size = newsize; } // Reset the consumer position. connp->out_current_consume_offset = connp->out_current_read_offset; return HTP_OK; }
/** * Extracts chunk length. * * @param connp * @returns HTP_OK on state change, HTTP_ERROR on error, or HTP_DATA when more data is needed. */ int htp_connp_REQ_BODY_CHUNKED_LENGTH(htp_connp_t *connp) { for (;;) { IN_COPY_BYTE_OR_RETURN(connp); connp->in_tx->request_message_len++; // Have we reached the end of the line? if (connp->in_next_byte == LF) { htp_chomp(connp->in_line, &connp->in_line_len); // Extract chunk length connp->in_chunked_length = htp_parse_chunked_length(connp->in_line, connp->in_line_len); // Cleanup for the next line connp->in_line_len = 0; // Handle chunk length if (connp->in_chunked_length > 0) { // More data available // TODO Add a check for chunk length connp->in_state = htp_connp_REQ_BODY_CHUNKED_DATA; } else if (connp->in_chunked_length == 0) { // End of data connp->in_state = htp_connp_REQ_HEADERS; connp->in_tx->progress[0] = TX_PROGRESS_REQ_TRAILER; } else { // Invalid chunk length htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request chunk encoding: Invalid chunk length"); return HTP_ERROR; } return HTP_OK; } } return HTP_ERROR; }
htp_status_t htp_tx_req_process_body_data_ex(htp_tx_t *tx, const void *data, size_t len) { if (tx == NULL) return HTP_ERROR; // NULL data is allowed in this private function; it's // used to indicate the end of request body. // Keep track of the body length. tx->request_entity_len += len; // Send data to the callbacks. htp_tx_data_t d; d.tx = tx; d.data = (unsigned char *) data; d.len = len; htp_status_t rc = htp_req_run_hook_body_data(tx->connp, &d); if (rc != HTP_OK) { htp_log(tx->connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request body data callback returned error (%d)", rc); return HTP_ERROR; } return HTP_OK; }
/** * Opens connection. * * @param connp * @param remote_addr Remote address * @param remote_port Remote port * @param local_addr Local address * @param local_port Local port * @param use_local_port Use local port for connection port * @param timestamp Optional */ void htp_connp_open(htp_connp_t *connp, const char *remote_addr, int remote_port, const char *local_addr, int local_port, htp_time_t *timestamp) { if ((connp->in_status != STREAM_STATE_NEW) || (connp->out_status != STREAM_STATE_NEW)) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Connection is already open"); return; } if (remote_addr != NULL) { connp->conn->remote_addr = strdup(remote_addr); if (connp->conn->remote_addr == NULL) return; } connp->conn->remote_port = remote_port; if (local_addr != NULL) { connp->conn->local_addr = strdup(local_addr); if (connp->conn->local_addr == NULL) { if (connp->conn->remote_addr != NULL) { free(connp->conn->remote_addr); } return; } } connp->conn->local_port = local_port; // Remember when the connection was opened. if (timestamp != NULL) { memcpy(&connp->conn->open_timestamp, timestamp, sizeof(*timestamp)); } connp->in_status = STREAM_STATE_OPEN; connp->out_status = STREAM_STATE_OPEN; }
/** * Generic response header line(s) processor, which assembles folded lines * into a single buffer before invoking the parsing function. * * @param[in] connp * @param[in] data * @param[in] len * @return HTP status */ htp_status_t htp_process_response_header_generic(htp_connp_t *connp, unsigned char *data, size_t len) { // Create a new header structure. htp_header_t *h = calloc(1, sizeof (htp_header_t)); if (h == NULL) return HTP_ERROR; if (htp_parse_response_header_generic(connp, h, data, len) != HTP_OK) { free(h); return HTP_ERROR; } #ifdef HTP_DEBUG fprint_bstr(stderr, "Header name", h->name); fprint_bstr(stderr, "Header value", h->value); #endif // Do we already have a header with the same name? htp_header_t *h_existing = htp_table_get(connp->out_tx->response_headers, h->name); if (h_existing != NULL) { // Keep track of repeated same-name headers. h_existing->flags |= HTP_FIELD_REPEATED; // Having multiple C-L headers is against the RFC but many // browsers ignore the subsequent headers if the values are the same. if (bstr_cmp_c_nocase(h->name, "Content-Length") == 0) { // Don't use string comparison here because we want to // ignore small formatting differences. int64_t existing_cl, new_cl; existing_cl = htp_parse_content_length(h_existing->value); new_cl = htp_parse_content_length(h->value); if ((existing_cl == -1) || (new_cl == -1) || (existing_cl != new_cl)) { // Ambiguous response C-L value. htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Ambiguous response C-L value"); bstr_free(h->name); bstr_free(h->value); free(h); return HTP_ERROR; } // Ignoring the new C-L header that has the same value as the previous ones. } else { // Add to the existing header. bstr *new_value = bstr_expand(h_existing->value, bstr_len(h_existing->value) + 2 + bstr_len(h->value)); if (new_value == NULL) { bstr_free(h->name); bstr_free(h->value); free(h); return HTP_ERROR; } h_existing->value = new_value; bstr_add_mem_noex(h_existing->value, (unsigned char *) ", ", 2); bstr_add_noex(h_existing->value, h->value); } // The new header structure is no longer needed. bstr_free(h->name); bstr_free(h->value); free(h); } else { // Add as a new header. if (htp_table_add(connp->out_tx->response_headers, h->name, h) != HTP_OK) { bstr_free(h->name); bstr_free(h->value); free(h); return HTP_ERROR; } } return HTP_OK; }
/** * Decompress a chunk of gzip-compressed data. * * @param drec * @param d */ static int htp_gzip_decompressor_decompress(htp_decompressor_gzip_t *drec, htp_tx_data_t *d) { size_t consumed = 0; // Return if we've previously had an error if (drec->initialized < 0) { return drec->initialized; } // Do we need to initialize? if (drec->initialized == 0) { // Check the header if ((drec->header_len == 0) && (d->len >= 10)) { // We have received enough data initialize; use the input buffer directly if ((d->data[0] != DEFLATE_MAGIC_1) || (d->data[1] != DEFLATE_MAGIC_2)) { htp_log(d->tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "GZip decompressor: Magic bytes mismatch"); drec->initialized = -1; return -1; } if (d->data[3] != 0) { htp_log(d->tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "GZip decompressor: Unable to handle flags: %d", d->data[3]); drec->initialized = -1; return -1; } drec->initialized = 1; consumed = 10; } else { // We do not (or did not) have enough bytes, so we have // to copy some data into our internal header buffer. // How many bytes do we need? size_t copylen = 10 - drec->header_len; // Is there enough in input? if (copylen > d->len) copylen = d->len; // Copy the bytes memcpy(drec->header + drec->header_len, d->data, copylen); drec->header_len += copylen; consumed = copylen; // Do we have enough now? if (drec->header_len == 10) { // We do! if ((drec->header[0] != DEFLATE_MAGIC_1) || (drec->header[1] != DEFLATE_MAGIC_2)) { htp_log(d->tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "GZip decompressor: Magic bytes mismatch"); drec->initialized = -1; return -1; } if (drec->header[3] != 0) { htp_log(d->tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "GZip decompressor: Unable to handle flags: %d", d->data[3]); drec->initialized = -1; return -1; } drec->initialized = 1; } else { // Need more data return 1; } } } // Decompress data int rc = 0; drec->stream.next_in = d->data + consumed; drec->stream.avail_in = d->len - consumed; while (drec->stream.avail_in != 0) { // If there's no more data left in the // buffer, send that information out if (drec->stream.avail_out == 0) { drec->crc = crc32(drec->crc, drec->buffer, GZIP_BUF_SIZE); // Prepare data for callback htp_tx_data_t d2; d2.tx = d->tx; d2.data = drec->buffer; d2.len = GZIP_BUF_SIZE; // Send decompressed data to callback if (drec->super.callback(&d2) < 0) { inflateEnd(&drec->stream); drec->zlib_initialized = 0; return -1; } drec->stream.next_out = drec->buffer; drec->stream.avail_out = GZIP_BUF_SIZE; } rc = inflate(&drec->stream, Z_NO_FLUSH); if (rc == Z_STREAM_END) { // How many bytes do we have? size_t len = GZIP_BUF_SIZE - drec->stream.avail_out; // Update CRC drec->crc = crc32(drec->crc, drec->buffer, len); // Prepare data for callback htp_tx_data_t d2; d2.tx = d->tx; d2.data = drec->buffer; d2.len = len; // Send decompressed data to callback if (drec->super.callback(&d2) < 0) { inflateEnd(&drec->stream); drec->zlib_initialized = 0; return -1; } // TODO Handle trailer return 1; } if (rc != Z_OK) { htp_log(d->tx->connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "GZip decompressor: inflate failed with %d", rc); inflateEnd(&drec->stream); drec->zlib_initialized = 0; return -1; } } return 1; }
/** * Parses response headers. * * @param[in] connp * @returns HTP_OK on state change, HTP_ERROR on error, or HTP_DATA when more data is needed. */ htp_status_t htp_connp_RES_HEADERS(htp_connp_t *connp) { for (;;) { OUT_COPY_BYTE_OR_RETURN(connp); // Have we reached the end of the line? if (connp->out_next_byte == LF) { unsigned char *data; size_t len; htp_connp_res_consolidate_data(connp, &data, &len); #ifdef HTP_DEBUG fprint_raw_data(stderr, __FUNCTION__, data, len); #endif // Should we terminate headers? if (htp_connp_is_line_terminator(connp, data, len)) { // Parse previous header, if any. if (connp->out_header != NULL) { if (connp->cfg->process_response_header(connp, bstr_ptr(connp->out_header), bstr_len(connp->out_header)) != HTP_OK) return HTP_ERROR; bstr_free(connp->out_header); connp->out_header = NULL; } htp_connp_res_clear_buffer(connp); // We've seen all response headers. if (connp->out_tx->progress == HTP_RESPONSE_HEADERS) { // Response headers. // The next step is to determine if this response has a body. connp->out_state = htp_connp_RES_BODY_DETERMINE; } else { // Response trailer. // Finalize sending raw trailer data. htp_status_t rc = htp_connp_res_receiver_finalize_clear(connp); if (rc != HTP_OK) return rc; // Run hook response_TRAILER. rc = htp_hook_run_all(connp->cfg->hook_response_trailer, connp); if (rc != HTP_OK) return rc; // The next step is to finalize this response. connp->out_state = htp_connp_RES_FINALIZE; } return HTP_OK; } htp_chomp(data, &len); // Check for header folding. if (htp_connp_is_line_folded(data, len) == 0) { // New header line. // Parse previous header, if any. if (connp->out_header != NULL) { if (connp->cfg->process_response_header(connp, bstr_ptr(connp->out_header), bstr_len(connp->out_header)) != HTP_OK) return HTP_ERROR; bstr_free(connp->out_header); connp->out_header = NULL; } OUT_PEEK_NEXT(connp); if (htp_is_folding_char(connp->out_next_byte) == 0) { // Because we know this header is not folded, we can process the buffer straight away. if (connp->cfg->process_response_header(connp, data, len) != HTP_OK) return HTP_ERROR; } else { // Keep the partial header data for parsing later. connp->out_header = bstr_dup_mem(data, len); if (connp->out_header == NULL) return HTP_ERROR; } } else { // Folding; check that there's a previous header line to add to. if (connp->out_header == NULL) { // Invalid folding. // Warn only once per transaction. if (!(connp->out_tx->flags & HTP_INVALID_FOLDING)) { connp->out_tx->flags |= HTP_INVALID_FOLDING; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Invalid response field folding"); } // Keep the header data for parsing later. connp->out_header = bstr_dup_mem(data, len); if (connp->out_header == NULL) return HTP_ERROR; } else { // Add to the existing header. bstr *new_out_header = bstr_add_mem(connp->out_header, data, len); if (new_out_header == NULL) return HTP_ERROR; connp->out_header = new_out_header; } } htp_connp_res_clear_buffer(connp); } } return HTP_ERROR; }
/** * Determines presence (and encoding) of a response body. * * @param[in] connp * @returns HTP_OK on state change, HTP_ERROR on error, or HTP_DATA when more data is needed. */ htp_status_t htp_connp_RES_BODY_DETERMINE(htp_connp_t *connp) { // If the request uses the CONNECT method, then not only are we // to assume there's no body, but we need to ignore all // subsequent data in the stream. if (connp->out_tx->request_method_number == HTP_M_CONNECT) { if ((connp->out_tx->response_status_number >= 200) && (connp->out_tx->response_status_number <= 299)) { // This is a successful CONNECT stream, which means // we need to switch into tunnelling mode. connp->in_status = HTP_STREAM_TUNNEL; connp->out_status = HTP_STREAM_TUNNEL; connp->out_state = htp_connp_RES_FINALIZE; return HTP_OK; } else { // This is a failed CONNECT stream, which means that // we can unblock request parsing connp->in_status = HTP_STREAM_DATA; // We are going to continue processing this transaction, // adding a note for ourselves to stop at the end (because // we don't want to see the beginning of a new transaction). connp->out_data_other_at_tx_end = 1; } } // Check for an interim "100 Continue" response. Ignore it if found, and revert back to RES_FIRST_LINE. if (connp->out_tx->response_status_number == 100) { if (connp->out_tx->seen_100continue != 0) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Already seen 100-Continue."); return HTP_ERROR; } // Ignore any response headers seen so far. htp_header_t *h = NULL; for (int i = 0, n = htp_table_size(connp->out_tx->response_headers); i < n; i++) { h = htp_table_get_index(connp->out_tx->response_headers, i, NULL); bstr_free(h->name); bstr_free(h->value); free(h); } htp_table_clear(connp->out_tx->response_headers); // Expecting to see another response line next. connp->out_state = htp_connp_RES_LINE; connp->out_tx->progress = HTP_RESPONSE_LINE; connp->out_tx->seen_100continue++; return HTP_OK; } // 1. Any response message which MUST NOT include a message-body // (such as the 1xx, 204, and 304 responses and any response to a HEAD // request) is always terminated by the first empty line after the // header fields, regardless of the entity-header fields present in the // message. if (((connp->out_tx->response_status_number >= 100) && (connp->out_tx->response_status_number <= 199)) || (connp->out_tx->response_status_number == 204) || (connp->out_tx->response_status_number == 304) || (connp->out_tx->request_method_number == HTP_M_HEAD)) { // There's no response body connp->out_tx->response_transfer_coding = HTP_CODING_NO_BODY; connp->out_state = htp_connp_RES_FINALIZE; } else { // We have a response body htp_header_t *ct = htp_table_get_c(connp->out_tx->response_headers, "content-type"); htp_header_t *cl = htp_table_get_c(connp->out_tx->response_headers, "content-length"); htp_header_t *te = htp_table_get_c(connp->out_tx->response_headers, "transfer-encoding"); if (ct != NULL) { connp->out_tx->response_content_type = bstr_dup_lower(ct->value); if (connp->out_tx->response_content_type == NULL) return HTP_ERROR; // Ignore parameters unsigned char *data = bstr_ptr(connp->out_tx->response_content_type); size_t len = bstr_len(ct->value); size_t newlen = 0; while (newlen < len) { // TODO Some platforms may do things differently here. if (htp_is_space(data[newlen]) || (data[newlen] == ';')) { bstr_adjust_len(connp->out_tx->response_content_type, newlen); break; } newlen++; } } // 2. If a Transfer-Encoding header field (section 14.40) is present and // indicates that the "chunked" transfer coding has been applied, then // the length is defined by the chunked encoding (section 3.6). if ((te != NULL) && (bstr_cmp_c(te->value, "chunked") == 0)) { // If the T-E header is present we are going to use it. connp->out_tx->response_transfer_coding = HTP_CODING_CHUNKED; // We are still going to check for the presence of C-L if (cl != NULL) { // This is a violation of the RFC connp->out_tx->flags |= HTP_REQUEST_SMUGGLING; } connp->out_state = htp_connp_RES_BODY_CHUNKED_LENGTH; connp->out_tx->progress = HTP_RESPONSE_BODY; }// 3. If a Content-Length header field (section 14.14) is present, its // value in bytes represents the length of the message-body. else if (cl != NULL) { // We know the exact length connp->out_tx->response_transfer_coding = HTP_CODING_IDENTITY; // Check for multiple C-L headers if (cl->flags & HTP_FIELD_REPEATED) { connp->out_tx->flags |= HTP_REQUEST_SMUGGLING; } // Get body length connp->out_tx->response_content_length = htp_parse_content_length(cl->value); if (connp->out_tx->response_content_length < 0) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Invalid C-L field in response: %d", connp->out_tx->response_content_length); return HTP_ERROR; } else { connp->out_content_length = connp->out_tx->response_content_length; connp->out_body_data_left = connp->out_content_length; if (connp->out_content_length != 0) { connp->out_state = htp_connp_RES_BODY_IDENTITY_CL_KNOWN; connp->out_tx->progress = HTP_RESPONSE_BODY; } else { connp->out_state = htp_connp_RES_FINALIZE; } } } else { // 4. If the message uses the media type "multipart/byteranges", which is // self-delimiting, then that defines the length. This media type MUST // NOT be used unless the sender knows that the recipient can parse it; // the presence in a request of a Range header with multiple byte-range // specifiers implies that the client can parse multipart/byteranges // responses. if (ct != NULL) { // TODO Handle multipart/byteranges if (bstr_index_of_c_nocase(ct->value, "multipart/byteranges") != -1) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "C-T multipart/byteranges in responses not supported"); return HTP_ERROR; } } // 5. By the server closing the connection. (Closing the connection // cannot be used to indicate the end of a request body, since that // would leave no possibility for the server to send back a response.) connp->out_state = htp_connp_RES_BODY_IDENTITY_STREAM_CLOSE; connp->out_tx->response_transfer_coding = HTP_CODING_IDENTITY; connp->out_tx->progress = HTP_RESPONSE_BODY; connp->out_body_data_left = -1; } } // NOTE We do not need to check for short-style HTTP/0.9 requests here because // that is done earlier, before response line parsing begins int rc = htp_tx_state_response_headers(connp->out_tx); if (rc != HTP_OK) return rc; return HTP_OK; }
/** * Parses request headers. * * @param[in] connp * @returns HTP_OK on state change, HTP_ERROR on error, or HTP_DATA when more data is needed. */ htp_status_t htp_connp_REQ_HEADERS(htp_connp_t *connp) { for (;;) { IN_COPY_BYTE_OR_RETURN(connp); // Have we reached the end of the line? if (connp->in_next_byte == LF) { unsigned char *data; size_t len; htp_connp_req_consolidate_data(connp, &data, &len); #ifdef HTP_DEBUG fprint_raw_data(stderr, __FUNCTION__, data, len); #endif // Should we terminate headers? if (htp_connp_is_line_terminator(connp, data, len)) { // Parse previous header, if any. if (connp->in_header != NULL) { if (connp->cfg->process_request_header(connp, bstr_ptr(connp->in_header), bstr_len(connp->in_header)) != HTP_OK) return HTP_ERROR; bstr_free(connp->in_header); connp->in_header = NULL; } htp_connp_req_clear_buffer(connp); // We've seen all the request headers. return htp_tx_state_request_headers(connp->in_tx); } htp_chomp(data, &len); // Check for header folding. if (htp_connp_is_line_folded(data, len) == 0) { // New header line. // Parse previous header, if any. if (connp->in_header != NULL) { if (connp->cfg->process_request_header(connp, bstr_ptr(connp->in_header), bstr_len(connp->in_header)) != HTP_OK) return HTP_ERROR; bstr_free(connp->in_header); connp->in_header = NULL; } IN_PEEK_NEXT(connp); if (htp_is_folding_char(connp->in_next_byte) == 0) { // Because we know this header is not folded, we can process the buffer straight away. if (connp->cfg->process_request_header(connp, data, len) != HTP_OK) return HTP_ERROR; } else { // Keep the partial header data for parsing later. connp->in_header = bstr_dup_mem(data, len); if (connp->in_header == NULL) return HTP_ERROR; } } else { // Folding; check that there's a previous header line to add to. if (connp->in_header == NULL) { // Invalid folding. // Warn only once per transaction. if (!(connp->in_tx->flags & HTP_INVALID_FOLDING)) { connp->in_tx->flags |= HTP_INVALID_FOLDING; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Invalid request field folding"); } // Keep the header data for parsing later. connp->in_header = bstr_dup_mem(data, len); if (connp->in_header == NULL) return HTP_ERROR; } else { // Add to the existing header. bstr *new_in_header = bstr_add_mem(connp->in_header, data, len); if (new_in_header == NULL) return HTP_ERROR; connp->in_header = new_in_header; } } htp_connp_req_clear_buffer(connp); } } return HTP_ERROR; }
htp_status_t htp_parse_request_line_generic_ex(htp_connp_t *connp, int nul_terminates) { htp_tx_t *tx = connp->in_tx; unsigned char *data = bstr_ptr(tx->request_line); size_t len = bstr_len(tx->request_line); size_t pos = 0; size_t mstart = 0; size_t start; size_t bad_delim; if (nul_terminates) { // The line ends with the first NUL byte. size_t newlen = 0; while ((pos < len) && (data[pos] != '\0')) { pos++; newlen++; } // Start again, with the new length. len = newlen; pos = 0; } // skip past leading whitespace. IIS allows this while ((pos < len) && htp_is_space(data[pos])) pos++; if (pos) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request line: leading whitespace"); mstart = pos; if (connp->cfg->requestline_leading_whitespace_unwanted != HTP_UNWANTED_IGNORE) { // reset mstart so that we copy the whitespace into the method mstart = 0; // set expected response code to this anomaly tx->response_status_expected_number = connp->cfg->requestline_leading_whitespace_unwanted; } } // The request method starts at the beginning of the // line and ends with the first whitespace character. while ((pos < len) && (!htp_is_space(data[pos]))) pos++; // No, we don't care if the method is empty. tx->request_method = bstr_dup_mem(data + mstart, pos - mstart); if (tx->request_method == NULL) return HTP_ERROR; #ifdef HTP_DEBUG fprint_raw_data(stderr, __FUNCTION__, bstr_ptr(tx->request_method), bstr_len(tx->request_method)); #endif tx->request_method_number = htp_convert_method_to_number(tx->request_method); bad_delim = 0; // Ignore whitespace after request method. The RFC allows // for only one SP, but then suggests any number of SP and HT // should be permitted. Apache uses isspace(), which is even // more permitting, so that's what we use here. while ((pos < len) && (isspace(data[pos]))) { if (!bad_delim && data[pos] != 0x20) { bad_delim++; } pos++; } if (bad_delim) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request line: non-compliant delimiter between Method and URI"); } // Is there anything after the request method? if (pos == len) { // No, this looks like a HTTP/0.9 request. tx->is_protocol_0_9 = 1; tx->request_protocol_number = HTP_PROTOCOL_0_9; return HTP_OK; } start = pos; bad_delim = 0; // The URI ends with the first whitespace. while ((pos < len) && (data[pos] != 0x20)) { if (!bad_delim && htp_is_space(data[pos])) { bad_delim++; } pos++; } /* if we've seen some 'bad' delimiters, we retry with those */ if (bad_delim && pos == len) { // special case: even though RFC's allow only SP (0x20), many // implementations allow other delimiters, like tab or other // characters that isspace() accepts. pos = start; while ((pos < len) && (!htp_is_space(data[pos]))) pos++; } if (bad_delim) { // warn regardless if we've seen non-compliant chars htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request line: URI contains non-compliant delimiter"); } tx->request_uri = bstr_dup_mem(data + start, pos - start); if (tx->request_uri == NULL) return HTP_ERROR; #ifdef HTP_DEBUG fprint_raw_data(stderr, __FUNCTION__, bstr_ptr(tx->request_uri), bstr_len(tx->request_uri)); #endif // Ignore whitespace after URI. while ((pos < len) && (htp_is_space(data[pos]))) pos++; // Is there protocol information available? if (pos == len) { // No, this looks like a HTTP/0.9 request. tx->is_protocol_0_9 = 1; tx->request_protocol_number = HTP_PROTOCOL_0_9; return HTP_OK; } // The protocol information continues until the end of the line. tx->request_protocol = bstr_dup_mem(data + pos, len - pos); if (tx->request_protocol == NULL) return HTP_ERROR; tx->request_protocol_number = htp_parse_protocol(tx->request_protocol); #ifdef HTP_DEBUG fprint_raw_data(stderr, __FUNCTION__, bstr_ptr(tx->request_protocol), bstr_len(tx->request_protocol)); #endif return HTP_OK; }
/** * Generic request header parser. * * @param[in] connp * @param[in] h * @param[in] data * @param[in] len * @return HTP_OK or HTP_ERROR */ htp_status_t htp_parse_request_header_generic(htp_connp_t *connp, htp_header_t *h, unsigned char *data, size_t len) { size_t name_start, name_end; size_t value_start, value_end; htp_chomp(data, &len); name_start = 0; // Look for the colon. size_t colon_pos = 0; while ((colon_pos < len) && (data[colon_pos] != '\0') && (data[colon_pos] != ':')) colon_pos++; if ((colon_pos == len) || (data[colon_pos] == '\0')) { // Missing colon. h->flags |= HTP_FIELD_UNPARSEABLE; // Log only once per transaction. if (!(connp->in_tx->flags & HTP_FIELD_UNPARSEABLE)) { connp->in_tx->flags |= HTP_FIELD_UNPARSEABLE; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request field invalid: colon missing"); } // We handle this case as a header with an empty name, with the value equal // to the entire input string. // TODO Apache will respond to this problem with a 400. // Now extract the name and the value h->name = bstr_dup_c(""); if (h->name == NULL) return HTP_ERROR; h->value = bstr_dup_mem(data, len); if (h->value == NULL) { bstr_free(h->name); return HTP_ERROR; } return HTP_OK; } if (colon_pos == 0) { // Empty header name. h->flags |= HTP_FIELD_INVALID; // Log only once per transaction. if (!(connp->in_tx->flags & HTP_FIELD_INVALID)) { connp->in_tx->flags |= HTP_FIELD_INVALID; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request field invalid: empty name"); } } name_end = colon_pos; // Ignore LWS after field-name. size_t prev = name_end; while ((prev > name_start) && (htp_is_lws(data[prev - 1]))) { // LWS after header name. prev--; name_end--; h->flags |= HTP_FIELD_INVALID; // Log only once per transaction. if (!(connp->in_tx->flags & HTP_FIELD_INVALID)) { connp->in_tx->flags |= HTP_FIELD_INVALID; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request field invalid: LWS after name"); } } // Header value. value_start = colon_pos; // Go over the colon. if (value_start < len) { value_start++; } // Ignore LWS before field-content. while ((value_start < len) && (htp_is_lws(data[value_start]))) { value_start++; } // Look for the end of field-content. value_end = value_start; while ((value_end < len) && (data[value_end] != '\0')) value_end++; // Ignore LWS after field-content. prev = value_end - 1; while ((prev > value_start) && (htp_is_lws(data[prev]))) { prev--; value_end--; } // Check that the header name is a token. size_t i = name_start; while (i < name_end) { if (!htp_is_token(data[i])) { // Incorrectly formed header name. h->flags |= HTP_FIELD_INVALID; // Log only once per transaction. if (!(connp->in_tx->flags & HTP_FIELD_INVALID)) { connp->in_tx->flags |= HTP_FIELD_INVALID; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request header name is not a token"); } break; } i++; } // Now extract the name and the value h->name = bstr_dup_mem(data + name_start, name_end - name_start); if (h->name == NULL) return HTP_ERROR; h->value = bstr_dup_mem(data + value_start, value_end - value_start); if (h->value == NULL) { bstr_free(h->name); return HTP_ERROR; } return HTP_OK; }
/** * Extract one request header. A header can span multiple lines, in * which case they will be folded into one before parsing is attempted. * * @param connp * @return HTP_OK or HTP_ERROR */ int htp_process_request_header_apache_2_2(htp_connp_t *connp) { bstr *tempstr = NULL; unsigned char *data = NULL; size_t len = 0; // Create new header structure htp_header_t *h = calloc(1, sizeof (htp_header_t)); if (h == NULL) return HTP_ERROR; // Ensure we have the necessary header data in a single buffer if (connp->in_header_line_index + 1 == connp->in_header_line_counter) { // Single line htp_header_line_t *hl = list_get(connp->in_tx->request_header_lines, connp->in_header_line_index); if (hl == NULL) { // Internal error htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Process request header (Apache 2.2): Internal error"); free(h); return HTP_ERROR; } data = (unsigned char *) bstr_ptr(hl->line); len = bstr_len(hl->line); hl->header = h; } else { // Multiple lines (folded) int i = 0; for (i = connp->in_header_line_index; i < connp->in_header_line_counter; i++) { htp_header_line_t *hl = list_get(connp->in_tx->request_header_lines, i); if (hl == NULL) { // Internal error htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Process request header (Apache 2.2): Internal error"); free(h); return HTP_ERROR; } len += bstr_len(hl->line); } tempstr = bstr_alloc(len); if (tempstr == NULL) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Process request header (Apache 2.2): Failed to allocate bstring of %d bytes", len); free(h); return HTP_ERROR; } for (i = connp->in_header_line_index; i < connp->in_header_line_counter; i++) { htp_header_line_t *hl = list_get(connp->in_tx->request_header_lines, i); if (hl == NULL) { // Internal error htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Process request header (Apache 2.2): Internal error"); bstr_free(tempstr); free(h); return HTP_ERROR; } char *data = bstr_ptr(hl->line); size_t len = bstr_len(hl->line); htp_chomp((unsigned char *)data, &len); bstr_add_mem_noex(tempstr, data, len); hl->header = h; } data = (unsigned char *) bstr_ptr(tempstr); } // Now try to parse the header if (htp_parse_request_header_apache_2_2(connp, h, data, len) != HTP_OK) { // Note: downstream responsible for error logging if (tempstr != NULL) { free(tempstr); } free(h); return HTP_ERROR; } // Do we already have a header with the same name? htp_header_t *h_existing = table_get(connp->in_tx->request_headers, h->name); if (h_existing != NULL) { // TODO Do we want to keep a list of the headers that are // allowed to be combined in this way? // Add to existing header h_existing->value = bstr_expand(h_existing->value, bstr_len(h_existing->value) + 2 + bstr_len(h->value)); bstr_add_mem_noex(h_existing->value, ", ", 2); bstr_add_str_noex(h_existing->value, h->value); // The header is no longer needed if (h->name != NULL) free(h->name); if (h->value != NULL) free(h->value); free(h); // Keep track of same-name headers h_existing->flags |= HTP_FIELD_REPEATED; } else { // Add as a new header table_add(connp->in_tx->request_headers, h->name, h); } if (tempstr != NULL) { free(tempstr); } return HTP_OK; }
int htp_connp_req_data(htp_connp_t *connp, const htp_time_t *timestamp, const void *data, size_t len) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data(connp->in_status %x)\n", connp->in_status); fprint_raw_data(stderr, __FUNCTION__, data, len); #endif // Return if the connection is in stop state. if (connp->in_status == HTP_STREAM_STOP) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_INFO, 0, "Inbound parser is in HTP_STREAM_STOP"); return HTP_STREAM_STOP; } // Return if the connection had a fatal error earlier if (connp->in_status == HTP_STREAM_ERROR) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Inbound parser is in HTP_STREAM_ERROR"); #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_DATA (previous error)\n"); #endif return HTP_STREAM_ERROR; } // If the length of the supplied data chunk is zero, proceed // only if the stream has been closed. We do not allow zero-sized // chunks in the API, but we use them internally to force the parsers // to finalize parsing. if (((data == NULL)||(len == 0)) && (connp->in_status != HTP_STREAM_CLOSED)) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Zero-length data chunks are not allowed"); #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_DATA (zero-length chunk)\n"); #endif return HTP_STREAM_CLOSED; } // Remember the timestamp of the current request data chunk if (timestamp != NULL) { memcpy(&connp->in_timestamp, timestamp, sizeof (*timestamp)); } // Store the current chunk information connp->in_current_data = (unsigned char *) data; connp->in_current_len = len; connp->in_current_read_offset = 0; connp->in_current_consume_offset = 0; connp->in_current_receiver_offset = 0; connp->in_chunk_count++; htp_conn_track_inbound_data(connp->conn, len, timestamp); // Return without processing any data if the stream is in tunneling // mode (which it would be after an initial CONNECT transaction). if (connp->in_status == HTP_STREAM_TUNNEL) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_TUNNEL\n"); #endif return HTP_STREAM_TUNNEL; } if (connp->out_status == HTP_STREAM_DATA_OTHER) { connp->out_status = HTP_STREAM_DATA; } // Invoke a processor, in a loop, until an error // occurs or until we run out of data. Many processors // will process a request, each pointing to the next // processor that needs to run. for (;;) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: in state=%s, progress=%s\n", htp_connp_in_state_as_string(connp), htp_tx_request_progress_as_string(connp->in_tx)); #endif // Return if there's been an error or if we've run out of data. We are relying // on processors to supply error messages, so we'll keep quiet here. htp_status_t rc = connp->in_state(connp); if (rc == HTP_OK) { if (connp->in_status == HTP_STREAM_TUNNEL) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_TUNNEL\n"); #endif return HTP_STREAM_TUNNEL; } rc = htp_req_handle_state_change(connp); } if (rc != HTP_OK) { // Do we need more data? if ((rc == HTP_DATA) || (rc == HTP_DATA_BUFFER)) { htp_connp_req_receiver_send_data(connp, 0 /* not last */); if (rc == HTP_DATA_BUFFER) { htp_connp_req_buffer(connp); } #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_DATA\n"); #endif connp->in_status = HTP_STREAM_DATA; return HTP_STREAM_DATA; } // Check for suspended parsing. if (rc == HTP_DATA_OTHER) { // We might have actually consumed the entire data chunk? if (connp->in_current_read_offset >= connp->in_current_len) { // Do not send STREAM_DATE_DATA_OTHER if we've consumed the entire chunk. #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_DATA (suspended parsing)\n"); #endif connp->in_status = HTP_STREAM_DATA; return HTP_STREAM_DATA; } else { // Partial chunk consumption. #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_DATA_OTHER\n"); #endif connp->in_status = HTP_STREAM_DATA_OTHER; return HTP_STREAM_DATA_OTHER; } } // Check for the stop signal. if (rc == HTP_STOP) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_STOP\n"); #endif connp->in_status = HTP_STREAM_STOP; return HTP_STREAM_STOP; } #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_req_data: returning HTP_STREAM_ERROR\n"); #endif // Permanent stream error. connp->in_status = HTP_STREAM_ERROR; return HTP_STREAM_ERROR; } } // Permanent stream error. connp->in_status = HTP_STREAM_ERROR; return HTP_STREAM_ERROR; }
/** * Parses a message header line as Apache 2.2 does. * * @param connp * @param h * @param data * @param len * @return HTP_OK or HTP_ERROR */ int htp_parse_request_header_apache_2_2(htp_connp_t *connp, htp_header_t *h, unsigned char *data, size_t len) { size_t name_start, name_end; size_t value_start, value_end; htp_chomp(data, &len); name_start = 0; // Look for the colon size_t colon_pos = 0; while ((colon_pos < len) && (data[colon_pos] != '\0') && (data[colon_pos] != ':')) colon_pos++; if ((colon_pos == len) || (data[colon_pos] == '\0')) { // Missing colon h->flags |= HTP_FIELD_UNPARSEABLE; if (!(connp->in_tx->flags & HTP_FIELD_UNPARSEABLE)) { connp->in_tx->flags |= HTP_FIELD_UNPARSEABLE; // Only log once per transaction htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Request field invalid: colon missing"); } return HTP_ERROR; } if (colon_pos == 0) { // Empty header name h->flags |= HTP_FIELD_INVALID; if (!(connp->in_tx->flags & HTP_FIELD_INVALID)) { connp->in_tx->flags |= HTP_FIELD_INVALID; // Only log once per transaction htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request field invalid: empty name"); } } name_end = colon_pos; // Ignore LWS after field-name size_t prev = name_end - 1; while ((prev > name_start) && (htp_is_lws(data[prev]))) { prev--; name_end--; h->flags |= HTP_FIELD_INVALID; if (!(connp->in_tx->flags & HTP_FIELD_INVALID)) { connp->in_tx->flags |= HTP_FIELD_INVALID; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request field invalid: LWS after name"); } } // Value value_start = colon_pos; // Go over the colon if (value_start < len) { value_start++; } // Ignore LWS before field-content while ((value_start < len) && (htp_is_lws(data[value_start]))) { value_start++; } // Look for the end of field-content value_end = value_start; while ((value_end < len) && (data[value_end] != '\0')) value_end++; // Ignore LWS after field-content prev = value_end - 1; while ((prev > value_start) && (htp_is_lws(data[prev]))) { prev--; value_end--; } // Check that the header name is a token size_t i = name_start; while (i < name_end) { if (!htp_is_token(data[i])) { h->flags |= HTP_FIELD_INVALID; if (!(connp->in_tx->flags & HTP_FIELD_INVALID)) { connp->in_tx->flags |= HTP_FIELD_INVALID; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request header name is not a token"); } break; } i++; } // Now extract the name and the value h->name = bstr_memdup((char *) data + name_start, name_end - name_start); h->value = bstr_memdup((char *) data + value_start, value_end - value_start); return HTP_OK; }