diff --git a/Makefile b/Makefile index 34817430b..163164980 100644 --- a/Makefile +++ b/Makefile @@ -418,6 +418,13 @@ ifeq ($(WITH_HTTP), yes) $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ihttp -o bin/websocket_parser_test unittest/websocket_parser_test.cpp -Llib -lhv -pthread endif ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP_SERVER), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_static_range_test unittest/http_static_range_test.cpp -Llib -lhv -pthread +endif +endif +endif +ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_REDIS), yes) $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Icpputil -Iredis -o bin/redis_protocol_test unittest/redis_protocol_test.cpp redis/RedisMessage.cpp $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Iredis -Ievpp -o bin/redis_async_client_test unittest/redis_async_client_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index d93bbb15a..c582350b2 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -604,6 +604,42 @@ int HttpHandler::defaultRequestHandler() { return status_code; } +// Parses the first range of a "bytes=" Range header for a file of `total` +// bytes: "first-last", "first-" or a suffix "-length" (RFC 9110 section 14.1.2). +// Returns 0 with [from, to] set, -1 if the range cannot be satisfied, and 1 if +// the header is to be ignored (another range unit or an invalid range). +static int parse_byte_range(const char* value, long total, long& from, long& to) { + if (strncasecmp(value, "bytes=", 6) != 0) return 1; + const char* p = value + 6; + while (*p == ' ' || *p == '\t') ++p; + char* end = NULL; + if (*p == '-') { + if (!isdigit((unsigned char)p[1])) return 1; + long length = strtol(p + 1, &end, 10); + while (*end == ' ' || *end == '\t') ++end; + if (*end != '\0' && *end != ',') return 1; + if (length == 0 || total == 0) return -1; + from = length < total ? total - length : 0; + to = total - 1; + return 0; + } + if (!isdigit((unsigned char)*p)) return 1; + from = strtol(p, &end, 10); + if (*end != '-') return 1; + p = end + 1; + while (*p == ' ' || *p == '\t') ++p; + if (*p == '\0' || *p == ',') { + to = total - 1; + } else { + if (!isdigit((unsigned char)*p)) return 1; + to = strtol(p, &end, 10); + while (*end == ' ' || *end == '\t') ++end; + if ((*end != '\0' && *end != ',') || to < from) return 1; + if (to >= total) to = total - 1; + } + return from < total ? 0 : -1; +} + int HttpHandler::defaultStaticHandler() { // file service std::string path = req->Path(); @@ -629,19 +665,28 @@ int HttpHandler::defaultStaticHandler() { int status_code = HTTP_STATUS_OK; // Range: bool has_range = false; - long from, to = 0; - if (req->GetRange(from, to)) { - has_range = true; + long from = 0, to = 0; + auto range = req->headers.find("Range"); + if (range != req->headers.end()) { if (openFile(filepath.c_str()) != 0) { return HTTP_STATUS_NOT_FOUND; } long total = file->size(); - if (to == 0 || to >= total) to = total - 1; - if (from < 0 || from >= total || to < from) { + int ret = parse_byte_range(range->second.c_str(), total, from, to); + if (ret < 0) { closeFile(); resp->SetHeader("Content-Range", hv::asprintf("bytes */%ld", total)); return HTTP_STATUS_RANGE_NOT_SATISFIABLE; } + if (ret > 0) { + // Ignore the Range header and send the whole file + closeFile(); + } else { + has_range = true; + } + } + if (has_range) { + long total = file->size(); file->seek(from); status_code = HTTP_STATUS_PARTIAL_CONTENT; resp->status_code = HTTP_STATUS_PARTIAL_CONTENT; @@ -649,8 +694,9 @@ int HttpHandler::defaultStaticHandler() { resp->SetContentTypeByFilename(filepath.c_str()); resp->SetRange(from, to, total); if(resp->content_length < service->max_file_cache_size) { - // read into body directly - int nread = file->readrange(resp->body, from, to); + // read into body directly (readrange would read to the end of the file for to = 0) + resp->body.resize(resp->content_length); + int nread = file->read((void*)resp->body.data(), resp->content_length); closeFile(); if (nread != resp->content_length) { resp->content_length = 0; diff --git a/scripts/unittest.sh b/scripts/unittest.sh index ca1046e8d..c9e04c1db 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -35,6 +35,9 @@ bin/http_router_test if [ -x bin/websocket_parser_test ]; then bin/websocket_parser_test || exit $? fi +if [ -x bin/http_static_range_test ]; then + bin/http_static_range_test || exit $? +fi if [ -x bin/tlv_test ]; then bin/tlv_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 6e17408b3..85f7d55a8 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -97,6 +97,13 @@ target_link_libraries(websocket_parser_test ${HV_LIBRARIES}) set(HTTP_UNITTEST_TARGETS websocket_parser_test) endif() +if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_SERVER AND WITH_HTTP_CLIENT) +add_executable(http_static_range_test http_static_range_test.cpp) +target_include_directories(http_static_range_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) +target_link_libraries(http_static_range_test ${HV_LIBRARIES}) +set(HTTP_UNITTEST_TARGETS ${HTTP_UNITTEST_TARGETS} http_static_range_test) +endif() + if(WITH_LUA) add_executable(lua_binding_test lua_binding_test.cpp) target_include_directories(lua_binding_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua) diff --git a/unittest/http_static_range_test.cpp b/unittest/http_static_range_test.cpp new file mode 100644 index 000000000..82b82fc2e --- /dev/null +++ b/unittest/http_static_range_test.cpp @@ -0,0 +1,54 @@ +/* + * http_static_range_test: Range requests served by the default static file + * handler (RFC 9110 section 14). + */ + +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "htime.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "requests.h" + +static void check(const char* range, int status, const char* content_range, const char* body) { + auto req = std::make_shared(); + req->url = "http://127.0.0.1:18082/a.txt"; + req->headers["Range"] = range; + auto resp = requests::request(req); + assert(resp != NULL); + printf("Range: %s => %d %s\n", range, (int)resp->status_code, resp->GetHeader("Content-Range").c_str()); + assert(resp->status_code == status); + assert(resp->GetHeader("Content-Range") == content_range); + if (body) assert(resp->body == body); +} + +int main() { + hv_mkdir_p("tmp/http_static_range_test"); + HFile file; + int ret = file.open("tmp/http_static_range_test/a.txt", "wb"); + assert(ret == 0); + file.write("0123456789", 10); + file.close(); + + HttpService service; + service.document_root = "tmp/http_static_range_test"; + + hv::HttpServer server(&service); + server.setPort(18082); + server.start(); + hv_msleep(200); + + check("bytes=0-0", 206, "bytes 0-0/10", "0"); + check("bytes=5-", 206, "bytes 5-9/10", "56789"); + check("bytes=-3", 206, "bytes 7-9/10", "789"); + check("bytes=-20", 206, "bytes 0-9/10", "0123456789"); + check("bytes=10-", 416, "bytes */10", NULL); + check("items=0-1", 200, "", "0123456789"); + + server.stop(); + printf("http_static_range_test passed\n"); + return 0; +}