Improve HTTP responses

In 'struct http_response', rename the 'result_code' field to
'status_code' for consistency with the terminology used in the HTTP
specification, and add a 'reason' field, so that the phrase that
appears in the first response line can differ from the standard
"canned" phrases.
This commit is contained in:
Andrew Bettison 2015-12-01 00:22:45 +10:30
parent 17b0644277
commit 078bf5eb6b
2 changed files with 50 additions and 41 deletions

View File

@ -237,14 +237,14 @@ static int _reserve(struct http_request *r, const char **resp, const char *src,
assert(r->reserved <= reslim); assert(r->reserved <= reslim);
size_t siz = sizeof(char**) + len + 1; size_t siz = sizeof(char**) + len + 1;
if (r->reserved + siz > reslim) { if (r->reserved + siz > reslim) {
r->response.result_code = 414; // Request-URI Too Long r->response.status_code = 414; // Request-URI Too Long
return 0; return 0;
} }
if (r->reserved + siz > r->parsed) { if (r->reserved + siz > r->parsed) {
WHYF("Error during HTTP parsing, unparsed content %s would be overwritten by reserving %zu bytes", WHYF("Error during HTTP parsing, unparsed content %s would be overwritten by reserving %zu bytes",
alloca_toprint(30, r->parsed, r->end - r->parsed), len + 1 alloca_toprint(30, r->parsed, r->end - r->parsed), len + 1
); );
r->response.result_code = 500; r->response.status_code = 500;
return 0; return 0;
} }
const char ***respp = (const char ***) r->reserved; const char ***respp = (const char ***) r->reserved;
@ -1103,8 +1103,8 @@ static int http_request_parse_header(struct http_request *r)
_commit(r); _commit(r);
return 0; return 0;
} }
if (r->response.result_code) if (r->response.status_code)
return r->response.result_code; return r->response.status_code;
goto malformed; goto malformed;
} }
_rewind(r); _rewind(r);
@ -1124,8 +1124,8 @@ static int http_request_parse_header(struct http_request *r)
_commit(r); _commit(r);
return 0; return 0;
} }
if (r->response.result_code) if (r->response.status_code)
return r->response.result_code; return r->response.status_code;
goto malformed; goto malformed;
} }
_rewind(r); _rewind(r);
@ -1666,14 +1666,14 @@ static void http_request_receive(struct http_request *r)
} }
} }
if (result >= 200 && result < 600) { if (result >= 200 && result < 600) {
assert(r->response.result_code == 0 || r->response.result_code == result); assert(r->response.status_code == 0 || r->response.status_code == result);
r->response.result_code = result; r->response.status_code = result;
} else if (result) { } else if (result) {
WHYF("Internal failure parsing HTTP request: invalid result code %d", result); WHYF("Internal failure parsing HTTP request: invalid result code %d", result);
DEBUG_DUMP_PARSER(r); DEBUG_DUMP_PARSER(r);
r->response.result_code = 500; r->response.status_code = 500;
} }
if (r->response.result_code) if (r->response.status_code)
break; break;
if (result == -1) { if (result == -1) {
WHY("Unrecoverable error parsing HTTP request, closing connection"); WHY("Unrecoverable error parsing HTTP request, closing connection");
@ -1683,13 +1683,13 @@ static void http_request_receive(struct http_request *r)
} }
} }
if (r->phase != RECEIVE) { if (r->phase != RECEIVE) {
assert(r->response.result_code != 0); assert(r->response.status_code != 0);
RETURNVOID; RETURNVOID;
} }
if (r->response.result_code == 0) { if (r->response.status_code == 0) {
WHY("No HTTP response set, using 500 Server Error"); WHY("No HTTP response set, using 500 Server Error");
DEBUG_DUMP_PARSER(r); DEBUG_DUMP_PARSER(r);
r->response.result_code = 500; r->response.status_code = 500;
} }
http_request_start_response(r); http_request_start_response(r);
OUT(); OUT();
@ -1985,13 +1985,13 @@ static const char *http_reason_phrase(int response_code)
} }
} }
static strbuf strbuf_status_body(strbuf sb, struct http_response *hr, const char *reason_phrase) static strbuf strbuf_status_body(strbuf sb, struct http_response *hr)
{ {
if ( hr->header.content_type == CONTENT_TYPE_TEXT if ( hr->header.content_type == CONTENT_TYPE_TEXT
|| (hr->header.content_type && strcmp(hr->header.content_type, CONTENT_TYPE_TEXT) == 0) || (hr->header.content_type && strcmp(hr->header.content_type, CONTENT_TYPE_TEXT) == 0)
) { ) {
hr->header.content_type = CONTENT_TYPE_TEXT; hr->header.content_type = CONTENT_TYPE_TEXT;
strbuf_sprintf(sb, "%03u %s", hr->result_code, reason_phrase); strbuf_sprintf(sb, "%03u %s", hr->status_code, hr->reason);
unsigned i; unsigned i;
for (i = 0; i < NELS(hr->result_extra); ++i) for (i = 0; i < NELS(hr->result_extra); ++i)
if (hr->result_extra[i].label) { if (hr->result_extra[i].label) {
@ -2006,8 +2006,8 @@ static strbuf strbuf_status_body(strbuf sb, struct http_response *hr, const char
|| (hr->header.content_type && strcmp(hr->header.content_type, CONTENT_TYPE_JSON) == 0) || (hr->header.content_type && strcmp(hr->header.content_type, CONTENT_TYPE_JSON) == 0)
) { ) {
hr->header.content_type = CONTENT_TYPE_JSON; hr->header.content_type = CONTENT_TYPE_JSON;
strbuf_sprintf(sb, "{\n \"http_status_code\": %u,\n \"http_status_message\": ", hr->result_code); strbuf_sprintf(sb, "{\n \"http_status_code\": %u,\n \"http_status_message\": ", hr->status_code);
strbuf_json_string(sb, reason_phrase); strbuf_json_string(sb, hr->reason);
unsigned i; unsigned i;
for (i = 0; i < NELS(hr->result_extra); ++i) for (i = 0; i < NELS(hr->result_extra); ++i)
if (hr->result_extra[i].label) { if (hr->result_extra[i].label) {
@ -2020,7 +2020,7 @@ static strbuf strbuf_status_body(strbuf sb, struct http_response *hr, const char
} }
else { else {
hr->header.content_type = CONTENT_TYPE_HTML; hr->header.content_type = CONTENT_TYPE_HTML;
strbuf_sprintf(sb, "<html>\n<h1>%03u %s</h1>", hr->result_code, reason_phrase); strbuf_sprintf(sb, "<html>\n<h1>%03u %s</h1>", hr->status_code, hr->reason);
strbuf_puts(sb, "\n<dl>"); strbuf_puts(sb, "\n<dl>");
unsigned i; unsigned i;
for (i = 0; i < NELS(hr->result_extra); ++i) for (i = 0; i < NELS(hr->result_extra); ++i)
@ -2045,12 +2045,13 @@ static strbuf strbuf_status_body(strbuf sb, struct http_response *hr, const char
static int _render_response(struct http_request *r) static int _render_response(struct http_request *r)
{ {
struct http_response hr = r->response; struct http_response hr = r->response;
assert(hr.result_code >= 100); assert(hr.status_code >= 100);
assert(hr.result_code < 600); assert(hr.status_code < 600);
// Status code 401 must be accompanied by a WWW-Authenticate header. // Status code 401 must be accompanied by a WWW-Authenticate header.
if (hr.result_code == 401) if (hr.status_code == 401)
assert(hr.header.www_authenticate.scheme != NOAUTH); assert(hr.header.www_authenticate.scheme != NOAUTH);
const char *reason_phrase = http_reason_phrase(hr.result_code); if (!hr.reason)
hr.reason = http_reason_phrase(hr.status_code);
strbuf sb = strbuf_local(r->response_buffer, r->response_buffer_size); strbuf sb = strbuf_local(r->response_buffer, r->response_buffer_size);
// Cannot specify both static (pre-rendered) content AND generated content. // Cannot specify both static (pre-rendered) content AND generated content.
assert(!(hr.content && hr.content_generator)); assert(!(hr.content && hr.content_generator));
@ -2079,8 +2080,8 @@ static int _render_response(struct http_request *r)
&& hr.header.content_length > 0 && hr.header.content_length > 0
&& hr.header.content_length < hr.header.resource_length && hr.header.content_length < hr.header.resource_length
) { ) {
if (hr.result_code == 200) if (hr.status_code == 200)
hr.result_code = 206; // Partial Content hr.status_code = 206; // Partial Content
} }
} else { } else {
// If no content has been supplied at all, then render a standard, short body based solely on // If no content has been supplied at all, then render a standard, short body based solely on
@ -2088,9 +2089,9 @@ static int _render_response(struct http_request *r)
assert(hr.header.content_length == CONTENT_LENGTH_UNKNOWN); assert(hr.header.content_length == CONTENT_LENGTH_UNKNOWN);
assert(hr.header.resource_length == CONTENT_LENGTH_UNKNOWN); assert(hr.header.resource_length == CONTENT_LENGTH_UNKNOWN);
assert(hr.header.content_range_start == 0); assert(hr.header.content_range_start == 0);
assert(hr.result_code != 206); assert(hr.status_code != 206);
strbuf cb; strbuf cb;
STRBUF_ALLOCA_FIT(cb, 40 + strlen(reason_phrase), (strbuf_status_body(cb, &hr, reason_phrase))); STRBUF_ALLOCA_FIT(cb, 40 + strlen(hr.reason), (strbuf_status_body(cb, &hr)));
hr.content = strbuf_str(cb); hr.content = strbuf_str(cb);
hr.header.content_length = strbuf_len(cb); hr.header.content_length = strbuf_len(cb);
hr.header.resource_length = hr.header.content_length; hr.header.resource_length = hr.header.content_length;
@ -2098,7 +2099,7 @@ static int _render_response(struct http_request *r)
} }
assert(hr.header.content_type != NULL); assert(hr.header.content_type != NULL);
assert(hr.header.content_type[0]); assert(hr.header.content_type[0]);
strbuf_sprintf(sb, "HTTP/1.0 %03u %s\r\n", hr.result_code, reason_phrase); strbuf_sprintf(sb, "HTTP/1.0 %03u %s\r\n", hr.status_code, hr.reason);
strbuf_sprintf(sb, "Content-Type: %s", hr.header.content_type); strbuf_sprintf(sb, "Content-Type: %s", hr.header.content_type);
if (hr.header.boundary) { if (hr.header.boundary) {
strbuf_puts(sb, "; boundary="); strbuf_puts(sb, "; boundary=");
@ -2108,7 +2109,7 @@ static int _render_response(struct http_request *r)
strbuf_puts(sb, hr.header.boundary); strbuf_puts(sb, hr.header.boundary);
} }
strbuf_puts(sb, "\r\n"); strbuf_puts(sb, "\r\n");
if (hr.result_code == 206) { if (hr.status_code == 206) {
// Must only use result code 206 (Partial Content) if the content is in fact less than the whole // Must only use result code 206 (Partial Content) if the content is in fact less than the whole
// resource length. // resource length.
assert(hr.header.content_length != CONTENT_LENGTH_UNKNOWN); assert(hr.header.content_length != CONTENT_LENGTH_UNKNOWN);
@ -2148,7 +2149,7 @@ static int _render_response(struct http_request *r)
case BASIC: scheme = "Basic"; break; case BASIC: scheme = "Basic"; break;
} }
if (scheme) { if (scheme) {
assert(hr.result_code == 401); assert(hr.status_code == 401);
strbuf_sprintf(sb, "WWW-Authenticate: %s realm=", scheme); strbuf_sprintf(sb, "WWW-Authenticate: %s realm=", scheme);
strbuf_append_quoted_string(sb, hr.header.www_authenticate.realm); strbuf_append_quoted_string(sb, hr.header.www_authenticate.realm);
strbuf_puts(sb, "\r\n"); strbuf_puts(sb, "\r\n");
@ -2238,9 +2239,9 @@ static void http_request_start_response(struct http_request *r)
if (r->phase != RECEIVE) if (r->phase != RECEIVE)
RETURNVOID; RETURNVOID;
// Ensure conformance to HTTP standards. // Ensure conformance to HTTP standards.
if (r->response.result_code == 401 && r->response.header.www_authenticate.scheme == NOAUTH) { if (r->response.status_code == 401 && r->response.header.www_authenticate.scheme == NOAUTH) {
WHY("HTTP 401 response missing WWW-Authenticate header, sending 500 Server Error instead"); WHY("HTTP 401 response missing WWW-Authenticate header, sending 500 Server Error instead");
r->response.result_code = 500; r->response.status_code = 500;
r->response.content = NULL; r->response.content = NULL;
r->response.content_generator = NULL; r->response.content_generator = NULL;
} }
@ -2249,7 +2250,7 @@ static void http_request_start_response(struct http_request *r)
http_request_render_response(r); http_request_render_response(r);
if (r->response_buffer == NULL) { if (r->response_buffer == NULL) {
WHY("Cannot render HTTP response, sending 500 Server Error instead"); WHY("Cannot render HTTP response, sending 500 Server Error instead");
r->response.result_code = 500; r->response.status_code = 500;
r->response.content = NULL; r->response.content = NULL;
r->response.content_generator = NULL; r->response.content_generator = NULL;
http_request_render_response(r); http_request_render_response(r);
@ -2278,7 +2279,7 @@ void http_request_response_static(struct http_request *r, int result, const char
assert(r->phase == RECEIVE); assert(r->phase == RECEIVE);
assert(mime_type != NULL); assert(mime_type != NULL);
assert(mime_type[0]); assert(mime_type[0]);
r->response.result_code = result; r->response.status_code = result;
r->response.header.content_type = mime_type; r->response.header.content_type = mime_type;
r->response.header.content_range_start = 0; r->response.header.content_range_start = 0;
r->response.header.content_length = r->response.header.resource_length = bytes; r->response.header.content_length = r->response.header.resource_length = bytes;
@ -2292,7 +2293,7 @@ void http_request_response_generated(struct http_request *r, int result, const c
assert(r->phase == RECEIVE); assert(r->phase == RECEIVE);
assert(mime_type != NULL); assert(mime_type != NULL);
assert(mime_type[0]); assert(mime_type[0]);
r->response.result_code = result; r->response.status_code = result;
r->response.header.content_type = mime_type; r->response.header.content_type = mime_type;
r->response.content = NULL; r->response.content = NULL;
r->response.content_generator = generator; r->response.content_generator = generator;
@ -2301,20 +2302,27 @@ void http_request_response_generated(struct http_request *r, int result, const c
/* Start sending a short response back to the client. The result code must be either a success /* Start sending a short response back to the client. The result code must be either a success
* (2xx), redirection (3xx) or client error (4xx) or server error (5xx) code. The 'reason_phrase' * (2xx), redirection (3xx) or client error (4xx) or server error (5xx) code. The 'reason_phrase'
* argument can be text which will be enclosed in either a JSON, HTML or plain text envelope to form * argument is an optional, nul-terminated string which will be placed in the first line of the
* the response content, so it should contain any special markup (eg, HTML). If the 'reason_phrase' * response, after the status code, and also enclosed in either a JSON, HTML or plain text envelope
* argument is NULL, then the reason phrase will be generated automatically from the result code. * to form the response content, so it should contain any special markup (eg, HTML). If the
* 'reason_phrase' argument is NULL, then the reason phrase will be generated automatically from the
* result code.
*
* This function copies the 'reason_phrase' string into the outgoing response buffer before it
* returns, and makes no further use of the string pointer, so the caller can construct the string
* in any way, including as an auto array of char, or allocated on the stack using alloca(3).
* *
* @author Andrew Bettison <andrew@servalproject.com> * @author Andrew Bettison <andrew@servalproject.com>
*/ */
void http_request_simple_response(struct http_request *r, uint16_t result, const char *reason_phrase) void http_request_simple_response(struct http_request *r, uint16_t result, const char *reason_phrase)
{ {
assert(r->phase == RECEIVE); assert(r->phase == RECEIVE);
r->response.result_code = result; r->response.status_code = result;
r->response.reason = reason_phrase;
r->response.header.content_range_start = 0; r->response.header.content_range_start = 0;
strbuf h = NULL; strbuf h = NULL;
if (reason_phrase) if (r->response.reason)
STRBUF_ALLOCA_FIT(h, 40 + strlen(reason_phrase), (strbuf_status_body(h, &r->response, reason_phrase))); STRBUF_ALLOCA_FIT(h, 40 + strlen(r->response.reason), (strbuf_status_body(h, &r->response)));
if (h) { if (h) {
r->response.header.resource_length = r->response.header.content_length = strbuf_len(h); r->response.header.resource_length = r->response.header.content_length = strbuf_len(h);
r->response.content = strbuf_str(h); r->response.content = strbuf_str(h);

View File

@ -123,7 +123,8 @@ struct http_content_generator_result {
typedef int (HTTP_CONTENT_GENERATOR)(struct http_request *, unsigned char *, size_t, struct http_content_generator_result *); typedef int (HTTP_CONTENT_GENERATOR)(struct http_request *, unsigned char *, size_t, struct http_content_generator_result *);
struct http_response { struct http_response {
uint16_t result_code; uint16_t status_code;
const char *reason;
struct { struct {
const char *label; const char *label;
struct json_atom value; struct json_atom value;