http: abstract http parsing out of httpclient

Avoid #ifdef's in httpclient.c, and move http parsing into its own file.
This commit is contained in:
Edward Thomson
2024-04-19 21:07:11 +01:00
parent 3599de9073
commit d396819101
5 changed files with 305 additions and 155 deletions

View File

@@ -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 "$<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(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 "$<TARGET_OBJECTS:http-parser>")
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 "$<TARGET_OBJECTS:http-parser>")
set(GIT_HTTPPARSER_BUILTIN 1)
add_feature_info(http-parser ON "using bundled parser")
endif()

View File

@@ -8,31 +8,6 @@
#include "common.h"
#include "git2.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"
@@ -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,

View File

@@ -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 <string.h>
#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 <llhttp.h>
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

View File

@@ -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 <http_parser.h>
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 <llhttp.h>
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

View File

@@ -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