blob: 04a7a5726d94e637c0146353caa1c77db57d41d0 [file] [log] [blame] [raw]
/*
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