From dc05e13c976ebec216062cda25e3011c1634dcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Wed, 7 Oct 2009 22:49:40 +0200 Subject: [PATCH] Implement ranged requests for static files --- include/lighttpd/http_range_parser.h | 31 ++++++++ include/lighttpd/plugin_core.h | 1 + src/CMakeLists.txt | 2 + src/main/Makefile.am | 3 +- src/main/http_range_parser.rl | 101 +++++++++++++++++++++++++++ src/main/plugin_core.c | 87 +++++++++++++++++++++-- src/main/wscript | 1 + 7 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 include/lighttpd/http_range_parser.h create mode 100644 src/main/http_range_parser.rl diff --git a/include/lighttpd/http_range_parser.h b/include/lighttpd/http_range_parser.h new file mode 100644 index 0000000..3702c5c --- /dev/null +++ b/include/lighttpd/http_range_parser.h @@ -0,0 +1,31 @@ +#ifndef _LIGHTTPD_HTTP_RANGE_PARSER_H_ +#define _LIGHTTPD_HTTP_RANGE_PARSER_H_ + +#include + +typedef struct { + /* public */ + gboolean last_range; + goffset range_start, range_length, range_end; /* length = end - start + 1; */ + + /* private */ + GString *data; + goffset limit; /* "file size" */ + gboolean found_valid_range; + + int cs; + gchar *data_pos; +} liParseHttpRangeState; + +typedef enum { + LI_PARSE_HTTP_RANGE_OK, + LI_PARSE_HTTP_RANGE_DONE, + LI_PARSE_HTTP_RANGE_INVALID, + LI_PARSE_HTTP_RANGE_NOT_SATISFIABLE +} liParseHttpRangeResult; + +LI_API void li_parse_http_range_init(liParseHttpRangeState* s, GString *range_str, goffset limit); +LI_API liParseHttpRangeResult li_parse_http_range_next(liParseHttpRangeState* s); +LI_API void li_parse_http_range_clear(liParseHttpRangeState* s); + +#endif diff --git a/include/lighttpd/plugin_core.h b/include/lighttpd/plugin_core.h index f3d801e..84fa0a4 100644 --- a/include/lighttpd/plugin_core.h +++ b/include/lighttpd/plugin_core.h @@ -12,6 +12,7 @@ enum liCoreOptions { LI_CORE_OPTION_LOG, LI_CORE_OPTION_STATIC_FILE_EXCLUDE, + LI_CORE_OPTION_STATIC_RANGE_REQUESTS, LI_CORE_OPTION_SERVER_NAME, LI_CORE_OPTION_SERVER_TAG, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ea085f5..8af95d4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -177,6 +177,7 @@ SET(LIGHTTPD_SHARED_SRC etag.c filter_chunked.c http_headers.c + http_range_parser.c http_request_parser.c http_response_parser.c lighttpd-glue.c @@ -235,6 +236,7 @@ ADD_PREFIX(ANGEL_SHARED_SRC angel/) ## Build parsers by using ragel... RAGEL_PARSER(main/config_parser.rl -T0) +RAGEL_PARSER(main/http_range_parser.rl) RAGEL_PARSER(main/http_request_parser.rl) RAGEL_PARSER(main/http_response_parser.rl) RAGEL_PARSER(common/ip_parsers.rl) diff --git a/src/main/Makefile.am b/src/main/Makefile.am index 2dab2f4..ee4c76c 100644 --- a/src/main/Makefile.am +++ b/src/main/Makefile.am @@ -16,6 +16,7 @@ lighttpd_shared_src= \ etag.c \ filter_chunked.c \ http_headers.c \ + http_range_parser.c \ http_request_parser.c \ http_response_parser.c \ lighttpd-glue.c \ @@ -61,7 +62,7 @@ lighttpd_shared_src+=$(lua_src) endif EXTRA_lighttpd_SOURCES=$(lua_src) -BUILT_SOURCES=config_parser.c http_request_parser.c http_response_parser.c url_parser.c +BUILT_SOURCES=config_parser.c http_range_parser.rl http_request_parser.c http_response_parser.c url_parser.c config_parser.c: config_parser.rl ragel -C -T0 -o $@ $< diff --git a/src/main/http_range_parser.rl b/src/main/http_range_parser.rl new file mode 100644 index 0000000..ffdc7da --- /dev/null +++ b/src/main/http_range_parser.rl @@ -0,0 +1,101 @@ + +#include +#include + +#include + +%%{ + machine http_range_parser; + variable cs s->cs; + variable p s->data_pos; + + SP = ' '; + HT = '\t'; + + ws = SP | HT; + + action int_start { + tmp = 0; + } + action int_step { + int d = fc - '0'; + if (tmp > (G_MAXOFFSET-10)/10) { + s->cs = http_range_parser_error; + return LI_PARSE_HTTP_RANGE_INVALID; + } + tmp = 10*tmp + d; + } + + int = (digit digit**) >int_start $int_step ; + + action first_byte { + s->range_start = tmp; + } + action last_byte { + s->range_end = tmp; + } + action last_byte_empty { + s->range_end = s->limit; + } + action suffix_range { + s->range_end = s->limit - 1; + s->range_start = s->limit - tmp; + } + + range = (int %first_byte "-" (int %last_byte | "" %last_byte_empty) | "-" int %suffix_range); + + main := ws* "bytes" "=" range ("," >{fbreak;} range)**; + + write data; +}%% + +liParseHttpRangeResult li_parse_http_range_next(liParseHttpRangeState* s) { + const char *pe, *eof; + goffset tmp = 0; + + eof = pe = s->data->str + s->data->len; + + for ( ;; ) { + if (s->cs >= http_range_parser_first_final) { + return s->found_valid_range ? LI_PARSE_HTTP_RANGE_DONE : LI_PARSE_HTTP_RANGE_NOT_SATISFIABLE; + } + + %% write exec; + + if (s->cs >= http_range_parser_first_final) { + s->last_range = TRUE; + } else if (s->cs == http_range_parser_error) { + return LI_PARSE_HTTP_RANGE_INVALID; + } + + if (s->range_end >= s->limit) { + s->range_end = s->limit - 1; + } + if (s->range_start < 0) { + s->range_start = 0; + } + + if (s->range_start <= s->range_end) { + s->found_valid_range = TRUE; + s->range_length = s->range_end - s->range_start + 1; + return LI_PARSE_HTTP_RANGE_OK; + } + } +} + +void li_parse_http_range_init(liParseHttpRangeState* s, GString *range_str, goffset limit) { + s->data = g_string_new_len(GSTR_LEN(range_str)); + s->data_pos = s->data->str; + s->limit = limit; + s->last_range = FALSE; + s->found_valid_range = FALSE; + + %% write init; +} + +void li_parse_http_range_clear(liParseHttpRangeState* s) { + if (s->data) { + g_string_free(s->data, TRUE); + s->data = NULL; + } +} diff --git a/src/main/plugin_core.c b/src/main/plugin_core.c index e2c8ab1..cad4622 100644 --- a/src/main/plugin_core.c +++ b/src/main/plugin_core.c @@ -5,6 +5,8 @@ #include +#include + #include #include @@ -312,6 +314,7 @@ static liHandlerResult core_handle_static(liVRequest *vr, gpointer param, gpoint struct stat st; int err; liHandlerResult res; + static const gchar boundary[] = "fkj49sn38dcn3"; UNUSED(param); UNUSED(context); @@ -375,8 +378,13 @@ static liHandlerResult core_handle_static(liVRequest *vr, gpointer param, gpoint } vr->response.http_status = 403; } else { - GString *mime_str; + const GString *mime_str; gboolean cachable; + gboolean ranged_response = FALSE; + liHttpHeader *hh_range; + liChunkFile *cf; + static const GString default_mime_str = { CONST_STR_LEN("application/octet-stream"), 0 }; + #ifdef FD_CLOEXEC fcntl(fd, F_SETFD, FD_CLOEXEC); #endif @@ -393,13 +401,79 @@ static liHandlerResult core_handle_static(liVRequest *vr, gpointer param, gpoint return LI_HANDLER_GO_ON; } + cf = li_chunkfile_new(NULL, fd, FALSE); + mime_str = li_mimetype_get(vr, vr->physical.path); - vr->response.http_status = 200; - if (mime_str) + if (!mime_str) mime_str = &default_mime_str; + + if (CORE_OPTION(LI_CORE_OPTION_STATIC_RANGE_REQUESTS).boolean) { + li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Accept-Ranges"), CONST_STR_LEN("bytes")); + + hh_range = li_http_header_lookup(vr->request.headers, CONST_STR_LEN("range")); + if (hh_range) { + /* TODO: Check If-Range: header */ + GString range_str = { HEADER_VALUE_LEN(hh_range), 0 }; + liParseHttpRangeState rs; + gboolean is_multipart = FALSE, done = FALSE; + + li_parse_http_range_init(&rs, &range_str, st.st_size); + do { + switch (li_parse_http_range_next(&rs)) { + case LI_PARSE_HTTP_RANGE_OK: + if (!is_multipart && !rs.last_range) { + is_multipart = TRUE; + } + g_string_printf(vr->wrk->tmp_str, "bytes %"G_GOFFSET_FORMAT"-%"G_GOFFSET_FORMAT"/%"G_GOFFSET_FORMAT, rs.range_start, rs.range_end, (goffset) st.st_size); + if (is_multipart) { + GString *subheader = g_string_sized_new(1023); + g_string_append_printf(subheader, "\r\n--%s\r\nContent-Type: %s\r\nContent-Range: %s\r\n\r\n", boundary, mime_str->str, vr->wrk->tmp_str->str); + li_chunkqueue_append_string(vr->out, subheader); + li_chunkqueue_append_chunkfile(vr->out, cf, rs.range_start, rs.range_length); + } else { + li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Range"), GSTR_LEN(vr->wrk->tmp_str)); + li_chunkqueue_append_chunkfile(vr->out, cf, rs.range_start, rs.range_length); + } + break; + case LI_PARSE_HTTP_RANGE_DONE: + ranged_response = TRUE; + done = TRUE; + vr->response.http_status = 206; + if (is_multipart) { + GString *subheader = g_string_sized_new(1023); + g_string_append_printf(subheader, "\r\n--%s--\r\n", boundary); + li_chunkqueue_append_string(vr->out, subheader); + + g_string_printf(vr->wrk->tmp_str, "multipart/byteranges; boundary=%s", boundary); + li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), GSTR_LEN(vr->wrk->tmp_str)); + } else { + li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), GSTR_LEN(mime_str)); + } + break; + case LI_PARSE_HTTP_RANGE_INVALID: + done = TRUE; + li_chunkqueue_reset(vr->out); + break; + case LI_PARSE_HTTP_RANGE_NOT_SATISFIABLE: + ranged_response = TRUE; + done = TRUE; + li_chunkqueue_reset(vr->out); vr->out->is_closed = TRUE; + g_string_printf(vr->wrk->tmp_str, "bytes */%"G_GOFFSET_FORMAT, (goffset) st.st_size); + li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Range"), GSTR_LEN(vr->wrk->tmp_str)); + vr->response.http_status = 416; + break; + } + } while (!done); + li_parse_http_range_clear(&rs); + } + } + + if (!ranged_response) { + vr->response.http_status = 200; li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), GSTR_LEN(mime_str)); - else - li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("application/octet-stream")); - li_chunkqueue_append_file_fd(vr->out, NULL, 0, st.st_size, fd); + li_chunkqueue_append_chunkfile(vr->out, cf, st.st_size, fd); + } + + li_chunkfile_release(cf); } return LI_HANDLER_GO_ON; @@ -1195,6 +1269,7 @@ static const liPluginOption options[] = { { "log", LI_VALUE_HASH, NULL, core_option_log_parse, core_option_log_free }, { "static.exclude", LI_VALUE_LIST, NULL, NULL, NULL }, /* TODO: not used right now */ + { "static.range-requests", LI_VALUE_BOOLEAN, GINT_TO_POINTER(TRUE), NULL, NULL }, { "server.name", LI_VALUE_STRING, NULL, NULL, NULL }, { "server.tag", LI_VALUE_STRING, PACKAGE_DESC, NULL, NULL }, diff --git a/src/main/wscript b/src/main/wscript index af0881f..b3b4c89 100644 --- a/src/main/wscript +++ b/src/main/wscript @@ -31,6 +31,7 @@ def build(bld): etag.c filter_chunked.c http_headers.c + http_range_parser.rl http_request_parser.rl http_response_parser.rl lighttpd-glue.c