/*************************************************************************** * Copyright (c) 2009-2010 Open Information Security Foundation * Copyright (c) 2010-2013 Qualys, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - Neither the name of the Qualys, Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***************************************************************************/ /** * @file * @author Ivan Ristic */ #include "htp_config_auto.h" #include "htp_private.h" #define OUT_TEST_NEXT_BYTE_OR_RETURN(X) \ if ((X)->out_current_read_offset >= (X)->out_current_len) { \ return HTP_DATA; \ } #define OUT_PEEK_NEXT(X) \ if ((X)->out_current_read_offset >= (X)->out_current_len) { \ (X)->out_next_byte = -1; \ } else { \ (X)->out_next_byte = (X)->out_current_data[(X)->out_current_read_offset]; \ } #define OUT_NEXT_BYTE(X) \ if ((X)->out_current_read_offset < (X)->out_current_len) { \ (X)->out_next_byte = (X)->out_current_data[(X)->out_current_read_offset]; \ (X)->out_current_read_offset++; \ (X)->out_current_consume_offset++; \ (X)->out_stream_offset++; \ } else { \ (X)->out_next_byte = -1; \ } #define OUT_NEXT_BYTE_OR_RETURN(X) \ if ((X)->out_current_read_offset < (X)->out_current_len) { \ (X)->out_next_byte = (X)->out_current_data[(X)->out_current_read_offset]; \ (X)->out_current_read_offset++; \ (X)->out_current_consume_offset++; \ (X)->out_stream_offset++; \ } else { \ return HTP_DATA; \ } #define OUT_COPY_BYTE_OR_RETURN(X) \ if ((X)->out_current_read_offset < (X)->out_current_len) { \ (X)->out_next_byte = (X)->out_current_data[(X)->out_current_read_offset]; \ (X)->out_current_read_offset++; \ (X)->out_stream_offset++; \ } else { \ return HTP_DATA_BUFFER; \ } #define REQUEST_URI_NOT_SEEN "/libhtp::request_uri_not_seen" /** * Sends outstanding connection data to the currently active data receiver hook. * * @param[in] connp * @param[in] is_last * @return HTP_OK, or a value returned from a callback. */ static htp_status_t htp_connp_res_receiver_send_data(htp_connp_t *connp, int is_last) { if (connp->out_data_receiver_hook == NULL) return HTP_OK; htp_tx_data_t d; d.tx = connp->out_tx; d.data = connp->out_current_data + connp->out_current_receiver_offset; d.len = connp->out_current_read_offset - connp->out_current_receiver_offset; d.is_last = is_last; htp_status_t rc = htp_hook_run_all(connp->out_data_receiver_hook, &d); if (rc != HTP_OK) return rc; connp->out_current_receiver_offset = connp->out_current_read_offset; return HTP_OK; } /** * Finalizes an existing data receiver hook by sending any outstanding data to it. The * hook is then removed so that it receives no more data. * * @param[in] connp * @return HTP_OK, or a value returned from a callback. */ htp_status_t htp_connp_res_receiver_finalize_clear(htp_connp_t *connp) { if (connp->out_data_receiver_hook == NULL) return HTP_OK; htp_status_t rc = htp_connp_res_receiver_send_data(connp, 1 /* last */); connp->out_data_receiver_hook = NULL; return rc; } /** * Configures the data receiver hook. If there is a previous hook, it will be finalized and cleared. * * @param[in] connp * @param[in] data_receiver_hook * @return HTP_OK, or a value returned from a callback. */ static htp_status_t htp_connp_res_receiver_set(htp_connp_t *connp, htp_hook_t *data_receiver_hook) { htp_status_t rc = htp_connp_res_receiver_finalize_clear(connp); connp->out_data_receiver_hook = data_receiver_hook; connp->out_current_receiver_offset = connp->out_current_read_offset; return rc; } /** * Handles request parser state changes. At the moment, this function is used only * to configure data receivers, which are sent raw connection data. * * @param[in] connp * @return HTP_OK, or a value returned from a callback. */ static htp_status_t htp_res_handle_state_change(htp_connp_t *connp) { if (connp->out_state_previous == connp->out_state) return HTP_OK; if (connp->out_state == htp_connp_RES_HEADERS) { htp_status_t rc = HTP_OK; switch (connp->out_tx->response_progress) { case HTP_RESPONSE_HEADERS: rc = htp_connp_res_receiver_set(connp, connp->out_tx->cfg->hook_response_header_data); break; case HTP_RESPONSE_TRAILER: rc = htp_connp_res_receiver_set(connp, connp->out_tx->cfg->hook_response_trailer_data); break; default: // Do nothing; receivers are currently used only for header blocks. break; } if (rc != HTP_OK) return rc; } // Same comment as in htp_req_handle_state_change(). Below is a copy. // Initially, I had the finalization of raw data sending here, but that // caused the last REQUEST_HEADER_DATA hook to be invoked after the // REQUEST_HEADERS hook -- which I thought made no sense. For that reason, // the finalization is now initiated from the request header processing code, // which is less elegant but provides a better user experience. Having some // (or all) hooks to be invoked on state change might work better. connp->out_state_previous = connp->out_state; return HTP_OK; } /** * If there is any data left in the outbound 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_res_buffer(htp_connp_t *connp) { if (connp->out_current_data == NULL) return HTP_OK; 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; // When calculating the size of the buffer, take into account the // space we're using for the response header buffer. 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 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; } /** * Returns to the caller the memory region that should be processed next. This function * hides away the buffering process from the rest of the code, allowing it to work with * non-buffered data that's in the outbound chunk, or buffered data that's in our structures. * * @param[in] connp * @param[out] data * @param[out] len * @return HTP_OK */ static htp_status_t htp_connp_res_consolidate_data(htp_connp_t *connp, unsigned char **data, size_t *len) { if (connp->out_buf == NULL) { // We do not have any data buffered; point to the current data chunk. *data = connp->out_current_data + connp->out_current_consume_offset; *len = connp->out_current_read_offset - connp->out_current_consume_offset; } else { // We do have data in the buffer. Add data from the current // chunk, and point to the consolidated buffer. if (htp_connp_res_buffer(connp) != HTP_OK) { return HTP_ERROR; } *data = connp->out_buf; *len = connp->out_buf_size; } return HTP_OK; } /** * Clears buffered outbound data and resets the consumer position to the reader position. * * @param[in] connp */ static void htp_connp_res_clear_buffer(htp_connp_t *connp) { connp->out_current_consume_offset = connp->out_current_read_offset; if (connp->out_buf != NULL) { free(connp->out_buf); connp->out_buf = NULL; connp->out_buf_size = 0; } } /** * Consumes bytes until the end of the current line. * * @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_DATA_END(htp_connp_t *connp) { // TODO We shouldn't really see anything apart from CR and LF, // so we should warn about anything else. for (;;) { OUT_NEXT_BYTE_OR_RETURN(connp); connp->out_tx->response_message_len++; if (connp->out_next_byte == LF) { connp->out_state = htp_connp_RES_BODY_CHUNKED_LENGTH; return HTP_OK; } } return HTP_ERROR; } /** * Processes a chunk of data. * * @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_DATA(htp_connp_t *connp) { size_t bytes_to_consume; // Determine how many bytes we can consume. if (connp->out_current_len - connp->out_current_read_offset >= connp->out_chunked_length) { bytes_to_consume = connp->out_chunked_length; } else { bytes_to_consume = connp->out_current_len - connp->out_current_read_offset; } if (bytes_to_consume == 0) return HTP_DATA; // Consume the data. htp_status_t rc = htp_tx_res_process_body_data_ex(connp->out_tx, connp->out_current_data + connp->out_current_read_offset, bytes_to_consume); if (rc != HTP_OK) return rc; // Adjust the counters. connp->out_current_read_offset += bytes_to_consume; connp->out_current_consume_offset += bytes_to_consume; connp->out_stream_offset += bytes_to_consume; connp->out_chunked_length -= bytes_to_consume; // Have we seen the entire chunk? if (connp->out_chunked_length == 0) { connp->out_state = htp_connp_RES_BODY_CHUNKED_DATA_END; return HTP_OK; } return HTP_DATA; } static inline int is_chunked_ctl_char(const unsigned char c) { switch (c) { case 0x0d: case 0x0a: case 0x20: case 0x09: case 0x0b: case 0x0c: return 1; default: return 0; } } /** * Peeks ahead into the data to try to see if it starts with a valid Chunked * length field. * * @returns 1 if it looks valid, 0 if it looks invalid */ static inline int data_probe_chunk_length(htp_connp_t *connp) { if (connp->out_current_read_offset - connp->out_current_consume_offset < 8) { // not enough data so far, consider valid still return 1; } 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; size_t i = 0; while (i < len) { unsigned char c = data[i]; if (is_chunked_ctl_char(c)) { // ctl char, still good. } else if (isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { // real chunklen char return 1; } else { // leading junk, bad return 0; } i++; } return 1; } /** * 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? Or is this not chunked after all? if (connp->out_next_byte == LF || (!is_chunked_ctl_char((unsigned char) connp->out_next_byte) && !data_probe_chunk_length(connp))) { 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 int chunk_ext = 0; connp->out_chunked_length = htp_parse_chunked_length(data, len, &chunk_ext); if (chunk_ext == 1) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Request chunk extension"); } // empty chunk length line, lets try to continue if (connp->out_chunked_length == -1004) { connp->out_current_consume_offset = connp->out_current_read_offset; continue; } if (connp->out_chunked_length < 0) { // reset out_current_read_offset so htp_connp_RES_BODY_IDENTITY_STREAM_CLOSE // doesn't miss the first bytes if (len > (size_t)connp->out_current_read_offset) { connp->out_current_read_offset = 0; } else { connp->out_current_read_offset -= len; } connp->out_state = htp_connp_RES_BODY_IDENTITY_STREAM_CLOSE; connp->out_tx->response_transfer_coding = HTP_CODING_IDENTITY; htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Response chunk encoding: Invalid chunk length: %"PRId64"", connp->out_chunked_length); return HTP_OK; } 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; } return HTP_OK; } } return HTP_ERROR; } /** * Processes an identity response body of known 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_IDENTITY_CL_KNOWN(htp_connp_t *connp) { size_t bytes_to_consume; // Determine how many bytes we can consume. if (connp->out_current_len - connp->out_current_read_offset >= connp->out_body_data_left) { bytes_to_consume = connp->out_body_data_left; } else { bytes_to_consume = connp->out_current_len - connp->out_current_read_offset; } if (connp->out_status == HTP_STREAM_CLOSED) { connp->out_state = htp_connp_RES_FINALIZE; // Sends close signal to decompressors htp_status_t rc = htp_tx_res_process_body_data_ex(connp->out_tx, NULL, 0); return rc; } if (bytes_to_consume == 0) return HTP_DATA; // Consume the data. htp_status_t rc = htp_tx_res_process_body_data_ex(connp->out_tx, connp->out_current_data + connp->out_current_read_offset, bytes_to_consume); if (rc != HTP_OK) return rc; // Adjust the counters. connp->out_current_read_offset += bytes_to_consume; connp->out_current_consume_offset += bytes_to_consume; connp->out_stream_offset += bytes_to_consume; connp->out_body_data_left -= bytes_to_consume; // Have we seen the entire response body? if (connp->out_body_data_left == 0) { connp->out_state = htp_connp_RES_FINALIZE; // Tells decompressors to output partially decompressed data rc = htp_tx_res_process_body_data_ex(connp->out_tx, NULL, 0); return rc; } return HTP_DATA; } /** * Processes identity response body of unknown length. In this case, we assume the * response body consumes all data until the end of the stream. * * @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_IDENTITY_STREAM_CLOSE(htp_connp_t *connp) { // Consume all data from the input buffer. size_t bytes_to_consume = connp->out_current_len - connp->out_current_read_offset; #ifdef HTP_DEBUG fprintf(stderr, "bytes_to_consume %"PRIuMAX, (uintmax_t)bytes_to_consume); #endif if (bytes_to_consume != 0) { htp_status_t rc = htp_tx_res_process_body_data_ex(connp->out_tx, connp->out_current_data + connp->out_current_read_offset, bytes_to_consume); if (rc != HTP_OK) return rc; // Adjust the counters. connp->out_current_read_offset += bytes_to_consume; connp->out_current_consume_offset += bytes_to_consume; connp->out_stream_offset += bytes_to_consume; } // Have we seen the entire response body? if (connp->out_status == HTP_STREAM_CLOSED) { connp->out_state = htp_connp_RES_FINALIZE; return HTP_OK; } return HTP_DATA; } /** * 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 tunneling mode: on the // request side we'll now probe the tunnel data to see // if we need to parse or ignore it. So on the response // side we wrap up the tx and wait. connp->out_state = htp_connp_RES_FINALIZE; // we may have response headers htp_status_t rc = htp_tx_state_response_headers(connp->out_tx); return rc; } else if (connp->out_tx->response_status_number == 407) { // proxy telling us to auth if (connp->in_status != HTP_STREAM_ERROR) connp->in_status = HTP_STREAM_DATA; } else { // This is a failed CONNECT stream, which means that // we can unblock request parsing if (connp->in_status != HTP_STREAM_ERROR) 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; } } 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"); // Check for "101 Switching Protocol" response. // If it's seen, it means that traffic after empty line following headers // is no longer HTTP. We can treat it similarly to CONNECT. // Unlike CONNECT, however, upgrades from HTTP to HTTP seem // rather unlikely, so don't try to probe tunnel for nested HTTP, // and switch to tunnel mode right away. if (connp->out_tx->response_status_number == 101) { if (te == NULL && cl == NULL) { connp->out_state = htp_connp_RES_FINALIZE; if (connp->in_status != HTP_STREAM_ERROR) connp->in_status = HTP_STREAM_TUNNEL; connp->out_status = HTP_STREAM_TUNNEL; // we may have response headers htp_status_t rc = htp_tx_state_response_headers(connp->out_tx); return rc; } else { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Switching Protocol with Content-Length"); } } // Check for an interim "100 Continue" response. Ignore it if found, and revert back to RES_LINE. if (connp->out_tx->response_status_number == 100 && te == NULL) { int is100continue = 1; if (cl != NULL){ if (htp_parse_content_length(cl->value, connp) > 0) { is100continue = 0; } } if (is100continue) { if (connp->out_tx->seen_100continue != 0) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Already seen 100-Continue."); } // Ignore any response headers seen so far. htp_header_t *h = NULL; for (size_t 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->response_progress = HTP_RESPONSE_LINE; connp->out_tx->seen_100continue++; return HTP_OK; } } // A request can indicate it waits for headers validation // before sending its body cf // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect if (connp->out_tx->response_status_number >= 400 && connp->out_tx->response_status_number <= 499 && connp->in_content_length > 0 && connp->in_body_data_left == connp->in_content_length) { htp_header_t *exp = htp_table_get_c(connp->out_tx->request_headers, "expect"); if ((exp != NULL) && (bstr_cmp_c_nocase(exp->value, "100-continue") == 0)) { connp->in_state = htp_connp_REQ_FINALIZE; } } // 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->request_method_number == HTP_M_HEAD) { // There's no response body whatsoever connp->out_tx->response_transfer_coding = HTP_CODING_NO_BODY; connp->out_state = htp_connp_RES_FINALIZE; } else 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)) { // There should be no response body // but browsers interpret content sent by the server as such if (te == NULL && cl == NULL) { connp->out_tx->response_transfer_coding = HTP_CODING_NO_BODY; connp->out_state = htp_connp_RES_FINALIZE; } else { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Unexpected Response body"); } } // Hack condition to check that we do not assume "no body" if (connp->out_state != htp_connp_RES_FINALIZE) { // We have a response body htp_header_t *ct = htp_table_get_c(connp->out_tx->response_headers, "content-type"); 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_index_of_c_nocasenorzero(te->value, "chunked") != -1)) { if (bstr_cmp_c_nocase(te->value, "chunked") != 0) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Transfer-encoding has abnormal chunked value"); } // spec says chunked is HTTP/1.1 only, but some browsers accept it // with 1.0 as well if (connp->out_tx->response_protocol_number < HTP_PROTOCOL_1_1) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Chunked transfer-encoding on HTTP/0.9 or HTTP/1.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->response_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, connp); if (connp->out_tx->response_content_length < 0) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Invalid C-L field in response: %"PRId64"", 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->response_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->response_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 htp_status_t rc = htp_tx_state_response_headers(connp->out_tx); if (rc != HTP_OK) return rc; return HTP_OK; } /** * 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) { int endwithcr; int lfcrending = 0; for (;;) { if (connp->out_status == HTP_STREAM_CLOSED) { // 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->out_tx); if (rc != HTP_OK) return rc; connp->out_state = htp_connp_RES_FINALIZE; return HTP_OK; } OUT_COPY_BYTE_OR_RETURN(connp); // Have we reached the end of the line? if (connp->out_next_byte != LF && connp->out_next_byte != CR) { lfcrending = 0; } else { endwithcr = 0; if (connp->out_next_byte == CR) { OUT_PEEK_NEXT(connp); if (connp->out_next_byte == -1) { return HTP_DATA_BUFFER; } else if (connp->out_next_byte == LF) { OUT_COPY_BYTE_OR_RETURN(connp); if (lfcrending) { // Handling LFCRCRLFCRLF // These 6 characters mean only 2 end of lines OUT_PEEK_NEXT(connp); if (connp->out_next_byte == CR) { OUT_COPY_BYTE_OR_RETURN(connp); connp->out_current_consume_offset++; OUT_PEEK_NEXT(connp); if (connp->out_next_byte == LF) { OUT_COPY_BYTE_OR_RETURN(connp); connp->out_current_consume_offset++; htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Weird response end of lines mix"); } } } } else if (connp->out_next_byte == CR) { continue; } lfcrending = 0; endwithcr = 1; } else { // connp->out_next_byte == LF OUT_PEEK_NEXT(connp); lfcrending = 0; if (connp->out_next_byte == CR) { // hanldes LF-CR sequence as end of line OUT_COPY_BYTE_OR_RETURN(connp); lfcrending = 1; } } unsigned char *data; size_t len; if (htp_connp_res_consolidate_data(connp, &data, &len) != HTP_OK) { return HTP_ERROR; } // CRCRLF is not an empty line if (endwithcr && len < 2) { continue; } #ifdef HTP_DEBUG fprint_raw_data(stderr, __func__, data, len); #endif int next_no_lf = 0; if (connp->out_current_read_offset < connp->out_current_len && connp->out_current_data[connp->out_current_read_offset] != LF) { next_no_lf = 1; } // Should we terminate headers? if (htp_connp_is_line_terminator(connp, data, len, next_no_lf)) { // 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->response_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->out_tx); 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. size_t trim = 0; while(trim < len) { if (!htp_is_folding_char(data[trim])) { break; } trim++; } connp->out_header = bstr_dup_mem(data + trim, len - trim); if (connp->out_header == NULL) return HTP_ERROR; } else { size_t colon_pos = 0; while ((colon_pos < len) && (data[colon_pos] != ':')) colon_pos++; if (colon_pos < len && bstr_chr(connp->out_header, ':') >= 0 && connp->out_tx->response_protocol_number == HTP_PROTOCOL_1_1) { // 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"); } 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 = bstr_dup_mem(data+1, len-1); if (connp->out_header == NULL) return HTP_ERROR; } else { // Add to the existing header. if (bstr_len(connp->out_header) < HTP_MAX_HEADER_FOLDED) { 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; } else { htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Response field length exceeds folded maximum"); } } } } htp_connp_res_clear_buffer(connp); } } return HTP_ERROR; } /** * Parses response line. * * @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_LINE(htp_connp_t *connp) { for (;;) { // Don't try to get more data if the stream is closed. If we do, we'll return, asking for more data. if (connp->out_status != HTP_STREAM_CLOSED) { // Get one byte OUT_COPY_BYTE_OR_RETURN(connp); } // Have we reached the end of the line? We treat stream closure as end of line in // order to handle the case when the first line of the response is actually response body // (and we wish it processed as such). if (connp->out_next_byte == CR) { OUT_PEEK_NEXT(connp); if (connp->out_next_byte == -1) { return HTP_DATA_BUFFER; } else if (connp->out_next_byte == LF) { continue; } connp->out_next_byte = LF; } if ((connp->out_next_byte == LF)||(connp->out_status == HTP_STREAM_CLOSED)) { unsigned char *data; size_t len; if (htp_connp_res_consolidate_data(connp, &data, &len) != HTP_OK) { return HTP_ERROR; } #ifdef HTP_DEBUG fprint_raw_data(stderr, __func__, data, len); #endif // Is this a line that should be ignored? if (htp_connp_is_line_ignorable(connp, data, len)) { if (connp->out_status == HTP_STREAM_CLOSED) { connp->out_state = htp_connp_RES_FINALIZE; } // We have an empty/whitespace line, which we'll note, ignore and move on connp->out_tx->response_ignored_lines++; // TODO How many lines are we willing to accept? // Start again htp_connp_res_clear_buffer(connp); return HTP_OK; } // Deallocate previous response line allocations, which we would have on a 100 response. if (connp->out_tx->response_line != NULL) { bstr_free(connp->out_tx->response_line); connp->out_tx->response_line = NULL; } if (connp->out_tx->response_protocol != NULL) { bstr_free(connp->out_tx->response_protocol); connp->out_tx->response_protocol = NULL; } if (connp->out_tx->response_status != NULL) { bstr_free(connp->out_tx->response_status); connp->out_tx->response_status = NULL; } if (connp->out_tx->response_message != NULL) { bstr_free(connp->out_tx->response_message); connp->out_tx->response_message = NULL; } // Process response line. int chomp_result = htp_chomp(data, &len); // If the response line is invalid, determine if it _looks_ like // a response line. If it does not look like a line, process the // data as a response body because that is what browsers do. if (htp_treat_response_line_as_body(data, len)) { // if we have a next line beginning with H, skip this one if (connp->out_current_read_offset+1 < connp->out_current_len && (connp->out_current_data[connp->out_current_read_offset] == 'H' || len <= 2)) { connp->out_tx->response_ignored_lines++; htp_connp_res_clear_buffer(connp); return HTP_OK; } connp->out_tx->response_content_encoding_processing = HTP_COMPRESSION_NONE; connp->out_current_consume_offset = connp->out_current_read_offset; htp_status_t rc = htp_tx_res_process_body_data_ex(connp->out_tx, data, len + chomp_result); htp_connp_res_clear_buffer(connp); if (rc != HTP_OK) return rc; // Continue to process response body. Because we don't have // any headers to parse, we assume the body continues until // the end of the stream. // Have we seen the entire response body? if (connp->out_current_len <= connp->out_current_read_offset) { connp->out_tx->response_transfer_coding = HTP_CODING_IDENTITY; connp->out_tx->response_progress = HTP_RESPONSE_BODY; connp->out_body_data_left = -1; connp->out_state = htp_connp_RES_FINALIZE; } return HTP_OK; } connp->out_tx->response_line = bstr_dup_mem(data, len); if (connp->out_tx->response_line == NULL) return HTP_ERROR; if (connp->cfg->parse_response_line(connp) != HTP_OK) return HTP_ERROR; htp_status_t rc = htp_tx_state_response_line(connp->out_tx); if (rc != HTP_OK) return rc; htp_connp_res_clear_buffer(connp); // Move on to the next phase. connp->out_state = htp_connp_RES_HEADERS; connp->out_tx->response_progress = HTP_RESPONSE_HEADERS; return HTP_OK; } } return HTP_ERROR; } size_t htp_connp_res_data_consumed(htp_connp_t *connp) { return connp->out_current_read_offset; } htp_status_t htp_connp_RES_FINALIZE(htp_connp_t *connp) { if (connp->out_status != HTP_STREAM_CLOSED) { OUT_PEEK_NEXT(connp); if (connp->out_next_byte == -1) { return htp_tx_state_response_complete_ex(connp->out_tx, 0); } if (connp->out_next_byte != LF || connp->out_current_consume_offset >= connp->out_current_read_offset) { for (;;) {//;i < max_read; i++) { OUT_COPY_BYTE_OR_RETURN(connp); // Have we reached the end of the line? For some reason // we can't test after IN_COPY_BYTE_OR_RETURN */ if (connp->out_next_byte == LF) break; } } } size_t bytes_left; unsigned char * data; if (htp_connp_res_consolidate_data(connp, &data, &bytes_left) != HTP_OK) { return HTP_ERROR; } #ifdef HTP_DEBUG fprint_raw_data(stderr, "PROBING response finalize", data, bytes_left); #endif if (bytes_left == 0) { //closing return htp_tx_state_response_complete_ex(connp->out_tx, 0); } if (htp_treat_response_line_as_body(data, bytes_left)) { // Interpret remaining bytes as body data htp_log(connp, HTP_LOG_MARK, HTP_LOG_WARNING, 0, "Unexpected response body"); htp_status_t rc = htp_tx_res_process_body_data_ex(connp->out_tx, data, bytes_left); htp_connp_res_clear_buffer(connp); return rc; } //unread last end of line so that RES_LINE works if (connp->out_current_read_offset < (int64_t)bytes_left) { connp->out_current_read_offset=0; } else { connp->out_current_read_offset-=bytes_left; } if (connp->out_current_read_offset < connp->out_current_consume_offset) { connp->out_current_consume_offset=connp->out_current_read_offset; } return htp_tx_state_response_complete_ex(connp->out_tx, 0 /* not hybrid mode */); } /** * 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 // If there is none, we just create one so that responses without // request can still be processed. 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"); // finalize dangling request waiting for next request or body if (connp->in_state == htp_connp_REQ_FINALIZE) { htp_tx_state_request_complete(connp->in_tx); } connp->out_tx = htp_connp_tx_create(connp); if (connp->out_tx == NULL) { return HTP_ERROR; } connp->out_tx->parsed_uri = htp_uri_alloc(); if (connp->out_tx->parsed_uri == NULL) { return HTP_ERROR; } connp->out_tx->parsed_uri->path = bstr_dup_c(REQUEST_URI_NOT_SEEN); if (connp->out_tx->parsed_uri->path == NULL) { return HTP_ERROR; } connp->out_tx->request_uri = bstr_dup_c(REQUEST_URI_NOT_SEEN); if (connp->out_tx->request_uri == NULL) { return HTP_ERROR; } connp->in_state = htp_connp_REQ_FINALIZE; #ifdef HTP_DEBUG fprintf(stderr, "picked up response w/o request"); #endif // We've used one transaction connp->out_next_tx_index++; } else { // 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; } htp_status_t rc = htp_tx_state_response_start(connp->out_tx); if (rc != HTP_OK) return rc; return HTP_OK; } int htp_connp_res_data(htp_connp_t *connp, const htp_time_t *timestamp, const void *data, size_t len) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data(connp->out_status %x)\n", connp->out_status); fprint_raw_data(stderr, __func__, data, len); #endif // Return if the connection is in stop state if (connp->out_status == HTP_STREAM_STOP) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_INFO, 0, "Outbound parser is in HTP_STREAM_STOP"); return HTP_STREAM_STOP; } // Return if the connection has had a fatal error if (connp->out_status == HTP_STREAM_ERROR) { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Outbound parser is in HTP_STREAM_ERROR"); #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_DATA (previous error)\n"); #endif return HTP_STREAM_ERROR; } // Sanity check: we must have a transaction pointer if the state is not IDLE (no outbound transaction) if ((connp->out_tx == NULL)&&(connp->out_state != htp_connp_RES_IDLE)) { connp->out_status = HTP_STREAM_ERROR; htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Missing outbound transaction data"); 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 it internally to force the parsers // to finalize parsing. if (len == 0 && connp->out_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_res_data: returning HTP_STREAM_DATA (zero-length chunk)\n"); #endif return HTP_STREAM_CLOSED; } // Remember the timestamp of the current response data chunk if (timestamp != NULL) { memcpy(&connp->out_timestamp, timestamp, sizeof (*timestamp)); } // Store the current chunk information connp->out_current_data = (unsigned char *) data; connp->out_current_len = len; connp->out_current_read_offset = 0; connp->out_current_consume_offset = 0; connp->out_current_receiver_offset = 0; htp_conn_track_outbound_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->out_status == HTP_STREAM_TUNNEL) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_TUNNEL\n"); #endif return HTP_STREAM_TUNNEL; } // 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_res_data: out state=%s, progress=%s\n", htp_connp_out_state_as_string(connp), htp_tx_response_progress_as_string(connp->out_tx)); #endif // Return if there's been an error // or if we've run out of data. We are relying // on processors to add error messages, so we'll // keep quiet here. htp_status_t rc; //handle gap if (data == NULL && len > 0) { if (connp->out_state == htp_connp_RES_BODY_IDENTITY_CL_KNOWN || connp->out_state == htp_connp_RES_BODY_IDENTITY_STREAM_CLOSE) { rc = connp->out_state(connp); } else if (connp->out_state == htp_connp_RES_FINALIZE) { rc = htp_tx_state_response_complete_ex(connp->out_tx, 0); } else { htp_log(connp, HTP_LOG_MARK, HTP_LOG_ERROR, 0, "Gaps are not allowed during this state"); return HTP_STREAM_CLOSED; } } else { rc = connp->out_state(connp); } if (rc == HTP_OK) { if (connp->out_status == HTP_STREAM_TUNNEL) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_TUNNEL\n"); #endif return HTP_STREAM_TUNNEL; } rc = htp_res_handle_state_change(connp); } if (rc != HTP_OK) { // Do we need more data? if ((rc == HTP_DATA) || (rc == HTP_DATA_BUFFER)) { htp_connp_res_receiver_send_data(connp, 0 /* not last */); if (rc == HTP_DATA_BUFFER) { if (htp_connp_res_buffer(connp) != HTP_OK) { connp->out_status = HTP_STREAM_ERROR; return HTP_STREAM_ERROR; } } #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_DATA\n"); #endif connp->out_status = HTP_STREAM_DATA; return HTP_STREAM_DATA; } // Check for stop if (rc == HTP_STOP) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_STOP\n"); #endif connp->out_status = HTP_STREAM_STOP; return HTP_STREAM_STOP; } // Check for suspended parsing if (rc == HTP_DATA_OTHER) { // We might have actually consumed the entire data chunk? if (connp->out_current_read_offset >= connp->out_current_len) { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_DATA (suspended parsing)\n"); #endif connp->out_status = HTP_STREAM_DATA; // Do not send STREAM_DATE_DATA_OTHER if we've // consumed the entire chunk return HTP_STREAM_DATA; } else { #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_DATA_OTHER\n"); #endif connp->out_status = HTP_STREAM_DATA_OTHER; // Partial chunk consumption return HTP_STREAM_DATA_OTHER; } } #ifdef HTP_DEBUG fprintf(stderr, "htp_connp_res_data: returning HTP_STREAM_ERROR\n"); #endif // Permanent stream error. connp->out_status = HTP_STREAM_ERROR; return HTP_STREAM_ERROR; } } }