diff --git a/cmake/SelectHTTPParser.cmake b/cmake/SelectHTTPParser.cmake index aa6711d96..111170cb2 100644 --- a/cmake/SelectHTTPParser.cmake +++ b/cmake/SelectHTTPParser.cmake @@ -1,39 +1,32 @@ # Optional external dependency: http-parser -if(USE_HTTP_PARSER STREQUAL "builtin") - message(STATUS "support for bundled (legacy) http-parser explicitly requested") +if(USE_HTTP_PARSER STREQUAL "http-parser") + find_package(HTTPParser) - add_subdirectory("${PROJECT_SOURCE_DIR}/deps/http-parser" "${PROJECT_BINARY_DIR}/deps/http-parser") - list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${PROJECT_SOURCE_DIR}/deps/http-parser") - list(APPEND LIBGIT2_DEPENDENCY_OBJECTS "$") - add_feature_info(http-parser ON "http-parser support (bundled)") -else() - # By default, try to use system LLHTTP. Fall back to - # system http-parser, and even to bundled http-parser - # as a last resort. - find_package(LLHTTP) + if(HTTP_PARSER_FOUND AND HTTP_PARSER_VERSION_MAJOR EQUAL 2) + list(APPEND LIBGIT2_SYSTEM_INCLUDES ${HTTP_PARSER_INCLUDE_DIRS}) + list(APPEND LIBGIT2_SYSTEM_LIBS ${HTTP_PARSER_LIBRARIES}) + list(APPEND LIBGIT2_PC_LIBS "-lhttp_parser") + set(GIT_HTTPPARSER_HTTPPARSER 1) + add_feature_info(http-parser ON "using http-parser (system)") + else() + message(FATAL_ERROR "http-parser support was requested but not found") + endif() +elseif(USE_HTTP_PARSER STREQUAL "llhttp") + find_package(LLHTTP) if(LLHTTP_FOUND AND LLHTTP_VERSION_MAJOR EQUAL 9) - add_compile_definitions(USE_LLHTTP) list(APPEND LIBGIT2_SYSTEM_INCLUDES ${LLHTTP_INCLUDE_DIRS}) list(APPEND LIBGIT2_SYSTEM_LIBS ${LLHTTP_LIBRARIES}) list(APPEND LIBGIT2_PC_LIBS "-lllhttp") - add_feature_info(llhttp ON "llhttp support (system)") + set(GIT_HTTPPARSER_LLHTTP 1) + add_feature_info(http-parser ON "using llhttp (system)") else() - message(STATUS "llhttp support was requested but not found; checking (legacy) http-parser support") - find_package(HTTPParser) - - if(HTTP_PARSER_FOUND AND HTTP_PARSER_VERSION_MAJOR EQUAL 2) - list(APPEND LIBGIT2_SYSTEM_INCLUDES ${HTTP_PARSER_INCLUDE_DIRS}) - list(APPEND LIBGIT2_SYSTEM_LIBS ${HTTP_PARSER_LIBRARIES}) - list(APPEND LIBGIT2_PC_LIBS "-lhttp_parser") - add_feature_info(http-parser ON "http-parser support (system)") - else() - message(STATUS "neither llhttp nor http-parser support was found; proceeding with bundled (legacy) http-parser") - - add_subdirectory("${PROJECT_SOURCE_DIR}/deps/http-parser" "${PROJECT_BINARY_DIR}/deps/http-parser") - list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${PROJECT_SOURCE_DIR}/deps/http-parser") - list(APPEND LIBGIT2_DEPENDENCY_OBJECTS "$") - add_feature_info(http-parser ON "http-parser support (bundled)") - endif() - endif() + message(FATAL_ERROR "llhttp support was requested but not found") + endif() +else() + add_subdirectory("${PROJECT_SOURCE_DIR}/deps/http-parser" "${PROJECT_BINARY_DIR}/deps/http-parser") + list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${PROJECT_SOURCE_DIR}/deps/http-parser") + list(APPEND LIBGIT2_DEPENDENCY_OBJECTS "$") + set(GIT_HTTPPARSER_BUILTIN 1) + add_feature_info(http-parser ON "using bundled parser") endif() diff --git a/src/libgit2/transports/httpclient.c b/src/libgit2/transports/httpclient.c index 6e3491ed2..9937e9484 100644 --- a/src/libgit2/transports/httpclient.c +++ b/src/libgit2/transports/httpclient.c @@ -8,31 +8,6 @@ #include "common.h" #include "git2.h" -#ifdef USE_LLHTTP -#include -typedef llhttp_settings_t http_settings_t; -typedef llhttp_t http_parser_t; -GIT_INLINE(http_settings_t *) http_client_parser_settings(void); -#define git_http_parser_init(parser) llhttp_init(parser, HTTP_RESPONSE, http_client_parser_settings()) -#define git_http_parser_pause(parser) llhttp_pause(parser) -#define git_http_parser_resume(parser) llhttp_resume(parser) -#define git_http_parser_errno(parser) parser.error -#define git_http_should_keep_alive(parser) llhttp_should_keep_alive(parser) -#define git_http_errno_description(parser, errno) llhttp_get_error_reason(parser) -#else -#include -/* Legacy http-parser. */ -typedef http_parser_settings http_settings_t; -typedef struct http_parser http_parser_t; -GIT_INLINE(http_settings_t *) http_client_parser_settings(void); -#define git_http_parser_init(parser) http_parser_init(parser, HTTP_RESPONSE) -#define git_http_parser_pause(parser) http_parser_pause(parser, 1) -#define git_http_parser_resume(parser) http_parser_pause(parser, 0) -#define git_http_parser_errno(parser) parser.http_errno -#define git_http_should_keep_alive(parser) http_should_keep_alive(parser) -#define git_http_errno_description(parser, errno) http_errno_description(errno) -#endif /* USE_LLHTTP */ - #include "vector.h" #include "trace.h" #include "httpclient.h" @@ -46,6 +21,7 @@ GIT_INLINE(http_settings_t *) http_client_parser_settings(void); #include "streams/socket.h" #include "streams/tls.h" #include "auth.h" +#include "httpparser.h" static git_http_auth_scheme auth_schemes[] = { { GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDENTIAL_DEFAULT, git_http_auth_negotiate }, @@ -133,7 +109,7 @@ struct git_http_client { git_http_server_t current_server; http_client_state state; - http_parser_t parser; + git_http_parser parser; git_http_server server; git_http_server proxy; @@ -179,7 +155,7 @@ void git_http_response_dispose(git_http_response *response) memset(response, 0, sizeof(git_http_response)); } -static int on_header_complete(http_parser_t *parser) +static int on_header_complete(git_http_parser *parser) { http_parser_context *ctx = (http_parser_context *) parser->data; git_http_client *client = ctx->client; @@ -244,7 +220,7 @@ static int on_header_complete(http_parser_t *parser) return 0; } -static int on_header_field(http_parser_t *parser, const char *str, size_t len) +static int on_header_field(git_http_parser *parser, const char *str, size_t len) { http_parser_context *ctx = (http_parser_context *) parser->data; @@ -279,7 +255,7 @@ static int on_header_field(http_parser_t *parser, const char *str, size_t len) return 0; } -static int on_header_value(http_parser_t *parser, const char *str, size_t len) +static int on_header_value(git_http_parser *parser, const char *str, size_t len) { http_parser_context *ctx = (http_parser_context *) parser->data; @@ -367,7 +343,7 @@ static int resend_needed(git_http_client *client, git_http_response *response) return 0; } -static int on_headers_complete(http_parser_t *parser) +static int on_headers_complete(git_http_parser *parser) { http_parser_context *ctx = (http_parser_context *) parser->data; @@ -389,8 +365,8 @@ static int on_headers_complete(http_parser_t *parser) return ctx->parse_status = PARSE_STATUS_ERROR; } - ctx->response->status = parser->status_code; - ctx->client->keepalive = git_http_should_keep_alive(parser); + ctx->response->status = git_http_parser_status_code(parser); + ctx->client->keepalive = git_http_parser_keep_alive(parser); /* Prepare for authentication */ collect_authinfo(&ctx->response->server_auth_schemetypes, @@ -403,28 +379,15 @@ static int on_headers_complete(http_parser_t *parser) ctx->response->resend_credentials = resend_needed(ctx->client, ctx->response); -#ifndef USE_LLHTTP - /* Stop parsing. llhttp documentation says about llhttp_pause(): - * "Do not call this from user callbacks! User callbacks must - * return HPE_PAUSED if pausing is required", so that's what - * we will do, and call git_http_parser_pause() only for - * http-parser. */ - git_http_parser_pause(parser); -#endif - if (ctx->response->content_type || ctx->response->chunked) ctx->client->state = READING_BODY; else ctx->client->state = DONE; -#ifdef USE_LLHTTP - return HPE_PAUSED; -#else - return 0; -#endif + return git_http_parser_pause(parser); } -static int on_body(http_parser_t *parser, const char *buf, size_t len) +static int on_body(git_http_parser *parser, const char *buf, size_t len) { http_parser_context *ctx = (http_parser_context *) parser->data; size_t max_len; @@ -446,7 +409,7 @@ static int on_body(http_parser_t *parser, const char *buf, size_t len) return 0; } -static int on_message_complete(http_parser_t *parser) +static int on_message_complete(git_http_parser *parser) { http_parser_context *ctx = (http_parser_context *) parser->data; @@ -911,9 +874,29 @@ GIT_INLINE(int) server_setup_from_url( return 0; } +static bool parser_settings_initialized; +static git_http_parser_settings parser_settings; + +GIT_INLINE(git_http_parser_settings *) http_client_parser_settings(void) +{ + if (!parser_settings_initialized) { + parser_settings.on_header_field = on_header_field; + parser_settings.on_header_value = on_header_value; + parser_settings.on_headers_complete = on_headers_complete; + parser_settings.on_body = on_body; + parser_settings.on_message_complete = on_message_complete; + + parser_settings_initialized = true; + } + + return &parser_settings; +} + static void reset_parser(git_http_client *client) { - git_http_parser_init(&client->parser); + git_http_parser_init(&client->parser, + GIT_HTTP_PARSER_RESPONSE, + http_client_parser_settings()); } static int setup_hosts( @@ -1156,64 +1139,9 @@ GIT_INLINE(int) client_read(git_http_client *client) return (int)read_len; } -static bool parser_settings_initialized; -static http_settings_t parser_settings; - -static size_t git_http_parser_execute(http_parser_t *parser, const char* data, size_t len) -{ -#ifdef USE_LLHTTP - llhttp_errno_t error; - size_t parsed_len; - - /* - * Unlike http_parser, which returns the number of parsed - * bytes in the _execute() call, llhttp returns an error - * code. - */ - - if (data == NULL || len == 0) { - error = llhttp_finish(parser); - } else { - error = llhttp_execute(parser, data, len); - } - - parsed_len = len; - /* - * Adjust number of parsed bytes in case of error. - */ - if (error != HPE_OK) { - parsed_len = llhttp_get_error_pos(parser) - data; - - /* This isn't a real pause, just a way to stop parsing early. */ - if (error == HPE_PAUSED_UPGRADE) { - llhttp_resume_after_upgrade(parser); - } - } - - return parsed_len; -#else - return http_parser_execute(parser, http_client_parser_settings(), data, len); -#endif -} - -GIT_INLINE(http_settings_t *) http_client_parser_settings(void) -{ - if (!parser_settings_initialized) { - parser_settings.on_header_field = on_header_field; - parser_settings.on_header_value = on_header_value; - parser_settings.on_headers_complete = on_headers_complete; - parser_settings.on_body = on_body; - parser_settings.on_message_complete = on_message_complete; - - parser_settings_initialized = true; - } - - return &parser_settings; -} - GIT_INLINE(int) client_read_and_parse(git_http_client *client) { - http_parser_t *parser = &client->parser; + git_http_parser *parser = &client->parser; http_parser_context *ctx = (http_parser_context *) parser->data; unsigned char http_errno; int read_len; @@ -1230,7 +1158,7 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client) parsed_len = git_http_parser_execute(parser, client->read_buf.ptr, client->read_buf.size); - http_errno = git_http_parser_errno(client->parser); + http_errno = git_http_parser_errno(parser); if (parsed_len > INT_MAX) { git_error_set(GIT_ERROR_HTTP, "unexpectedly large parse"); @@ -1249,29 +1177,29 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client) * (This can happen in response to an expect/continue request, * where the server gives you a 100 and 200 simultaneously.) */ - if (http_errno == HPE_PAUSED) { -#ifndef USE_LLHTTP - /* - * http-parser has a "feature" where it will not deliver the - * final byte when paused in a callback. Consume that byte. - * https://github.com/nodejs/http-parser/issues/97 - */ - GIT_ASSERT(client->read_buf.size > parsed_len); + if (http_errno == GIT_HTTP_PARSER_PAUSED) { + size_t additional_size; -#endif git_http_parser_resume(parser); -#ifndef USE_LLHTTP - parsed_len += git_http_parser_execute(parser, - client->read_buf.ptr + parsed_len, - 1); -#endif + /* + * http-parser has a "feature" where it will not deliver + * the final byte when paused in a callback. Consume + * that byte. + */ + if ((additional_size = git_http_parser_remain_after_pause(parser)) > 0) { + GIT_ASSERT((client->read_buf.size - parsed_len) >= additional_size); + + parsed_len += git_http_parser_execute(parser, + client->read_buf.ptr + parsed_len, + additional_size); + } } /* Most failures will be reported in http_errno */ - else if (git_http_parser_errno(client->parser) != HPE_OK) { + else if (git_http_parser_errno(parser) != GIT_HTTP_PARSER_OK) { git_error_set(GIT_ERROR_HTTP, "http parser error: %s", - git_http_errno_description(parser, http_errno)); + git_http_parser_errmsg(parser, http_errno)); return -1; } @@ -1279,7 +1207,7 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client) else if (parsed_len != client->read_buf.size) { git_error_set(GIT_ERROR_HTTP, "http parser did not consume entire buffer: %s", - git_http_errno_description(parser, http_errno)); + git_http_parser_errmsg(parser, http_errno)); return -1; } @@ -1318,7 +1246,7 @@ static void complete_response_body(git_http_client *client) /* If there was an error, just close the connection. */ if (client_read_and_parse(client) < 0 || - parser_context.error != HPE_OK || + parser_context.error != GIT_HTTP_PARSER_OK || (parser_context.parse_status != PARSE_STATUS_OK && parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { git_error_clear(); @@ -1596,7 +1524,7 @@ int git_http_client_skip_body(git_http_client *client) do { error = client_read_and_parse(client); - if (parser_context.error != HPE_OK || + if (parser_context.error != GIT_HTTP_PARSER_OK || (parser_context.parse_status != PARSE_STATUS_OK && parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { git_error_set(GIT_ERROR_HTTP, diff --git a/src/libgit2/transports/httpparser.c b/src/libgit2/transports/httpparser.c new file mode 100644 index 000000000..abd8f0244 --- /dev/null +++ b/src/libgit2/transports/httpparser.c @@ -0,0 +1,126 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "httpparser.h" + +#include + +#if defined(GIT_HTTPPARSER_HTTPPARSER) || defined(GIT_HTTPPARSER_BUILTIN) + +#include "http_parser.h" + +static int on_message_begin(http_parser *p) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_message_begin(parser); +} + +static int on_url(http_parser *p, const char *str, size_t len) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_url(parser, str, len); +} + +static int on_header_field(http_parser *p, const char *str, size_t len) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_header_field(parser, str, len); +} + +static int on_header_value(http_parser *p, const char *str, size_t len) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_header_value(parser, str, len); +} + +static int on_headers_complete(http_parser *p) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_headers_complete(parser); +} + +static int on_body(http_parser *p, const char *buf, size_t len) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_body(parser, buf, len); +} + +static int on_message_complete(http_parser *p) +{ + git_http_parser *parser = (git_http_parser *)p; + return parser->settings.on_message_complete(parser); +} + +void git_http_parser_init( + git_http_parser *parser, + git_http_parser_t type, + git_http_parser_settings *settings) +{ + http_parser_init(&parser->parser, (enum http_parser_type)type); + memcpy(&parser->settings, settings, sizeof(git_http_parser_settings)); +} + +size_t git_http_parser_execute( + git_http_parser *parser, + const char *data, + size_t len) +{ + struct http_parser_settings settings_proxy; + + settings_proxy.on_message_begin = parser->settings.on_message_begin ? on_message_begin : NULL; + settings_proxy.on_url = parser->settings.on_url ? on_url : NULL; + settings_proxy.on_header_field = parser->settings.on_header_field ? on_header_field : NULL; + settings_proxy.on_header_value = parser->settings.on_header_value ? on_header_value : NULL; + settings_proxy.on_headers_complete = parser->settings.on_headers_complete ? on_headers_complete : NULL; + settings_proxy.on_body = parser->settings.on_body ? on_body : NULL; + settings_proxy.on_message_complete = parser->settings.on_message_complete ? on_message_complete : NULL; + + return http_parser_execute(&parser->parser, &settings_proxy, data, len); +} + +#elif defined(GIT_HTTPPARSER_LLHTTP) + +# include + +size_t git_http_parser_execute( + git_http_parser *parser, + const char* data, + size_t len) +{ + llhttp_errno_t error; + size_t parsed_len; + + /* + * Unlike http_parser, which returns the number of parsed + * bytes in the _execute() call, llhttp returns an error + * code. + */ + + if (data == NULL || len == 0) + error = llhttp_finish(parser); + else + error = llhttp_execute(parser, data, len); + + parsed_len = len; + + /* + * Adjust number of parsed bytes in case of error. + */ + if (error != HPE_OK) { + parsed_len = llhttp_get_error_pos(parser) - data; + + /* This isn't a real pause, just a way to stop parsing early. */ + if (error == HPE_PAUSED_UPGRADE) + llhttp_resume_after_upgrade(parser); + } + + return parsed_len; +} + +#else +# error unknown http-parser +#endif diff --git a/src/libgit2/transports/httpparser.h b/src/libgit2/transports/httpparser.h new file mode 100644 index 000000000..616be587e --- /dev/null +++ b/src/libgit2/transports/httpparser.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#ifndef INCLUDE_transports_httpparser_h__ +#define INCLUDE_transports_httpparser_h__ + +#include "git2_util.h" + +#if defined(GIT_HTTPPARSER_HTTPPARSER) || defined(GIT_HTTPPARSER_BUILTIN) + +# include + +typedef enum { + GIT_HTTP_PARSER_OK = HPE_OK, + GIT_HTTP_PARSER_PAUSED = HPE_PAUSED, +} git_http_parser_error_t; + +typedef enum { + GIT_HTTP_PARSER_REQUEST = HTTP_REQUEST, + GIT_HTTP_PARSER_RESPONSE = HTTP_RESPONSE, +} git_http_parser_t; + +typedef struct git_http_parser git_http_parser; + +typedef struct { + int (*on_message_begin)(git_http_parser *); + int (*on_url)(git_http_parser *, const char *, size_t); + int (*on_header_field)(git_http_parser *, const char *, size_t); + int (*on_header_value)(git_http_parser *, const char *, size_t); + int (*on_headers_complete)(git_http_parser *); + int (*on_body)(git_http_parser *, const char *, size_t); + int (*on_message_complete)(git_http_parser *); +} git_http_parser_settings; + +struct git_http_parser { + http_parser parser; + git_http_parser_settings settings; + void *data; +}; + +void git_http_parser_init( + git_http_parser *parser, + git_http_parser_t type, + git_http_parser_settings *settings); + +size_t git_http_parser_execute( + git_http_parser *parser, + const char *data, + size_t len); + +# define git_http_parser_status_code(parser) parser->parser.status_code +# define git_http_parser_keep_alive(parser) http_should_keep_alive(&parser->parser) +# define git_http_parser_pause(parser) (http_parser_pause(&parser->parser, 1), 0) +# define git_http_parser_resume(parser) http_parser_pause(&parser->parser, 0) +# define git_http_parser_remain_after_pause(parser) 1 +# define git_http_parser_errno(parser) parser->parser.http_errno +# define git_http_parser_errmsg(parser, errno) http_errno_description(errno) + +#elif defined(GIT_HTTPPARSER_LLHTTP) + +# include + +typedef enum { + GIT_HTTP_PARSER_OK = HPE_OK, + GIT_HTTP_PARSER_PAUSED = HPE_PAUSED, +} git_http_parser_error_t; + +typedef enum { + GIT_HTTP_PARSER_REQUEST = HTTP_REQUEST, + GIT_HTTP_PARSER_RESPONSE = HTTP_RESPONSE, +} git_http_parser_t; + +typedef llhttp_t git_http_parser; +typedef llhttp_settings_t git_http_parser_settings; + +# define git_http_parser_init(parser, direction, settings) llhttp_init(parser, (llhttp_type_t)direction, settings) + +size_t git_http_parser_execute( + git_http_parser *parser, + const char *data, + size_t len); + +# define git_http_parser_status_code(parser) parser->status_code +# define git_http_parser_keep_alive(parser) llhttp_should_keep_alive(parser) +# define git_http_parser_pause(parser) (llhttp_pause(parser), GIT_HTTP_PARSER_PAUSED) +# define git_http_parser_resume(parser) llhttp_resume(parser) +# define git_http_parser_remain_after_pause(parser) 0 +# define git_http_parser_errno(parser) parser->error +# define git_http_parser_errmsg(parser, errno) llhttp_get_error_reason(parser) + +#else +# error unknown http-parser +#endif + +#endif diff --git a/src/util/git2_features.h.in b/src/util/git2_features.h.in index a328f77cb..52b732846 100644 --- a/src/util/git2_features.h.in +++ b/src/util/git2_features.h.in @@ -46,6 +46,10 @@ #cmakedefine GIT_MBEDTLS 1 #cmakedefine GIT_SCHANNEL 1 +#cmakedefine GIT_HTTPPARSER_HTTPPARSER 1 +#cmakedefine GIT_HTTPPARSER_LLHTTP 1 +#cmakedefine GIT_HTTPPARSER_BUILTIN 1 + #cmakedefine GIT_SHA1_COLLISIONDETECT 1 #cmakedefine GIT_SHA1_WIN32 1 #cmakedefine GIT_SHA1_COMMON_CRYPTO 1