[core] relay 1xx from backend over HTTP/2

relay 1xx from backend over HTTP/2, e.g. 103 Early Hints
(if client is connected using HTTP/2)

enabled by default unless disabled in lighttpd.conf with:
  server.feature-flags += ( "server.h2-discard-backend-1xx" = "enable" )

Warning: backends which send 103 Early Hints should check User-Agent
before doing so since naive clients might not handle unexpected 1xx.
Some clients may take the 1xx response as the final response, expecting
only one response.  Some clients might not properly handle 100 Continue
if the client did not send Expect: 100-continue with the request.
  https://tools.ietf.org/html/rfc8297#section-3 Security Considerations

x-ref:
  An HTTP Status Code for Indicating Hints (103 Early Hints)
  https://tools.ietf.org/html/rfc8297
This commit is contained in:
Glenn Strauss 2020-09-17 19:29:59 -04:00
parent 10d9d14633
commit 869c778aa7
6 changed files with 93 additions and 7 deletions

View File

@ -2098,11 +2098,43 @@ h2_send_headers_block (request_st * const r, connection * const con, const char
}
static void
h2_send_1xx_block (request_st * const r, connection * const con, const char * const hdrs, const uint32_t hlen)
{
h2_send_headers_block(r, con, hdrs, hlen, 0);
}
int
h2_send_1xx (request_st * const r, connection * const con)
{
buffer * const b = chunk_buffer_acquire();
buffer_copy_string_len(b, CONST_STR_LEN(":status: "));
buffer_append_int(b, r->http_status);
for (uint32_t i = 0; i < r->resp_headers.used; ++i) {
const data_string * const ds = (data_string *)r->resp_headers.data[i];
if (buffer_string_is_empty(&ds->value)) continue;
if (buffer_string_is_empty(&ds->key)) continue;
buffer_append_string_len(b, CONST_STR_LEN("\r\n"));
buffer_append_string_buffer(b, &ds->key);
buffer_append_string_len(b, CONST_STR_LEN(": "));
buffer_append_string_buffer(b, &ds->value);
}
buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n"));
h2_send_1xx_block(r, con, CONST_BUF_LEN(b));
chunk_buffer_release(b);
return 1; /* for http_response_send_1xx_cb */
}
void
h2_send_100_continue (request_st * const r, connection * const con)
{
/* place frame directly in con->write_queue for accounting to be part of
* HTTP/2 protocol overhead, and not part of response header or body len */
/* 100 Continue is small and will always fit in SETTING_MAX_FRAME_SIZE;
* i.e. there will not be any CONTINUATION frames here */
@ -2114,7 +2146,7 @@ h2_send_100_continue (request_st * const r, connection * const con)
/* short header block, so reuse shared code used for trailers
* rather than adding something specific for ls-hpack here */
h2_send_headers_block(r, con, CONST_STR_LEN(":status: 100\r\n\r\n"), 0);
h2_send_1xx_block(r, con, CONST_STR_LEN(":status: 100\r\n\r\n"));
}

View File

@ -102,6 +102,8 @@ int h2_want_read (connection *con);
void h2_init_con (request_st * restrict h2r, connection * restrict con, const buffer * restrict http2_settings);
int h2_send_1xx (request_st *r, connection *con);
void h2_send_100_continue (request_st *r, connection *con);
void h2_send_headers (request_st *r, connection *con);

View File

