Enable llhttp for HTTP parsing

Fixes: https://github.com/libgit2/libgit2/issues/6074

We now try to use llhttp by default, falling back to http-parser
if the former is not available.

As a last resort, we use the bundled http-parser.

Co-authored-by: Sergio Correia <scorreia@redhat.com>
Signed-off-by: Stephen Gallagher <sgallagh@redhat.com>
Signed-off-by: Sergio Correia <scorreia@redhat.com>
This commit is contained in:
Stephen Gallagher
2024-01-10 17:07:45 -05:00
parent 00eb347068
commit 06e384a0a4
5 changed files with 169 additions and 38 deletions

39
cmake/FindLLHTTP.cmake Normal file
View File

@@ -0,0 +1,39 @@
# - Try to find llhttp
#
# Defines the following variables:
#
# LLHTTP_FOUND - system has llhttp
# LLHTTP_INCLUDE_DIR - the llhttp include directory
# LLHTTP_LIBRARIES - Link these to use llhttp
# LLHTTP_VERSION_MAJOR - major version
# LLHTTP_VERSION_MINOR - minor version
# LLHTTP_VERSION_STRING - the version of llhttp found
# Find the header and library
find_path(LLHTTP_INCLUDE_DIR NAMES llhttp.h)
find_library(LLHTTP_LIBRARY NAMES llhttp libllhttp)
# Found the header, read version
if(LLHTTP_INCLUDE_DIR AND EXISTS "${LLHTTP_INCLUDE_DIR}/llhttp.h")
file(READ "${LLHTTP_INCLUDE_DIR}/llhttp.h" LLHTTP_H)
if(LLHTTP_H)
string(REGEX REPLACE ".*#define[\t ]+LLHTTP_VERSION_MAJOR[\t ]+([0-9]+).*" "\\1" LLHTTP_VERSION_MAJOR "${LLHTTP_H}")
string(REGEX REPLACE ".*#define[\t ]+LLHTTP_VERSION_MINOR[\t ]+([0-9]+).*" "\\1" LLHTTP_VERSION_MINOR "${LLHTTP_H}")
set(LLHTTP_VERSION_STRING "${LLHTTP_VERSION_MAJOR}.${LLHTTP_VERSION_MINOR}")
endif()
unset(LLHTTP_H)
endif()
# Handle the QUIETLY and REQUIRED arguments and set LLHTTP_FOUND
# to TRUE if all listed variables are TRUE
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LLHTTP REQUIRED_VARS LLHTTP_INCLUDE_DIR LLHTTP_LIBRARY)
# Hide advanced variables
mark_as_advanced(LLHTTP_INCLUDE_DIR LLHTTP_LIBRARY)
# Set standard variables
if(LLHTTP_FOUND)
set(LLHTTP_LIBRARIES ${LLHTTP_LIBRARY})
set(LLHTTP_INCLUDE_DIRS ${LLHTTP_INCLUDE_DIR})
endif()

View File

@@ -1,19 +1,39 @@
# Optional external dependency: http-parser
if(USE_HTTP_PARSER STREQUAL "system")
find_package(HTTPParser)
if(USE_HTTP_PARSER STREQUAL "builtin")
message(STATUS "support for bundled (legacy) http-parser explicitly requested")
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(FATAL_ERROR "http-parser support was requested but not found")
endif()
else()
message(STATUS "http-parser version 2 was not found or disabled; using bundled 3rd-party sources.")
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 "$<TARGET_OBJECTS:http-parser>")
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(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)")
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 "$<TARGET_OBJECTS:http-parser>")
add_feature_info(http-parser ON "http-parser support (bundled)")
endif()
endif()
endif()

View File

@@ -9,7 +9,6 @@
#ifndef GIT_WINHTTP
#include "http_parser.h"
#include "net.h"
#include "remote.h"
#include "smart.h"

View File

