| /* |
| Copyright: Boaz segev, 2016-2017 |
| License: MIT |
| |
| Feel free to copy, use and enjoy according to the license provided. |
| */ |
| #ifndef HTTP1_RESPONSE_FORMATTER |
| #define HTTP1_RESPONSE_FORMATTER |
| |
| // clang-format off |
| #include "libsock.h" |
| #include "http_response.h" |
| // clang-format on |
| |
| #ifndef __unused |
| #define __unused __attribute__((unused)) |
| #endif |
| |
| /* ***************************************************************************** |
| Helpers |
| ***************************************************************************** */ |
| |
| /** |
| The padding for the status line (62 + 2 for the extra \r\n after the headers). |
| */ |
| #define H1P_HEADER_START 80 |
| #define H1P_OVERFLOW_PADDING 512 |
| |
| /** checks for overflow */ |
| #define overflowing(response) \ |
| (((response)->metadata.headers_pos - \ |
| (char *)((response)->metadata.packet->buffer)) >= \ |
| (BUFFER_PACKET_SIZE - H1P_OVERFLOW_PADDING)) |
| |
| #define HEADERS_FINISHED(response) \ |
| ((response)->metadata.packet == NULL || (response)->metadata.headers_sent) |
| |
| #define invalid_cookie_char(c) \ |
| ((c) < '!' || (c) > '~' || (c) == '=' || (c) == ' ' || (c) == ',' || \ |
| (c) == ';') |
| |
| #define h1p_validate_hpos(response) \ |
| { \ |
| if ((response)->metadata.headers_pos == 0) { \ |
| (response)->metadata.headers_pos = \ |
| (response)->metadata.packet->buffer + H1P_HEADER_START; \ |
| } \ |
| } |
| static inline int h1p_protected_copy(http_response_s *response, |
| const char *data, size_t data_len) { |
| if (data_len > 0 && data_len < H1P_OVERFLOW_PADDING) { |
| memcpy(response->metadata.headers_pos, data, data_len); |
| response->metadata.headers_pos += data_len; |
| } else { |
| while (*data) { |
| if (overflowing(response)) |
| return -1; |
| *(response->metadata.headers_pos++) = *(data++); |
| } |
| } |
| return overflowing(response); |
| } |
| |
| /* this function assume the padding in `h1p_protected_copy` saved enough room |
| * for the data to be safely written.*/ |
| __unused static inline sock_packet_s * |
| h1p_finalize_headers(http_response_s *response) { |
| if (HEADERS_FINISHED(response)) |
| return NULL; |
| h1p_validate_hpos(response); |
| |
| sock_packet_s *headers = response->metadata.packet; |
| response->metadata.packet = NULL; |
| const char *status = http_response_status_str(response->status); |
| if (!status) { |
| response->status = 500; |
| status = http_response_status_str(response->status); |
| } |
| |
| /* write the content length header, unless forced not to (<0) */ |
| if (response->metadata.content_length_written == 0 && |
| !(response->content_length < 0) && response->status >= 200 && |
| response->status != 204 && response->status != 304) { |
| h1p_protected_copy(response, "Content-Length:", 15); |
| response->metadata.headers_pos += |
| http_ul2a(response->metadata.headers_pos, response->content_length); |
| /* write the header seperator (`\r\n`) */ |
| *(response->metadata.headers_pos++) = '\r'; |
| *(response->metadata.headers_pos++) = '\n'; |
| } |
| /* write the date, if missing */ |
| if (!response->metadata.date_written) { |
| if (response->date < response->last_modified) |
| response->date = response->last_modified; |
| struct tm t; |
| /* date header */ |
| http_gmtime(&response->date, &t); |
| h1p_protected_copy(response, "Date:", 5); |
| response->metadata.headers_pos += |
| http_date2str(response->metadata.headers_pos, &t); |
| *(response->metadata.headers_pos++) = '\r'; |
| *(response->metadata.headers_pos++) = '\n'; |
| /* last-modified header */ |
| http_gmtime(&response->last_modified, &t); |
| h1p_protected_copy(response, "Last-Modified:", 14); |
| response->metadata.headers_pos += |
| http_date2str(response->metadata.headers_pos, &t); |
| *(response->metadata.headers_pos++) = '\r'; |
| *(response->metadata.headers_pos++) = '\n'; |
| } |
| /* write the keep-alive (connection) header, if missing */ |
| if (!response->metadata.connection_written) { |
| if (response->metadata.should_close) { |
| h1p_protected_copy(response, "Connection:close\r\n", 18); |
| } else { |
| h1p_protected_copy(response, "Connection:keep-alive\r\n" |
| "Keep-Alive:timeout=2\r\n", |
| 45); |
| } |
| } |
| /* write the headers completion marker (empty line - `\r\n`) */ |
| *(response->metadata.headers_pos++) = '\r'; |
| *(response->metadata.headers_pos++) = '\n'; |
| |
| /* write the status string is "HTTP/1.1 xxx <...>\r\n" length == 15 + |
| * strlen(status) */ |
| |
| size_t tmp = strlen(status); |
| int start = H1P_HEADER_START - (15 + tmp); |
| memcpy(headers->buffer + start, "HTTP/1.1 ### ", 13); |
| memcpy(headers->buffer + start + 13, status, tmp); |
| ((char *)headers->buffer)[H1P_HEADER_START - 1] = '\n'; |
| ((char *)headers->buffer)[H1P_HEADER_START - 2] = '\r'; |
| tmp = response->status / 10; |
| *((char *)headers->buffer + start + 11) = |
| '0' + (response->status - (10 * tmp)); |
| *((char *)headers->buffer + start + 10) = '0' + (tmp - (10 * (tmp / 10))); |
| *((char *)headers->buffer + start + 9) = '0' + (response->status / 100); |
| |
| headers->buffer = (char *)headers->buffer + start; |
| headers->length = response->metadata.headers_pos - (char *)headers->buffer; |
| return headers; |
| } |
| |
| static int h1p_send_headers(http_response_s *response, sock_packet_s *packet) { |
| if (packet == NULL) |
| return -1; |
| /* mark headers as sent */ |
| response->metadata.headers_sent = 1; |
| response->metadata.packet = NULL; |
| response->metadata.headers_pos = (char *)packet->length; |
| /* write data to network */ |
| return sock_send_packet(response->metadata.fd, packet); |
| }; |
| |
| /* ***************************************************************************** |
| Implementation |
| ***************************************************************************** */ |
| |
| __unused static inline int h1p_response_write_header(http_response_s *response, |
| http_headers_s header) { |
| if (HEADERS_FINISHED(response) || header.name == NULL) |
| return -1; |
| |
| h1p_validate_hpos(response); |
| |
| if (h1p_protected_copy(response, header.name, header.name_length)) |
| return -1; |
| *(response->metadata.headers_pos++) = ':'; |
| /* *(response->metadata.headers_pos++) = ' '; -- better leave out */ |
| if (header.value != NULL && |
| h1p_protected_copy(response, header.value, header.value_length)) |
| return -1; |
| *(response->metadata.headers_pos++) = '\r'; |
| *(response->metadata.headers_pos++) = '\n'; |
| return 0; |
| } |
| |
| /** |
| Set / Delete a cookie using this helper function. |
| */ |
| __unused static int h1p_response_set_cookie(http_response_s *response, |
| http_cookie_s cookie) { |
| if (HEADERS_FINISHED(response) || cookie.name == NULL || |
| overflowing(response)) |
| return -1; /* must have a cookie name. */ |
| h1p_validate_hpos(response); |
| |
| /* write the header's name to the buffer */ |
| if (h1p_protected_copy(response, "Set-Cookie:", 11)) |
| return -1; |
| |
| /* we won't use h1p_protected_copy because we'll be testing the name and value |
| * for illegal characters. */ |
| |
| /* write the cookie name */ |
| if (cookie.name_len && cookie.name_len < H1P_OVERFLOW_PADDING) { |
| for (size_t i = 0; i < cookie.name_len; i++) { |
| if (invalid_cookie_char(*cookie.name) == 0) { |
| *(response->metadata.headers_pos++) = *(cookie.name++); |
| continue; |
| } else { |
| fprintf(stderr, "Invalid cookie name cookie name character: %c\n", |
| *cookie.name); |
| return -1; |
| } |
| } |
| } else { |
| while (*cookie.name && overflowing(response) == 0) { |
| if (invalid_cookie_char(*cookie.name) == 0) { |
| *(response->metadata.headers_pos++) = *(cookie.name++); |
| continue; |
| } else { |
| fprintf(stderr, "Invalid cookie name cookie name character: %c\n", |
| *cookie.name); |
| return -1; |
| } |
| } |
| } |
| if (overflowing(response)) |
| return -1; |
| /* seperate name from value */ |
| *(response->metadata.headers_pos++) = '='; |
| /* write the cookie value, if any */ |
| if (cookie.value) { |
| if (cookie.value_len && cookie.value_len < H1P_OVERFLOW_PADDING) { |
| for (size_t i = 0; i < cookie.value_len; i++) { |
| if (invalid_cookie_char(*cookie.value) == 0) { |
| *(response->metadata.headers_pos++) = *(cookie.value++); |
| continue; |
| } else { |
| fprintf(stderr, "Invalid cookie value cookie name character: %c\n", |
| *cookie.value); |
| return -1; |
| } |
| } |
| } else { |
| while (*cookie.value && overflowing(response) == 0) { |
| if (invalid_cookie_char(*cookie.value) == 0) { |
| *(response->metadata.headers_pos++) = *(cookie.value++); |
| continue; |
| } else { |
| fprintf(stderr, "Invalid cookie value cookie name character: %c\n", |
| *cookie.value); |
| return -1; |
| } |
| } |
| } |
| if (overflowing(response)) |
| return -1; |
| } else { |
| cookie.max_age = -1; |
| } |
| /* complete value data */ |
| *(response->metadata.headers_pos++) = ';'; |
| if (cookie.max_age) { |
| response->metadata.headers_pos += |
| sprintf(response->metadata.headers_pos, "Max-Age=%d;", cookie.max_age); |
| } |
| if (cookie.domain) { |
| memcpy(response->metadata.headers_pos, "domain=", 7); |
| response->metadata.headers_pos += 7; |
| if (h1p_protected_copy(response, cookie.domain, cookie.domain_len)) |
| return -1; |
| *(response->metadata.headers_pos++) = ';'; |
| } |
| if (cookie.path) { |
| memcpy(response->metadata.headers_pos, "path=", 5); |
| response->metadata.headers_pos += 5; |
| if (h1p_protected_copy(response, cookie.path, cookie.path_len)) |
| return -1; |
| *(response->metadata.headers_pos++) = ';'; |
| } |
| if (cookie.http_only) { |
| memcpy(response->metadata.headers_pos, "HttpOnly;", 9); |
| response->metadata.headers_pos += 9; |
| } |
| if (cookie.secure) { |
| memcpy(response->metadata.headers_pos, "secure;", 7); |
| response->metadata.headers_pos += 7; |
| } |
| *(response->metadata.headers_pos++) = '\r'; |
| *(response->metadata.headers_pos++) = '\n'; |
| return 0; |
| } |
| |
| /** |
| Sends the headers (if unsent) and sends the body. |
| */ |
| __unused static inline int h1p_response_write_body(http_response_s *response, |
| const char *body, |
| size_t length) { |
| if (!response->content_length) |
| response->content_length = length; |
| sock_packet_s *headers = h1p_finalize_headers(response); |
| if (headers != NULL) { /* we haven't sent the headers yet */ |
| ssize_t i_read = |
| ((BUFFER_PACKET_SIZE - H1P_HEADER_START) - headers->length); |
| if (i_read > 1024) { |
| /* we can fit at least some of the data inside the response buffer. */ |
| if (i_read > length) { |
| i_read = length; |
| /* we can fit the data inside the response buffer. */ |
| memcpy(response->metadata.headers_pos, body, i_read); |
| response->metadata.headers_pos += i_read; |
| headers->length += i_read; |
| return h1p_send_headers(response, headers); |
| } |
| memcpy(response->metadata.headers_pos, body, i_read); |
| response->metadata.headers_pos += i_read; |
| headers->length += i_read; |
| length -= i_read; |
| body += i_read; |
| } |
| /* we need to send the (rest of the) body seperately. */ |
| if (h1p_send_headers(response, headers)) |
| return -1; |
| } |
| response->metadata.headers_pos += length; |
| return sock_write(response->metadata.fd, (void *)body, length); |
| } |
| /** |
| Sends the headers (if unsent) and schedules the file to be sent. |
| */ |
| __unused static inline int h1p_response_sendfile(http_response_s *response, |
| int source_fd, off_t offset, |
| size_t length) { |
| if (!response->content_length) |
| response->content_length = length; |
| |
| sock_packet_s *headers = h1p_finalize_headers(response); |
| |
| if (headers != NULL) { /* we haven't sent the headers yet */ |
| if (headers->length < (BUFFER_PACKET_SIZE - H1P_HEADER_START)) { |
| /* we can fit at least some of the data inside the response buffer. */ |
| ssize_t i_read = pread( |
| source_fd, response->metadata.headers_pos, |
| ((BUFFER_PACKET_SIZE - H1P_HEADER_START) - headers->length), offset); |
| if (i_read > 0) { |
| if (i_read >= length) { |
| headers->length += length; |
| close(source_fd); |
| return h1p_send_headers(response, headers); |
| } else { |
| headers->length += i_read; |
| length -= i_read; |
| offset += i_read; |
| } |
| } |
| } |
| /* we need to send the rest seperately. */ |
| if (h1p_send_headers(response, headers)) { |
| close(source_fd); |
| return -1; |
| } |
| } |
| response->metadata.headers_pos += length; |
| return sock_sendfile(response->metadata.fd, source_fd, offset, length); |
| } |
| |
| __unused static inline int h1p_response_finish(http_response_s *response) { |
| sock_packet_s *headers = h1p_finalize_headers(response); |
| if (headers) { |
| return h1p_send_headers(response, headers); |
| } |
| if (response->metadata.should_close) { |
| sock_close(response->metadata.fd); |
| } |
| |
| return 0; |
| } |
| |
| /* ***************************************************************************** |
| Cleanup |
| ***************************************************************************** */ |
| |
| /* clear any used definitions */ |
| #undef overflowing |
| #undef HEADERS_FINISHED |
| #undef invalid_cookie_char |
| #undef h1p_validate_hpos |
| #endif |