@ -1185,6 +1185,36 @@ static int http_response_process_headers(request_st * const r, http_response_opt
}
static http_response_send_1xx_cb http_response_send_1xx_h1;
static http_response_send_1xx_cb http_response_send_1xx_h2;
void
http_response_send_1xx_cb_set (http_response_send_1xx_cb fn, int vers)
{
if (vers >= HTTP_VERSION_2)
http_response_send_1xx_h2 = fn;
else if (vers == HTTP_VERSION_1_1)
http_response_send_1xx_h1 = fn;
}
int
http_response_send_1xx (request_st * const r)
{
http_response_send_1xx_cb http_response_send_1xx_fn = NULL;
if (r->http_version >= HTTP_VERSION_2)
http_response_send_1xx_fn = http_response_send_1xx_h2;
else if (r->http_version == HTTP_VERSION_1_1)
http_response_send_1xx_fn = http_response_send_1xx_h1;
if (http_response_send_1xx_fn && !http_response_send_1xx_fn(r, r->con))
return 0; /* error occurred */
http_response_header_clear(r);
return 1; /* 1xx response handled */
}
__attribute_cold__
__attribute_noinline__
static int
@ -1203,8 +1233,8 @@ http_response_check_1xx (request_st * const r, buffer * const restrict b, uint32
/* Note: while GW_AUTHORIZER mode is not expected to return 1xx, as a
* feature, 1xx responses from authorizer are passed back to client */
http_response_header_clear(r);
return 1; /* 1xx response handled; loop for next response headers */
return http_response_send_1xx(r);
/* 0: error, 1: 1xx response handled; loop for next response headers */
}

View File

@ -5,6 +5,7 @@
#include "buffer.h"
#include "http_chunk.h"
#include "http_header.h"
#include "response.h" /* http_response_send_1xx() */
#include "plugin.h"
@ -980,7 +981,7 @@ static handler_t magnet_attract(request_st * const r, plugin_data * const p, buf
{
handler_t result = HANDLER_GO_ON;
if (lua_return_value > 99) {
if (lua_return_value >= 200) {
r->http_status = lua_return_value;
r->resp_body_finished = 1;
@ -998,6 +999,12 @@ static handler_t magnet_attract(request_st * const r, plugin_data * const p, buf
}
result = HANDLER_FINISHED;
} else if (lua_return_value >= 100) {
/*(custom lua code should not return 101 Switching Protocols)*/
r->http_status = lua_return_value;
result = http_response_send_1xx(r)
? HANDLER_GO_ON
: HANDLER_ERROR;
} else if (MAGNET_RESTART_REQUEST == lua_return_value) {
result = HANDLER_COMEBACK;
}

View File

@ -40,6 +40,11 @@ typedef struct http_response_opts_t {
typedef int (*http_cgi_header_append_cb)(void *vdata, const char *k, size_t klen, const char *v, size_t vlen);
int http_cgi_headers(request_st *r, http_cgi_opts *opts, http_cgi_header_append_cb cb, void *vdata);
typedef int (*http_response_send_1xx_cb)(request_st *r, connection *con);
__attribute_cold__
void http_response_send_1xx_cb_set (http_response_send_1xx_cb fn, int vers);
int http_response_send_1xx (request_st *r);
handler_t http_response_parse_headers(request_st *r, http_response_opts *opts, buffer *hdrs);
handler_t http_response_read(request_st *r, http_response_opts *opts, buffer *b, fdnode *fdn);

View File

@ -6,6 +6,7 @@
#include "log.h"
#include "rand.h"
#include "chunk.h"
#include "h2.h" /* h2_send_1xx() */
#include "http_auth.h" /* http_auth_dumbdata_reset() */
#include "http_vhostdb.h" /* http_vhostdb_dumbdata_reset() */
#include "fdevent.h"
@ -13,9 +14,10 @@
#include "sock_addr.h"
#include "stat_cache.h"
#include "plugin.h"
#include "plugin_config.h" /* config_plugin_value_tobool() */
#include "network_write.h" /* network_write_show_handlers() */
#include "reqpool.h" /* request_pool_init() request_pool_free() */
#include "response.h" /* strftime_cache_reset() */
#include "response.h" /* http_response_send_1xx_cb_set() strftime_cache_reset() */
#ifdef HAVE_VERSIONSTAMP_H
# include "versionstamp.h"
@ -894,6 +896,14 @@ static int server_main_setup (server * const srv, int argc, char **argv) {
#endif
}
http_response_send_1xx_cb_set(NULL, HTTP_VERSION_2);
if (srv->srvconf.feature_flags
&& !config_plugin_value_tobool(
array_get_element_klen(srv->srvconf.feature_flags,
CONST_STR_LEN("server.h2-discard-backend-1xx")), 0))
http_response_send_1xx_cb_set(h2_send_1xx,
HTTP_VERSION_2);
if (0 != config_set_defaults(srv)) {
log_error(srv->errh, __FILE__, __LINE__,
"setting default values failed");