diff options
Diffstat (limited to '')
-rw-r--r-- | src/lib-http/http-server-response.c | 801 |
1 files changed, 801 insertions, 0 deletions
diff --git a/src/lib-http/http-server-response.c b/src/lib-http/http-server-response.c new file mode 100644 index 0000000..8d55593 --- /dev/null +++ b/src/lib-http/http-server-response.c @@ -0,0 +1,801 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "array.h" +#include "istream.h" +#include "ostream-private.h" +#include "http-date.h" +#include "http-transfer.h" +#include "http-server-private.h" + +struct http_server_response_payload { + struct http_server_response *resp; + struct const_iovec *iov; + unsigned int iov_count, iov_idx; + size_t iov_pos; +}; + +/* + * Response + */ + +static void http_server_response_update_event(struct http_server_response *resp) +{ + event_add_int(resp->event, "status", resp->status); + event_set_append_log_prefix(resp->event, + t_strdup_printf("%u response: ", + resp->status)); +} + +struct http_server_response * +http_server_response_create(struct http_server_request *req, + unsigned int status, const char *reason) +{ + struct http_server_response *resp; + + i_assert(req->state < HTTP_SERVER_REQUEST_STATE_SENT_RESPONSE); + + if (req->response == NULL) { + resp = req->response = p_new(req->pool, + struct http_server_response, 1); + } else { + /* Was already composing a response, but decided to + start a new one (would usually be a failure response) + */ + resp = req->response; + + ARRAY_TYPE(string) perm_headers = resp->perm_headers; + i_zero(&resp->perm_headers); + + http_server_response_request_free(resp); + i_zero(resp); + + resp->perm_headers = perm_headers; + } + + resp->request = req; + resp->status = status; + resp->reason = p_strdup(req->pool, reason); + resp->headers = str_new(default_pool, 256); + resp->date = (time_t)-1; + resp->event = event_create(req->event); + http_server_response_update_event(resp); + + if (array_is_created(&resp->perm_headers)) { + unsigned int i, count; + char *const *headers = array_get(&resp->perm_headers, &count); + for (i = 0; i < count; i += 2) + http_server_response_add_header(resp, headers[i], + headers[i+1]); + } + return resp; +} + +void http_server_response_request_free(struct http_server_response *resp) +{ + e_debug(resp->event, "Free"); + + /* Cannot be destroyed while payload output stream still exists */ + i_assert(resp->payload_stream == NULL); + + i_stream_unref(&resp->payload_input); + o_stream_unref(&resp->payload_output); + event_unref(&resp->event); + str_free(&resp->headers); + + if (array_is_created(&resp->perm_headers)) { + char *headers; + + array_foreach_elem(&resp->perm_headers, headers) + i_free(headers); + array_free(&resp->perm_headers); + } +} + +void http_server_response_request_destroy(struct http_server_response *resp) +{ + e_debug(resp->event, "Destroy"); + + if (resp->payload_stream != NULL) + http_server_ostream_response_destroyed(resp->payload_stream); +} + +void http_server_response_request_abort(struct http_server_response *resp, + const char *reason) +{ + if (reason == NULL) + e_debug(resp->event, "Abort"); + else + e_debug(resp->event, "Abort: %s", reason); + + if (resp->payload_stream != NULL) { + http_server_ostream_set_error(resp->payload_stream, + EPIPE, reason); + } +} + +void http_server_response_ref(struct http_server_response *resp) +{ + http_server_request_ref(resp->request); +} + +bool http_server_response_unref(struct http_server_response **_resp) +{ + struct http_server_response *resp = *_resp; + struct http_server_request *req; + + *_resp = NULL; + if (resp == NULL) + return FALSE; + + req = resp->request; + return http_server_request_unref(&req); +} + +void http_server_response_add_header(struct http_server_response *resp, + const char *key, const char *value) +{ + i_assert(!resp->submitted); + i_assert(strchr(key, '\r') == NULL && strchr(key, '\n') == NULL); + i_assert(strchr(value, '\r') == NULL && strchr(value, '\n') == NULL); + + /* Mark presence of special headers */ + switch (key[0]) { + case 'c': case 'C': + if (strcasecmp(key, "Connection") == 0) + resp->have_hdr_connection = TRUE; + else if (strcasecmp(key, "Content-Length") == 0) + resp->have_hdr_body_spec = TRUE; + break; + case 'd': case 'D': + if (strcasecmp(key, "Date") == 0) + resp->have_hdr_date = TRUE; + break; + case 't': case 'T': + if (strcasecmp(key, "Transfer-Encoding") == 0) + resp->have_hdr_body_spec = TRUE; + break; + } + str_printfa(resp->headers, "%s: %s\r\n", key, value); +} + +void http_server_response_update_status(struct http_server_response *resp, + unsigned int status, + const char *reason) +{ + i_assert(!resp->submitted); + resp->status = status; + /* Free not called because pool is alloconly */ + resp->reason = p_strdup(resp->request->pool, reason); +} + +void http_server_response_set_date(struct http_server_response *resp, + time_t date) +{ + i_assert(!resp->submitted); + + resp->date = date; +} + +void http_server_response_set_payload(struct http_server_response *resp, + struct istream *input) +{ + int ret; + + i_assert(!resp->submitted); + i_assert(resp->payload_input == NULL); + i_assert(resp->payload_stream == NULL); + + i_stream_ref(input); + resp->payload_input = input; + if ((ret = i_stream_get_size(input, TRUE, &resp->payload_size)) <= 0) { + if (ret < 0) { + e_error(resp->event, "i_stream_get_size(%s) failed: %s", + i_stream_get_name(input), + i_stream_get_error(input)); + } + resp->payload_size = 0; + resp->payload_chunked = TRUE; + } else { + i_assert(input->v_offset <= resp->payload_size); + resp->payload_size -= input->v_offset; + } + resp->payload_offset = input->v_offset; +} + +void http_server_response_set_payload_data(struct http_server_response *resp, + const unsigned char *data, + size_t size) +{ + struct istream *input; + unsigned char *payload_data; + + i_assert(!resp->submitted); + i_assert(resp->payload_input == NULL); + i_assert(resp->payload_stream == NULL); + + if (size == 0) + return; + + payload_data = p_malloc(resp->request->pool, size); + memcpy(payload_data, data, size); + input = i_stream_create_from_data(payload_data, size); + + http_server_response_set_payload(resp, input); + i_stream_unref(&input); +} + +struct ostream * +http_server_response_get_payload_output(struct http_server_response *resp, + size_t max_buffer_size, bool blocking) +{ + struct http_server_request *req = resp->request; + struct http_server_connection *conn = req->conn; + struct ostream *output; + + i_assert(conn != NULL); + i_assert(!resp->submitted); + i_assert(resp->payload_input == NULL); + i_assert(resp->payload_stream == NULL); + + output = http_server_ostream_create(resp, max_buffer_size, blocking); + o_stream_set_name(output, + t_strdup_printf("(conn %s: request %s: %u response payload)", + conn->conn.label, + http_server_request_label(req), resp->status)); + return output; +} + +void http_server_response_add_auth(struct http_server_response *resp, + const struct http_auth_challenge *chlng) +{ + struct http_auth_challenge *new; + pool_t pool = resp->request->pool; + + if (!array_is_created(&resp->auth_challenges)) + p_array_init(&resp->auth_challenges, pool, 4); + + new = array_append_space(&resp->auth_challenges); + http_auth_challenge_copy(pool, new, chlng); +} + +void http_server_response_add_auth_basic(struct http_server_response *resp, + const char *realm) +{ + struct http_auth_challenge chlng; + + http_auth_basic_challenge_init(&chlng, realm); + http_server_response_add_auth(resp, &chlng); +} + +static void +http_server_response_do_submit(struct http_server_response *resp) +{ + i_assert(!resp->submitted); + if (resp->date == (time_t)-1) + resp->date = ioloop_time; + resp->submitted = TRUE; + http_server_request_submit_response(resp->request); +} + +void http_server_response_submit(struct http_server_response *resp) +{ + e_debug(resp->event, "Submitted"); + + http_server_response_do_submit(resp); +} + +void http_server_response_submit_close(struct http_server_response *resp) +{ + http_server_request_connection_close(resp->request, TRUE); + http_server_response_submit(resp); +} + +void http_server_response_submit_tunnel(struct http_server_response *resp, + http_server_tunnel_callback_t callback, + void *context) +{ + e_debug(resp->event, "Started tunnelling"); + + resp->tunnel_callback = callback; + resp->tunnel_context = context; + http_server_request_connection_close(resp->request, TRUE); + http_server_response_do_submit(resp); +} + +static int +http_server_response_flush_payload(struct http_server_response *resp) +{ + struct http_server_request *req = resp->request; + struct http_server_connection *conn = req->conn; + int ret; + + if (resp->payload_output != conn->conn.output && + (ret = o_stream_finish(resp->payload_output)) <= 0) { + if (ret < 0) + http_server_connection_handle_output_error(conn); + else + http_server_connection_start_idle_timeout(conn); + return ret; + } + + return 1; +} + +void http_server_response_request_finished(struct http_server_response *resp) +{ + e_debug(resp->event, "Finished"); + + if (resp->payload_stream != NULL) + http_server_ostream_response_finished(resp->payload_stream); + + event_add_int(resp->request->event, "status_code", resp->status); +} + +int http_server_response_finish_payload_out(struct http_server_response *resp) +{ + struct http_server_request *req = resp->request; + struct http_server_connection *conn = req->conn; + int ret; + + if (req->state >= HTTP_SERVER_REQUEST_STATE_FINISHED) + return 1; + + resp->payload_finished = TRUE; + + if (resp->payload_output != NULL) { + ret = http_server_response_flush_payload(resp); + if (ret < 0) + return -1; + if (ret == 0) { + e_debug(resp->event, + "Not quite finished sending payload"); + return 0; + } + o_stream_unref(&resp->payload_output); + resp->payload_output = NULL; + } + + e_debug(resp->event, "Finished sending payload"); + + http_server_connection_ref(conn); + conn->output_locked = FALSE; + if (conn->conn.output != NULL && !conn->conn.output->closed) { + if (resp->payload_corked && + o_stream_uncork_flush(conn->conn.output) < 0) + http_server_connection_handle_output_error(conn); + o_stream_set_flush_callback(conn->conn.output, + http_server_connection_output, + conn); + } + + if (conn->request_queue_head == NULL || + (conn->request_queue_head->state != + HTTP_SERVER_REQUEST_STATE_PROCESSING)) + http_server_connection_start_idle_timeout(conn); + + http_server_request_finished(resp->request); + http_server_connection_unref(&conn); + return 1; +} + +static int +http_server_response_output_payload(struct http_server_response **_resp, + const unsigned char *data, size_t size) +{ + struct http_server_response *resp = *_resp; + struct http_server_request *req = resp->request; + struct ostream *output; + ssize_t sret; + int ret; + + i_assert(req->state < HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE || + req->state == HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT); + + http_server_response_ref(resp); + + if (resp->payload_stream == NULL) { + output = http_server_response_get_payload_output( + resp, IO_BLOCK_SIZE, TRUE); + } else { + output = http_server_ostream_get_output(resp->payload_stream); + } + + if (data != NULL) { + if ((sret = o_stream_send(output, data, size)) < 0) { + *_resp = NULL; + o_stream_destroy(&output); + http_server_response_unref(&resp); + return -1; + } + i_assert((size_t)sret == size); + } else { + if ((ret = o_stream_finish(output)) < 0) { + *_resp = NULL; + o_stream_destroy(&output); + http_server_response_unref(&resp); + return -1; + } + i_assert(ret > 0); + } + + switch (req->state) { + case HTTP_SERVER_REQUEST_STATE_FINISHED: + ret = 1; + break; + case HTTP_SERVER_REQUEST_STATE_ABORTED: + e_debug(resp->event, + "Request aborted while sending blocking payload"); + ret = -1; + break; + default: + ret = 0; + break; + } + + if (data == NULL) + o_stream_destroy(&output); + + /* Callback may have messed with our pointer, so unref using local + variable */ + if (!http_server_response_unref(&resp)) + *_resp = NULL; + + /* Return status */ + return ret; +} + +int http_server_response_send_payload(struct http_server_response **_resp, + const unsigned char *data, size_t size) +{ + struct http_server_response *resp = *_resp; + int ret; + + resp->payload_corked = TRUE; + + i_assert(data != NULL); + + ret = http_server_response_output_payload(&resp, data, size); + if (ret < 0) + *_resp = NULL; + else { + i_assert(ret == 0); + i_assert(resp != NULL); + } + return ret; +} + +int http_server_response_finish_payload(struct http_server_response **_resp) +{ + struct http_server_response *resp = *_resp; + int ret; + + *_resp = NULL; + ret = http_server_response_output_payload(&resp, NULL, 0); + i_assert(ret != 0); + return ret < 0 ? -1 : 0; +} + +void http_server_response_abort_payload(struct http_server_response **_resp) +{ + struct http_server_response *resp = *_resp; + struct http_server_request *req = resp->request; + + *_resp = NULL; + + http_server_request_abort(&req, "Aborted sending response payload"); +} + +static void +http_server_response_payload_input(struct http_server_response *resp) +{ + struct http_server_connection *conn = resp->request->conn; + + io_remove(&conn->io_resp_payload); + + (void)http_server_connection_output(conn); +} + +int http_server_response_send_more(struct http_server_response *resp) +{ + struct http_server_connection *conn = resp->request->conn; + struct ostream *output = resp->payload_output; + enum ostream_send_istream_result res; + + i_assert(resp->payload_output != NULL); + + if (resp->payload_finished) { + e_debug(resp->event, "Finish sending payload (more)"); + return http_server_response_finish_payload_out(resp); + } + + if (resp->payload_stream != NULL) { + conn->output_locked = TRUE; + return http_server_ostream_continue(resp->payload_stream); + } + + i_assert(resp->payload_input != NULL); + io_remove(&conn->io_resp_payload); + + /* Chunked ostream needs to write to the parent stream's buffer */ + o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE); + res = o_stream_send_istream(output, resp->payload_input); + o_stream_set_max_buffer_size(output, SIZE_MAX); + + switch (res) { + case OSTREAM_SEND_ISTREAM_RESULT_FINISHED: + /* Finished sending */ + if (!resp->payload_chunked && + (resp->payload_input->v_offset - resp->payload_offset) != + resp->payload_size) { + e_error(resp->event, + "Payload stream %s size changed unexpectedly", + i_stream_get_name(resp->payload_input)); + http_server_connection_close( + &conn, "Payload read failure"); + return -1; + } + /* Finished sending payload */ + e_debug(resp->event, "Finish sending payload"); + return http_server_response_finish_payload_out(resp); + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT: + /* Input is blocking (server needs to act; disable timeout) */ + conn->output_locked = TRUE; + http_server_connection_stop_idle_timeout(conn); + conn->io_resp_payload = io_add_istream(resp->payload_input, + http_server_response_payload_input, resp); + return 1; + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT: + /* Output is blocking (client needs to act; enable timeout) */ + conn->output_locked = TRUE; + http_server_connection_start_idle_timeout(conn); + o_stream_set_flush_pending(output, TRUE); + //e_debug(resp->event, "Partially sent payload"); + return 0; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT: + /* We're in the middle of sending a response, so the connection + will also have to be aborted */ + e_error(resp->event, "read(%s) failed: %s", + i_stream_get_name(resp->payload_input), + i_stream_get_error(resp->payload_input)); + http_server_connection_close(&conn, + "Payload read failure"); + return -1; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT: + /* Failed to send response */ + http_server_connection_handle_output_error(conn); + return -1; + } + i_unreached(); +} + +static int http_server_response_send_real(struct http_server_response *resp) +{ + struct http_server_request *req = resp->request; + struct http_server_connection *conn = req->conn; + string_t *rtext = t_str_new(256); + struct const_iovec iov[3]; + uoff_t content_length = 0; + bool chunked = FALSE, send_content_length = FALSE, close = FALSE; + bool is_head = http_request_method_is(&req->req, "HEAD"); + int ret; + + i_assert(!conn->output_locked); + + /* Determine response payload to send */ + if (resp->payload_input != NULL) { + i_assert(resp->tunnel_callback == NULL && + resp->status / 100 != 1 && + resp->status != 204 && resp->status != 304); + if (resp->payload_chunked) { + if (http_server_request_version_equals(req, 1, 0)) { + /* Connection close marks end of payload + */ + close = TRUE; + } else { + /* Input stream with unknown size */ + chunked = TRUE; + } + } else { + /* Send Content-Length if we have specified a payload, + even if it's 0 bytes. */ + content_length = resp->payload_size; + send_content_length = TRUE; + } + } else if (resp->payload_stream != NULL) { + /* HTTP payload output stream */ + if (!http_server_ostream_get_size(resp->payload_stream, + &content_length)) { + /* size not known at this point */ + chunked = TRUE; + } else { + /* output stream already finished, so data is + pre-buffered */ + send_content_length = TRUE; + } + } else if (resp->tunnel_callback == NULL && resp->status / 100 != 1 && + resp->status != 204 && resp->status != 304 && !is_head) { + /* RFC 7230, Section 3.3: Message Body + + Responses to the HEAD request method (Section 4.3.2 of + [RFC7231]) never include a message body because the + associated response header fields (e.g., Transfer-Encoding, + Content-Length, etc.), if present, indicate only what their + values would have been if the request method had been GET + (Section 4.3.1 of [RFC7231]). 2xx (Successful) responses to a + CONNECT request method (Section 4.3.6 of [RFC7231]) switch to + tunnel mode instead of having a message body. All 1xx + (Informational), 204 (No Content), and 304 (Not Modified) + responses do not include a message body. All other responses + do include a message body, although the body might be of zero + length. + + RFC 7230, Section 3.3.2: Content-Length + + A server MUST NOT send a Content-Length header field in any + 2xx (Successful) response to a CONNECT request (Section 4.3.6 + of [RFC7231]). + + -> Create empty body if it is missing. + */ + send_content_length = TRUE; + } + + /* Initialize output payload stream if needed */ + if (is_head) { + e_debug(resp->event, "A HEAD response has no payload"); + } else if (chunked) { + i_assert(resp->payload_input != NULL || + resp->payload_stream != NULL); + + e_debug(resp->event, "Will send payload in chunks"); + + resp->payload_output = + http_transfer_chunked_ostream_create(conn->conn.output); + } else if (send_content_length) { + i_assert(resp->payload_input != NULL || content_length == 0 || + resp->payload_stream != NULL); + + e_debug(resp->event, + "Will send payload with explicit size %"PRIuUOFF_T, + content_length); + + if (content_length > 0) { + resp->payload_output = conn->conn.output; + o_stream_ref(conn->conn.output); + } + } else if (close) { + i_assert(resp->payload_input != NULL); + + e_debug(resp->event, + "Will close connection after sending payload " + "(HTTP/1.0)"); + + resp->payload_output = conn->conn.output; + o_stream_ref(conn->conn.output); + } else { + e_debug(resp->event, "Response has no payload"); + } + + /* Create status line */ + str_append(rtext, "HTTP/1.1 "); + str_printfa(rtext, "%u", resp->status); + str_append(rtext, " "); + str_append(rtext, resp->reason); + + /* Create special headers implicitly if not set explicitly using + http_server_response_add_header() */ + if (!resp->have_hdr_date) { + str_append(rtext, "\r\nDate: "); + str_append(rtext, http_date_create(resp->date)); + str_append(rtext, "\r\n"); + } + if (array_is_created(&resp->auth_challenges)) { + str_append(rtext, "WWW-Authenticate: "); + http_auth_create_challenges(rtext, &resp->auth_challenges); + str_append(rtext, "\r\n"); + } + if (chunked) { + if (!resp->have_hdr_body_spec) + str_append(rtext, "Transfer-Encoding: chunked\r\n"); + } else if (send_content_length) { + if (!resp->have_hdr_body_spec) { + str_printfa(rtext, "Content-Length: %"PRIuUOFF_T"\r\n", + content_length); + } + } + if (!resp->have_hdr_connection) { + close = (close || req->req.connection_close || + req->connection_close || req->conn->input_broken); + if (close && resp->tunnel_callback == NULL) + str_append(rtext, "Connection: close\r\n"); + else if (http_server_request_version_equals(req, 1, 0)) + str_append(rtext, "Connection: Keep-Alive\r\n"); + } + + /* Status line + implicit headers */ + iov[0].iov_base = str_data(rtext); + iov[0].iov_len = str_len(rtext); + /* Explicit headers */ + iov[1].iov_base = str_data(resp->headers); + iov[1].iov_len = str_len(resp->headers); + /* End of header */ + iov[2].iov_base = "\r\n"; + iov[2].iov_len = 2; + + req->state = HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT; + o_stream_cork(conn->conn.output); + + if (o_stream_sendv(conn->conn.output, iov, N_ELEMENTS(iov)) < 0) { + http_server_connection_handle_output_error(conn); + return -1; + } + + e_debug(resp->event, "Sent header"); + + if (resp->payload_stream != NULL) + http_server_ostream_output_available(resp->payload_stream); + if (resp->payload_output != NULL) { + /* Non-blocking payload */ + ret = http_server_response_send_more(resp); + if (ret < 0) + return -1; + } else { + /* No payload to send */ + e_debug(resp->event, "No payload to send"); + if (resp->payload_stream != NULL) { + ret = http_server_ostream_continue(resp->payload_stream); + if (ret < 0) + return -1; + } + conn->output_locked = FALSE; + ret = http_server_response_finish_payload_out(resp); + if (ret < 0) + return -1; + } + + if (conn->conn.output != NULL && !resp->payload_corked && + o_stream_uncork_flush(conn->conn.output) < 0) { + http_server_connection_handle_output_error(conn); + return -1; + } + return ret; +} + +int http_server_response_send(struct http_server_response *resp) +{ + int ret; + + T_BEGIN { + ret = http_server_response_send_real(resp); + } T_END; + return ret; +} + +void http_server_response_get_status(struct http_server_response *resp, + int *status_r, const char **reason_r) +{ + i_assert(resp != NULL); + *status_r = resp->status; + *reason_r = resp->reason; +} + +uoff_t http_server_response_get_total_size(struct http_server_response *resp) +{ + i_assert(resp != NULL); + return resp->payload_size + str_len(resp->headers); +} + +void http_server_response_add_permanent_header(struct http_server_response *resp, + const char *key, const char *value) +{ + http_server_response_add_header(resp, key, value); + + if (!array_is_created(&resp->perm_headers)) + i_array_init(&resp->perm_headers, 4); + char *key_dup = i_strdup(key); + char *value_dup = i_strdup(value); + array_push_back(&resp->perm_headers, &key_dup); + array_push_back(&resp->perm_headers, &value_dup); +} |