@@ -7,7 +7,32 @@
#include "common.h"
#include "git2.h"
#include "http_parser.h"
#ifdef USE_LLHTTP
#include <llhttp.h>
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 <http_parser.h>
/* 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"
@@ -108,7 +133,7 @@ struct git_http_client {
git_http_server_t current_server;
http_client_state state;
http_parser parser;
http_parser_t parser;
git_http_server server;
git_http_server proxy;
@@ -154,7 +179,7 @@ void git_http_response_dispose(git_http_response *response)
memset(response, 0, sizeof(git_http_response));
}
static int on_header_complete(http_parser *parser)
static int on_header_complete(http_parser_t *parser)
{
http_parser_context *ctx = (http_parser_context *) parser->data;
git_http_client *client = ctx->client;
@@ -219,7 +244,7 @@ static int on_header_complete(http_parser *parser)
return 0;
}
static int on_header_field(http_parser *parser, const char *str, size_t len)
static int on_header_field(http_parser_t *parser, const char *str, size_t len)
{
http_parser_context *ctx = (http_parser_context *) parser->data;
@@ -254,7 +279,7 @@ static int on_header_field(http_parser *parser, const char *str, size_t len)
return 0;
}
static int on_header_value(http_parser *parser, const char *str, size_t len)
static int on_header_value(http_parser_t *parser, const char *str, size_t len)
{
http_parser_context *ctx = (http_parser_context *) parser->data;
@@ -342,7 +367,7 @@ static int resend_needed(git_http_client *client, git_http_response *response)
return 0;
}
static int on_headers_complete(http_parser *parser)
static int on_headers_complete(http_parser_t *parser)
{
http_parser_context *ctx = (http_parser_context *) parser->data;
@@ -365,7 +390,7 @@ static int on_headers_complete(http_parser *parser)
}
ctx->response->status = parser->status_code;
ctx->client->keepalive = http_should_keep_alive(parser);
ctx->client->keepalive = git_http_should_keep_alive(parser);
/* Prepare for authentication */
collect_authinfo(&ctx->response->server_auth_schemetypes,
@@ -378,18 +403,28 @@ static int on_headers_complete(http_parser *parser)
ctx->response->resend_credentials = resend_needed(ctx->client,
ctx->response);
/* Stop parsing. */
http_parser_pause(parser, 1);
#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
}
static int on_body(http_parser *parser, const char *buf, size_t len)
static int on_body(http_parser_t *parser, const char *buf, size_t len)
{
http_parser_context *ctx = (http_parser_context *) parser->data;
size_t max_len;
@@ -411,7 +446,7 @@ static int on_body(http_parser *parser, const char *buf, size_t len)
return 0;
}
static int on_message_complete(http_parser *parser)
static int on_message_complete(http_parser_t *parser)
{
http_parser_context *ctx = (http_parser_context *) parser->data;
@@ -878,7 +913,7 @@ GIT_INLINE(int) server_setup_from_url(
static void reset_parser(git_http_client *client)
{
http_parser_init(&client->parser, HTTP_RESPONSE);
git_http_parser_init(&client->parser);
}
static int setup_hosts(
@@ -1122,9 +1157,46 @@ GIT_INLINE(int) client_read(git_http_client *client)
}
static bool parser_settings_initialized;
static http_parser_settings parser_settings;
static http_settings_t parser_settings;
GIT_INLINE(http_parser_settings *) http_client_parser_settings(void)
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;
@@ -1141,7 +1213,7 @@ GIT_INLINE(http_parser_settings *) http_client_parser_settings(void)
GIT_INLINE(int) client_read_and_parse(git_http_client *client)
{
http_parser *parser = &client->parser;
http_parser_t *parser = &client->parser;
http_parser_context *ctx = (http_parser_context *) parser->data;
unsigned char http_errno;
int read_len;
@@ -1155,11 +1227,10 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client)
if (!client->read_buf.size && (read_len = client_read(client)) < 0)
return read_len;
parsed_len = http_parser_execute(parser,
http_client_parser_settings(),
parsed_len = git_http_parser_execute(parser,
client->read_buf.ptr,
client->read_buf.size);
http_errno = client->parser.http_errno;
http_errno = git_http_parser_errno(client->parser);
if (parsed_len > INT_MAX) {
git_error_set(GIT_ERROR_HTTP, "unexpectedly large parse");
@@ -1179,6 +1250,7 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client)
* 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.
@@ -1186,18 +1258,20 @@ GIT_INLINE(int) client_read_and_parse(git_http_client *client)
*/
GIT_ASSERT(client->read_buf.size > parsed_len);
http_parser_pause(parser, 0);
#endif
git_http_parser_resume(parser);
parsed_len += http_parser_execute(parser,
http_client_parser_settings(),
#ifndef USE_LLHTTP
parsed_len += git_http_parser_execute(parser,
client->read_buf.ptr + parsed_len,
1);
#endif
}
/* Most failures will be reported in http_errno */
else if (parser->http_errno != HPE_OK) {
else if (git_http_parser_errno(client->parser) != HPE_OK) {
git_error_set(GIT_ERROR_HTTP, "http parser error: %s",
http_errno_description(http_errno));
git_http_errno_description(parser, http_errno));
return -1;
}
@@ -1205,7 +1279,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",
http_errno_description(http_errno));
git_http_errno_description(parser, http_errno));
return -1;
}

View File

@@ -11,7 +11,6 @@
#include "posix.h"
#include "str.h"
#include "http_parser.h"
#include "runtime.h"
#define DEFAULT_PORT_HTTP "80"