Implement HTTP basic authentication

Use it in /restful/rhizome/bundlelist.json -- first 'rhizomehttp' test
passes
This commit is contained in:
Andrew Bettison 2013-10-29 17:32:04 +10:30
parent b9faf54c91
commit 21fe12859f
4 changed files with 319 additions and 45 deletions

View File

@ -98,6 +98,8 @@ void http_request_init(struct http_request *r, int sockfd)
assert(sockfd != -1);
r->request_header.content_length = CONTENT_LENGTH_UNKNOWN;
r->request_content_remaining = CONTENT_LENGTH_UNKNOWN;
r->response.header.content_length = CONTENT_LENGTH_UNKNOWN;
r->response.header.resource_length = CONTENT_LENGTH_UNKNOWN;
r->alarm.stats = &http_server_stats;
r->alarm.function = http_server_poll;
if (r->idle_timeout == 0)
@ -162,31 +164,85 @@ void http_request_finalise(struct http_request *r)
r->phase = DONE;
}
#define _SEP (1 << 0)
#define _BND (1 << 1)
#define _BASE64 (1 << 6)
#define _MASK64 ((1 << 6) - 1)
#define _SEP (1 << 7)
#define _BND (1 << 8)
uint8_t http_ctype[256] = {
['0'] = _BND, ['1'] = _BND, ['2'] = _BND, ['3'] = _BND, ['4'] = _BND,
['5'] = _BND, ['6'] = _BND, ['7'] = _BND, ['8'] = _BND, ['9'] = _BND,
['A'] = _BND, ['B'] = _BND, ['C'] = _BND, ['D'] = _BND, ['E'] = _BND,
['F'] = _BND, ['G'] = _BND, ['H'] = _BND, ['I'] = _BND, ['J'] = _BND,
['K'] = _BND, ['L'] = _BND, ['M'] = _BND, ['N'] = _BND, ['O'] = _BND,
['P'] = _BND, ['Q'] = _BND, ['R'] = _BND, ['S'] = _BND, ['T'] = _BND,
['U'] = _BND, ['V'] = _BND, ['W'] = _BND, ['X'] = _BND, ['Y'] = _BND,
['Z'] = _BND,
['a'] = _BND, ['b'] = _BND, ['c'] = _BND, ['d'] = _BND, ['e'] = _BND,
['f'] = _BND, ['g'] = _BND, ['h'] = _BND, ['i'] = _BND, ['j'] = _BND,
['k'] = _BND, ['l'] = _BND, ['m'] = _BND, ['n'] = _BND, ['o'] = _BND,
['p'] = _BND, ['q'] = _BND, ['r'] = _BND, ['s'] = _BND, ['t'] = _BND,
['u'] = _BND, ['v'] = _BND, ['w'] = _BND, ['x'] = _BND, ['y'] = _BND,
['z'] = _BND,
['+'] = _BND, ['-'] = _BND, ['.'] = _BND, ['/'] = _BND, [':'] = _BND,
uint16_t http_ctype[256] = {
['A'] = _BND | _BASE64 | 0,
['B'] = _BND | _BASE64 | 1,
['C'] = _BND | _BASE64 | 2,
['D'] = _BND | _BASE64 | 3,
['E'] = _BND | _BASE64 | 4,
['F'] = _BND | _BASE64 | 5,
['G'] = _BND | _BASE64 | 6,
['H'] = _BND | _BASE64 | 7,
['I'] = _BND | _BASE64 | 8,
['J'] = _BND | _BASE64 | 9,
['K'] = _BND | _BASE64 | 10,
['L'] = _BND | _BASE64 | 11,
['M'] = _BND | _BASE64 | 12,
['N'] = _BND | _BASE64 | 13,
['O'] = _BND | _BASE64 | 14,
['P'] = _BND | _BASE64 | 15,
['Q'] = _BND | _BASE64 | 16,
['R'] = _BND | _BASE64 | 17,
['S'] = _BND | _BASE64 | 18,
['T'] = _BND | _BASE64 | 19,
['U'] = _BND | _BASE64 | 20,
['V'] = _BND | _BASE64 | 21,
['W'] = _BND | _BASE64 | 22,
['X'] = _BND | _BASE64 | 23,
['Y'] = _BND | _BASE64 | 24,
['Z'] = _BND | _BASE64 | 25,
['a'] = _BND | _BASE64 | 26,
['b'] = _BND | _BASE64 | 27,
['c'] = _BND | _BASE64 | 28,
['d'] = _BND | _BASE64 | 29,
['e'] = _BND | _BASE64 | 30,
['f'] = _BND | _BASE64 | 31,
['g'] = _BND | _BASE64 | 32,
['h'] = _BND | _BASE64 | 33,
['i'] = _BND | _BASE64 | 34,
['j'] = _BND | _BASE64 | 35,
['k'] = _BND | _BASE64 | 36,
['l'] = _BND | _BASE64 | 37,
['m'] = _BND | _BASE64 | 38,
['n'] = _BND | _BASE64 | 39,
['o'] = _BND | _BASE64 | 40,
['p'] = _BND | _BASE64 | 41,
['q'] = _BND | _BASE64 | 42,
['r'] = _BND | _BASE64 | 43,
['s'] = _BND | _BASE64 | 44,
['t'] = _BND | _BASE64 | 45,
['u'] = _BND | _BASE64 | 46,
['v'] = _BND | _BASE64 | 47,
['w'] = _BND | _BASE64 | 48,
['x'] = _BND | _BASE64 | 49,
['y'] = _BND | _BASE64 | 50,
['z'] = _BND | _BASE64 | 51,
['0'] = _BND | _BASE64 | 52,
['1'] = _BND | _BASE64 | 53,
['2'] = _BND | _BASE64 | 54,
['3'] = _BND | _BASE64 | 55,
['4'] = _BND | _BASE64 | 56,
['5'] = _BND | _BASE64 | 57,
['6'] = _BND | _BASE64 | 58,
['7'] = _BND | _BASE64 | 59,
['8'] = _BND | _BASE64 | 60,
['9'] = _BND | _BASE64 | 61,
['+'] = _BND | _BASE64 | 62,
['/'] = _BND | _BASE64 | 63,
['='] = _SEP | _BND,
['-'] = _BND,
['.'] = _BND,
[':'] = _BND,
['_'] = _BND,
['('] = _SEP | _BND,
[')'] = _SEP | _BND,
[','] = _SEP | _BND,
['?'] = _SEP | _BND,
['='] = _SEP | _BND,
[' '] = _SEP | _BND,
['\t'] = _SEP,
['<'] = _SEP,
@ -213,6 +269,21 @@ inline int is_http_ctl(char c)
return iscntrl(c);
}
inline int is_base64_digit(char c)
{
return (http_ctype[(unsigned char) c] & _BASE64) != 0;
}
inline int is_base64_pad(char c)
{
return c == '=';
}
inline uint8_t base64_digit(char c)
{
return http_ctype[(unsigned char) c] & _MASK64;
}
inline int is_http_separator(char c)
{
return (http_ctype[(unsigned char) c] & _SEP) != 0;
@ -612,6 +683,92 @@ static int _parse_content_type(struct http_request *r, struct mime_content_type
return 1;
}
static size_t _parse_base64(struct http_request *r, char *bin, size_t binsize)
{
uint8_t buf = 0;
size_t digits = 0;
size_t bytes = 0;
for (; !_run_out(r) && is_base64_digit(*r->cursor); _skip_optional_space(r), ++r->cursor) {
if (bytes < binsize) {
uint8_t d = base64_digit(*r->cursor);
switch (digits++ & 3) {
case 0:
buf = d << 2;
break;
case 1:
if (bin)
bin[bytes] = buf | (d >> 4);
++bytes;
buf = d << 4;
break;
case 2:
if (bin)
bin[bytes] = buf | (d >> 2);
++bytes;
buf = d << 6;
break;
case 3:
if (bin)
bin[bytes] = buf | d;
++bytes;
break;
}
}
}
if (digits == 0)
return 0;
if (!_run_out(r) && is_base64_pad(*r->cursor))
++r->cursor;
if (!_run_out(r) && is_base64_pad(*r->cursor))
++r->cursor;
return bytes;
}
static int _parse_authorization_credentials_basic(struct http_request *r, struct http_client_credentials_basic *cred, char *buf, size_t bufsz)
{
size_t n = _parse_base64(r, buf, bufsz - 1); // leave room for NUL terminator on password
assert(n < bufsz); // buffer must be big enough
char *pw = (char *) strnchr(buf, n, ':');
if (pw == NULL)
return 0; // malformed
cred->user = buf;
*pw++ = '\0'; // NUL terminate user
cred->password = pw;
buf[n] = '\0'; // NUL terminate password
return 1;
}
static int _parse_authorization(struct http_request *r, struct http_client_authorization *auth, size_t header_bytes)
{
const char *start = r->cursor;
if (_skip_literal(r, "Basic") && _skip_space(r)) {
size_t bufsz = 5 + header_bytes * 3 / 4; // enough for base64 decoding
char buf[bufsz];
if (_parse_authorization_credentials_basic(r, &auth->credentials.basic, buf, bufsz) == -1) {
auth->scheme = BASIC;
return 1;
}
if (r->debug_flag && *r->debug_flag)
DEBUGF("Malformed HTTP header: Authorization: %s", alloca_toprint(50, start, header_bytes));
return 0;
}
if (_skip_literal(r, "Digest") && _skip_space(r)) {
if (r->debug_flag && *r->debug_flag)
DEBUG("Ignoring unsupported HTTP Authorization scheme: Digest");
r->cursor += header_bytes;
return 1;
}
struct substring scheme;
if (_skip_token(r, &scheme) && _skip_space(r)) {
if (r->debug_flag && *r->debug_flag)
DEBUGF("Unrecognised HTTP Authorization scheme: %s", alloca_toprint(-1, scheme.start, scheme.end - scheme.start));
return 0;
}
if (r->debug_flag && *r->debug_flag)
DEBUGF("Malformed HTTP Authorization header: %s", alloca_toprint(50, r->parsed, r->end - r->parsed));
return 0;
}
static int _parse_quoted_rfc822_time(struct http_request *r, time_t *timep)
{
char datestr[40];
@ -854,7 +1011,7 @@ static int http_request_parse_header(struct http_request *r)
if ( _skip_literal(r, "bytes=")
&& (n = _parse_ranges(r, r->request_header.content_ranges, NELS(r->request_header.content_ranges)))
&& _skip_optional_space(r)
&& (r->cursor == eol)
&& r->cursor == eol
) {
r->cursor = nextline;
_commit(r);
@ -874,6 +1031,26 @@ static int http_request_parse_header(struct http_request *r)
goto malformed;
}
_rewind(r);
if (_skip_literal_nocase(r, "Authorization:")) {
if (r->request_header.authorization.scheme != NOAUTH) {
if (r->debug_flag && *r->debug_flag)
DEBUGF("Skipping duplicate HTTP header Authorization: %s", alloca_toprint(50, sol, r->end - sol));
r->cursor = nextline;
_commit(r);
return 0;
}
_skip_optional_space(r);
if ( _parse_authorization(r, &r->request_header.authorization, eol - r->cursor)
&& _skip_optional_space(r)
&& r->cursor == eol
) {
assert(r->request_header.authorization.scheme != NOAUTH);
r->cursor = nextline;
_commit(r);
}
goto malformed;
}
_rewind(r);
if (r->debug_flag && *r->debug_flag)
DEBUGF("Skipped HTTP request header: %s", alloca_toprint(-1, sol, eol - sol));
r->cursor = nextline;
@ -1520,6 +1697,9 @@ static void http_request_send_response(struct http_request *r)
r->response_buffer_sent += (size_t) written;
assert(r->response_sent <= r->response_length);
assert(r->response_buffer_sent <= r->response_buffer_length);
if (r->debug_flag && *r->debug_flag)
DEBUGF("Wrote %zu bytes to HTTP socket, total %"PRIhttp_size_t", remaining=%"PRIhttp_size_t,
(size_t) written, r->response_sent, r->response_length - r->response_sent);
// Reset inactivity timer.
r->alarm.alarm = gettime_ms() + r->idle_timeout;
r->alarm.deadline = r->alarm.alarm + r->idle_timeout;
@ -1640,24 +1820,30 @@ static const char *httpResultString(int response_code)
static int _render_response(struct http_request *r)
{
struct http_response hr = r->response;
assert(hr.result_code != 0);
assert(hr.header.content_range_start <= hr.header.resource_length);
assert(hr.header.content_length <= hr.header.resource_length);
// To save page handlers having to decide between 200 (OK) and 206 (Partial Content), they can
// just send 200 and the content range fields, and this logic will detect if it should be 206.
if (hr.header.content_length > 0 && hr.header.content_length < hr.header.resource_length && hr.result_code == 200)
hr.result_code = 206; // Partial Content
assert(hr.result_code >= 200);
assert(hr.result_code < 600);
const char *result_string = httpResultString(hr.result_code);
strbuf sb = strbuf_local(r->response_buffer, r->response_buffer_size);
if (hr.content == NULL && hr.content_generator == NULL) {
assert(hr.header.content_length == CONTENT_LENGTH_UNKNOWN);
assert(hr.header.resource_length == CONTENT_LENGTH_UNKNOWN);
assert(hr.header.content_range_start == 0);
strbuf cb = strbuf_alloca(100 + strlen(result_string));
strbuf_puts(cb, "<html><h1>");
strbuf_puts(cb, result_string);
strbuf_puts(cb, "</h1></html>\r\n");
strbuf_sprintf(cb, "<html><h1>%03u %s</h1></html>", hr.result_code, result_string);
hr.content = strbuf_str(cb);
hr.header.resource_length = hr.header.content_length = strbuf_len(cb);
hr.header.content_type = "text/html";
hr.header.content_range_start = 0;
} else {
assert(hr.header.content_length != CONTENT_LENGTH_UNKNOWN);
assert(hr.header.resource_length != CONTENT_LENGTH_UNKNOWN);
assert(hr.header.content_length <= hr.header.resource_length);
assert(hr.header.content_range_start + hr.header.content_length <= hr.header.resource_length);
// To save page handlers having to decide between 200 (OK) and 206 (Partial Content), they can
// just set the content range fields and pass 200 to http_request_response_static(), and this
// logic will change it to 206 if appropriate.
if (hr.header.content_length > 0 && hr.header.content_length < hr.header.resource_length && hr.result_code == 200)
hr.result_code = 206; // Partial Content
}
assert(hr.header.content_type != NULL);
assert(hr.header.content_type[0]);
@ -1684,6 +1870,16 @@ static int _render_response(struct http_request *r)
);
}
strbuf_sprintf(sb, "Content-Length: %"PRIhttp_size_t"\r\n", hr.header.content_length);
const char *scheme = NULL;
switch (hr.header.www_authenticate.scheme) {
case NOAUTH: break;
case BASIC: scheme = "Basic"; break;
}
if (scheme) {
strbuf_sprintf(sb, "WWW-Authenticate: %s realm=", scheme);
strbuf_append_quoted_string(sb, hr.header.www_authenticate.realm);
strbuf_puts(sb, "\r\n");
}
strbuf_puts(sb, "\r\n");
if (strbuf_overrun(sb))
return 0;
@ -1836,8 +2032,10 @@ void http_request_simple_response(struct http_request *r, uint16_t result, const
r->response.result_code = result;
r->response.header.content_type = "text/html";
r->response.header.content_range_start = 0;
r->response.header.resource_length = r->response.header.content_length = h ? strbuf_len(h) : 0;
r->response.content = h ? strbuf_str(h) : NULL;
if (h) {
r->response.header.resource_length = r->response.header.content_length = strbuf_len(h);
r->response.content = strbuf_str(h);
}
r->response.content_generator = NULL;
http_request_start_response(r);
}

View File

@ -63,11 +63,28 @@ struct mime_content_type {
char charset[31];
};
struct http_client_authorization {
enum http_authorization_scheme { NOAUTH = 0, BASIC } scheme;
union {
struct http_client_credentials_basic {
const char *user;
const char *password;
} basic;
} credentials;
};
struct http_www_authenticate {
enum http_authorization_scheme scheme;
const char *realm;
};
struct http_request_headers {
http_size_t content_length;
struct mime_content_type content_type;
unsigned short content_range_count;
struct http_range content_ranges[5];
struct http_client_authorization authorization;
};
struct http_response_headers {
@ -76,6 +93,7 @@ struct http_response_headers {
http_size_t resource_length; // size of entire resource
const char *content_type; // "type/subtype"
const char *boundary;
struct http_www_authenticate www_authenticate;
};
typedef int (*HTTP_CONTENT_GENERATOR)(struct http_request *);

View File

@ -34,24 +34,29 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#define RHIZOME_SERVER_MAX_LIVE_REQUESTS 32
typedef int HTTP_HANDLER(rhizome_http_request *r, const char *remainder);
struct http_handler{
const char *path;
int (*parser)(rhizome_http_request *r, const char *remainder);
HTTP_HANDLER *parser;
};
static int rhizome_status_page(rhizome_http_request *r, const char *remainder);
static int rhizome_file_page(rhizome_http_request *r, const char *remainder);
static int manifest_by_prefix_page(rhizome_http_request *r, const char *remainder);
static int interface_page(rhizome_http_request *r, const char *remainder);
static int neighbour_page(rhizome_http_request *r, const char *remainder);
static int fav_icon_header(rhizome_http_request *r, const char *remainder);
static int root_page(rhizome_http_request *r, const char *remainder);
static HTTP_HANDLER restful_rhizome_bundlelist_json;
extern int rhizome_direct_import(rhizome_http_request *r, const char *remainder);
extern int rhizome_direct_enquiry(rhizome_http_request *r, const char *remainder);
extern int rhizome_direct_dispatch(rhizome_http_request *r, const char *remainder);
static HTTP_HANDLER rhizome_status_page;
static HTTP_HANDLER rhizome_file_page;
static HTTP_HANDLER manifest_by_prefix_page;
static HTTP_HANDLER interface_page;
static HTTP_HANDLER neighbour_page;
static HTTP_HANDLER fav_icon_header;
static HTTP_HANDLER root_page;
extern HTTP_HANDLER rhizome_direct_import;
extern HTTP_HANDLER rhizome_direct_enquiry;
extern HTTP_HANDLER rhizome_direct_dispatch;
struct http_handler paths[]={
{"/restful/rhizome/bundlelist.json", restful_rhizome_bundlelist_json},
{"/rhizome/status", rhizome_status_page},
{"/rhizome/file/", rhizome_file_page},
{"/rhizome/import", rhizome_direct_import},
@ -312,6 +317,43 @@ int is_http_header_complete(const char *buf, size_t len, size_t read_since_last_
OUT();
}
/* Return 1 if the given authorization credentials are acceptable.
* Return 0 if not.
*/
static int is_authorized(struct http_client_authorization *auth)
{
if (auth->scheme != BASIC)
return 0;
unsigned i;
for (i = 0; i != config.rhizome.api.restful.users.ac; ++i) {
if ( strcmp(config.rhizome.api.restful.users.av[i].key, auth->credentials.basic.user) == 0
&& strcmp(config.rhizome.api.restful.users.av[i].value.password, auth->credentials.basic.password) == 0
)
return 1;
}
return 0;
}
static int restful_rhizome_bundlelist_json(rhizome_http_request *r, const char *remainder)
{
if (!is_rhizome_http_enabled())
return 1;
if (*remainder)
return 1;
if (r->http.verb != HTTP_VERB_GET) {
http_request_simple_response(&r->http, 405, NULL);
return 0;
}
if (!is_authorized(&r->http.request_header.authorization)) {
r->http.response.header.www_authenticate.scheme = BASIC;
r->http.response.header.www_authenticate.realm = "Serval Rhizome";
http_request_simple_response(&r->http, 401, NULL);
return 0;
}
http_request_simple_response(&r->http, 200, NULL);
return 0;
}
static int neighbour_page(rhizome_http_request *r, const char *remainder)
{
if (r->http.verb != HTTP_VERB_GET) {

View File

@ -41,8 +41,20 @@ setup() {
get_rhizome_server_port PORTA +A
}
finally() {
stop_all_servald_servers
}
teardown() {
kill_all_servald_processes
assert_no_servald_processes
report_all_servald_servers
}
set_rhizome_config() {
executeOk_servald config \
set debug.httpd on \
set debug.rhizome_httpd on \
set debug.rhizome on \
set debug.verbose on \
set log.console.level debug
@ -50,14 +62,18 @@ set_rhizome_config() {
doc_AuthBasicMissing="Basic Authentication credentials are required"
test_AuthBasicMissing() {
execute --exit-status=67 curl \
--silent --fail --show-error \
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output http.output \
--dump-header http.headers \
"http://$addr_localhost:$PORTA/restful/rhizome/bundlelist.json"
assertStdoutIs '401'
CR=' '
assertGrep http.headers "^WWW-Authenticate: Basic realm=\"Serval Rhizome\"$CR$"
}
teardown_AuthBasicMissing() {
tfw_cat http.headers http.output
teardown
}
doc_AuthBasicWrong="Basic Authentication credentials must be correct"