diff --git a/COPYING b/COPYING index a43e8375..6e079b80 100644 --- a/COPYING +++ b/COPYING @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2012, 2014, 2015 Tatsuhiro Tsujikawa +Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..45d9408f --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +See COPYING diff --git a/README.rst b/README.rst index 5b29e3d2..aa40c628 100644 --- a/README.rst +++ b/README.rst @@ -274,7 +274,7 @@ for this 24 bytes byte string and updated API. ``NGHTTP2_CLIENT_MAGIC_LEN``. * ``NGHTTP2_BAD_PREFACE`` was renamed as ``NGHTTP2_BAD_CLIENT_MAGIC`` -The alreay deprecated ``NGHTTP2_CLIENT_CONNECTION_HEADER`` and +The already deprecated ``NGHTTP2_CLIENT_CONNECTION_HEADER`` and ``NGHTTP2_CLIENT_CONNECTION_HEADER_LEN`` were removed. If application uses these macros, just replace old ones with new ones. diff --git a/configure.ac b/configure.ac index 6bc25f4d..33f461cf 100644 --- a/configure.ac +++ b/configure.ac @@ -25,7 +25,7 @@ dnl Do not change user variables! dnl http://www.gnu.org/software/automake/manual/html_node/Flag-Variables-Ordering.html AC_PREREQ(2.61) -AC_INIT([nghttp2], [1.5.1-DEV], [t-tujikawa@users.sourceforge.net]) +AC_INIT([nghttp2], [1.6.1-DEV], [t-tujikawa@users.sourceforge.net]) AC_CONFIG_AUX_DIR([.]) AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_HEADERS([config.h]) @@ -46,9 +46,9 @@ m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) dnl See versioning rule: dnl http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html -AC_SUBST(LT_CURRENT, 17) +AC_SUBST(LT_CURRENT, 18) AC_SUBST(LT_REVISION, 0) -AC_SUBST(LT_AGE, 3) +AC_SUBST(LT_AGE, 4) major=`echo $PACKAGE_VERSION |cut -d. -f1 | sed -e "s/[^0-9]//g"` minor=`echo $PACKAGE_VERSION |cut -d. -f2 | sed -e "s/[^0-9]//g"` @@ -104,6 +104,11 @@ AC_ARG_ENABLE([failmalloc], [Do not build failmalloc test program])], [request_failmalloc=$enableval], [request_failmalloc=yes]) +AC_ARG_ENABLE([lib-only], + [AS_HELP_STRING([--enable-lib-only], + [Build libnghttp2 only. This is a short hand for --disable-app --disable-examples --disable-hpack-tools --disable-python-bindings])], + [request_lib_only=$enableval], [request_lib_only=no]) + AC_ARG_WITH([libxml2], [AS_HELP_STRING([--with-libxml2], [Use libxml2 [default=check]])], @@ -150,6 +155,13 @@ PKG_PROG_PKG_CONFIG([0.20]) AM_PATH_PYTHON([2.7],, [:]) +if [test "x$request_lib_only" = "xyes"]; then + request_app=no + request_hpack_tools=no + request_examples=no + request_python_bindings=no +fi + if [test "x$request_python_bindings" != "xno"]; then AX_PYTHON_DEVEL([>= '2.7']) fi @@ -243,6 +255,11 @@ if test "x${have_zlib}" = "xno"; then AC_MSG_NOTICE($ZLIB_PKG_ERRORS) fi +# dl: openssl requires libdl when it is statically linked. +LIBS_OLD=$LIBS +AC_SEARCH_LIBS([dlopen], [dl], [APPLDFLAGS="-ldl $APPLDFLAGS"], [], []) +LIBS=$LIBS_OLD + # cunit PKG_CHECK_MODULES([CUNIT], [cunit >= 2.1], [have_cunit=yes], [have_cunit=no]) # If pkg-config does not find cunit, check it using AC_CHECK_LIB. We @@ -613,6 +630,10 @@ AC_CHECK_FUNCS([ \ AC_CHECK_FUNC([timerfd_create], [have_timerfd_create=yes], [have_timerfd_create=no]) +# For cygwin: we can link initgroups, so AC_CHECK_FUNCS succeeds, but +# cygwin disables initgroups due to feature test macro magic with our +# configuration. +AC_CHECK_DECLS([initgroups], [], [], [[#include ]]) # Checks for epoll availability, primarily for examples/tiny-nghttpd AX_HAVE_EPOLL([have_epoll=yes], [have_epoll=no]) diff --git a/doc/conf.py.in b/doc/conf.py.in index 66022335..9c192827 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -66,7 +66,7 @@ master_doc = 'index' # General information about the project. project = u'nghttp2' -copyright = u'2012, 2015, Tatsuhiro Tsujikawa' +copyright = u'2012, 2015, 2016, Tatsuhiro Tsujikawa' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/h2load.1 b/doc/h2load.1 index 241d6505..50ad4f8c 100644 --- a/doc/h2load.1 +++ b/doc/h2load.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "H2LOAD" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" +.TH "H2LOAD" "1" "December 23, 2015" "1.6.0" "nghttp2" .SH NAME h2load \- HTTP/2 benchmarking tool . @@ -374,6 +374,28 @@ The fraction of the number of connections within standard deviation range (mean +/\- sd) against total number of successful connections. .UNINDENT +.TP +.B req/s +.INDENT 7.0 +.TP +.B min +The minimum request per second among all clients. +.TP +.B max +The maximum request per second among all clients. +.TP +.B mean +The mean request per second among all clients. +.TP +.B sd +The standard deviation of request per second among all clients. +server. +.TP +.B +/\- sd +The fraction of the number of connections within standard +deviation range (mean +/\- sd) against total number of successful +connections. +.UNINDENT .UNINDENT .SH FLOW CONTROL .sp diff --git a/doc/h2load.1.rst b/doc/h2load.1.rst index 448c4276..368c6ab5 100644 --- a/doc/h2load.1.rst +++ b/doc/h2load.1.rst @@ -213,6 +213,8 @@ is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms (hours, minutes, seconds and milliseconds, respectively). If a unit is omitted, a second is used as unit. +.. _h2load-1-output: + OUTPUT ------ @@ -301,6 +303,21 @@ time for 1st byte (of (decrypted in case of TLS) application data) deviation range (mean +/- sd) against total number of successful connections. +req/s + min + The minimum request per second among all clients. + max + The maximum request per second among all clients. + mean + The mean request per second among all clients. + sd + The standard deviation of request per second among all clients. + server. + +/- sd + The fraction of the number of connections within standard + deviation range (mean +/- sd) against total number of successful + connections. + FLOW CONTROL ------------ diff --git a/doc/h2load.h2r b/doc/h2load.h2r index e022aa8d..f06f7e01 100644 --- a/doc/h2load.h2r +++ b/doc/h2load.h2r @@ -1,3 +1,5 @@ +.. _h2load-1-output: + OUTPUT ------ @@ -86,7 +88,7 @@ time for 1st byte (of (decrypted in case of TLS) application data) deviation range (mean +/- sd) against total number of successful connections. -req/s (client) +req/s min The minimum request per second among all clients. max diff --git a/doc/nghttp.1 b/doc/nghttp.1 index b246da69..6d7feb45 100644 --- a/doc/nghttp.1 +++ b/doc/nghttp.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "NGHTTP" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" +.TH "NGHTTP" "1" "December 23, 2015" "1.6.0" "nghttp2" .SH NAME nghttp \- HTTP/2 client . @@ -152,7 +152,8 @@ Default: \fB16\fP .B \-M, \-\-peer\-max\-concurrent\-streams= Use as SETTINGS_MAX_CONCURRENT_STREAMS value of remote endpoint as if it is received in SETTINGS frame. -The default is large enough as it is seen as unlimited. +.sp +Default: \fB100\fP .UNINDENT .INDENT 0.0 .TP diff --git a/doc/nghttp.1.rst b/doc/nghttp.1.rst index 128eeab6..775f86f0 100644 --- a/doc/nghttp.1.rst +++ b/doc/nghttp.1.rst @@ -116,7 +116,8 @@ OPTIONS Use as SETTINGS_MAX_CONCURRENT_STREAMS value of remote endpoint as if it is received in SETTINGS frame. - The default is large enough as it is seen as unlimited. + + Default: ``100`` .. option:: -c, --header-table-size= diff --git a/doc/nghttpd.1 b/doc/nghttpd.1 index ad8d93da..6d841e82 100644 --- a/doc/nghttpd.1 +++ b/doc/nghttpd.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "NGHTTPD" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" +.TH "NGHTTPD" "1" "December 23, 2015" "1.6.0" "nghttp2" .SH NAME nghttpd \- HTTP/2 server . diff --git a/doc/nghttpx.1 b/doc/nghttpx.1 index a0982c49..093a7fb9 100644 --- a/doc/nghttpx.1 +++ b/doc/nghttpx.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "NGHTTPX" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" +.TH "NGHTTPX" "1" "December 23, 2015" "1.6.0" "nghttp2" .SH NAME nghttpx \- HTTP/2 proxy . diff --git a/doc/sources/h2load-howto.rst b/doc/sources/h2load-howto.rst index 7c07194f..af3c42e3 100644 --- a/doc/sources/h2load-howto.rst +++ b/doc/sources/h2load-howto.rst @@ -1,10 +1,10 @@ h2load - HTTP/2 benchmarking tool - HOW-TO ========================================== -h2load is benchmarking tool for HTTP/2. If built with +h2load is benchmarking tool for HTTP/2 and HTTP/1.1. If built with spdylay (http://tatsuhiro-t.github.io/spdylay/) library, it also -supports SPDY protocol. It supports SSL/TLS and clear text for both -HTTP/2 and SPDY. +supports SPDY protocol. It supports SSL/TLS and clear text for all +supported protocols. Basic Usage ----------- @@ -22,29 +22,37 @@ In order to set benchmark settings, specify following 3 options. If ``auto`` is given, the number of given URIs is used. Default: ``auto`` +For SSL/TLS connection, the protocol will be negotiated via ALPN/NPN. +You can set specific protocols in ``--npn-list`` option. For +cleartext connection, the default protocol is HTTP/2. To change the +protocol in cleartext connection, use ``--no-tls-proto`` option. For +convenience, ``--h1`` option forces HTTP/1.1 for both cleartext and +SSL/TLS connections. + Here is a command-line to perform benchmark to URI \https://localhost using total 100000 requests, 100 concurrent clients and 10 max -concurrent streams:: +concurrent streams: + +.. code-block:: text $ h2load -n100000 -c100 -m10 https://localhost -The benchmarking result looks like this:: +The benchmarking result looks like this: - finished in 0 sec, 385 millisec and 851 microsec, 2591 req/s, 1689 kbytes/s - requests: 1000 total, 1000 started, 1000 done, 1000 succeeded, 0 failed, 0 errored - status codes: 1000 2xx, 0 3xx, 0 4xx, 0 5xx - traffic: 667500 bytes total, 28700 bytes headers, 612000 bytes data +.. code-block:: text -The number of ``failed`` is the number of requests returned with non -2xx status. The number of ``error`` is the number of ``failed`` plus -the number of requests which failed with connection error. + finished in 7.08s, 141164.80 req/s, 555.33MB/s + requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout + status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx + traffic: 4125025824 bytes total, 11023424 bytes headers (space savings 93.07%), 4096000000 bytes data + min max mean sd +/- sd + time for request: 15.31ms 146.85ms 69.78ms 9.26ms 92.43% + time for connect: 1.08ms 25.04ms 10.71ms 9.80ms 64.00% + time to 1st byte: 25.36ms 184.96ms 79.11ms 53.97ms 78.00% + req/s (client) : 1412.04 1447.84 1426.52 10.57 63.00% -The number of ``total`` in ``traffic`` is the received application -data. If SSL/TLS is used, this number is calculated after decryption. -The number of ``headers`` is the sum of payload size of response -HEADERS (or SYN_REPLY for SPDY). This number comes before -decompressing header block. The number of ``data`` is the sum of -response body. +See the h2load manual page :ref:`h2load-1-output` section for the +explanation of the above numbers. Flow Control ------------ diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 104159fc..e655f935 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -3296,6 +3296,9 @@ nghttp2_priority_spec_check_default(const nghttp2_priority_spec *pri_spec); * :enum:`NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE` * No stream ID is available because maximum stream ID was * reached. + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * Trying to depend on itself (new stream ID equals + * ``pri_spec->stream_id``). * * .. warning:: * @@ -3407,7 +3410,7 @@ nghttp2_submit_response(nghttp2_session *session, int32_t stream_id, * has already sent and if `nghttp2_submit_trailer()` is called before * any response HEADERS submission (usually by * `nghttp2_submit_response()`), the content of |nva| will be sent as - * reponse headers, which will result in error. + * response headers, which will result in error. * * This function has the same effect with `nghttp2_submit_headers()`, * with flags = :enum:`NGHTTP2_FLAG_END_HEADERS` and both pri_spec and @@ -3506,7 +3509,8 @@ NGHTTP2_EXTERN int nghttp2_submit_trailer(nghttp2_session *session, * No stream ID is available because maximum stream ID was * reached. * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` - * The |stream_id| is 0. + * The |stream_id| is 0; or trying to depend on itself (stream ID + * equals ``pri_spec->stream_id``). * :enum:`NGHTTP2_ERR_DATA_EXIST` * DATA or HEADERS has been already submitted and not fully * processed yet. This happens if stream denoted by |stream_id| @@ -3549,7 +3553,7 @@ nghttp2_submit_headers(nghttp2_session *session, uint8_t flags, * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` * The |stream_id| is 0. * :enum:`NGHTTP2_ERR_STREAM_CLOSED` - * The stream was alreay closed; or the |stream_id| is invalid. + * The stream was already closed; or the |stream_id| is invalid. * * .. note:: * @@ -3718,7 +3722,7 @@ NGHTTP2_EXTERN int nghttp2_submit_settings(nghttp2_session *session, * The |stream_id| is 0; The |stream_id| does not designate stream * that peer initiated. * :enum:`NGHTTP2_ERR_STREAM_CLOSED` - * The stream was alreay closed; or the |stream_id| is invalid. + * The stream was already closed; or the |stream_id| is invalid. * * .. warning:: * @@ -4336,7 +4340,7 @@ typedef enum { * :enum:`NGHTTP2_ERR_HEADER_COMP` * Inflation process has failed. * :enum:`NGHTTP2_ERR_BUFFER_ERROR` - * The heder field name or value is too large. + * The header field name or value is too large. * * Example follows:: * diff --git a/lib/nghttp2_outbound_item.h b/lib/nghttp2_outbound_item.h index c16d5dfe..f1daeb1c 100644 --- a/lib/nghttp2_outbound_item.h +++ b/lib/nghttp2_outbound_item.h @@ -43,9 +43,6 @@ typedef struct { /* nonzero if request HEADERS is canceled. The error code is stored in |error_code|. */ uint8_t canceled; - /* nonzero if this item should be attached to stream object to make - it under priority control */ - uint8_t attach_stream; } nghttp2_headers_aux_data; /* struct used for DATA frame */ diff --git a/lib/nghttp2_pq.c b/lib/nghttp2_pq.c index 54c3eca4..bebccc76 100644 --- a/lib/nghttp2_pq.c +++ b/lib/nghttp2_pq.c @@ -44,11 +44,13 @@ void nghttp2_pq_free(nghttp2_pq *pq) { } static void swap(nghttp2_pq *pq, size_t i, size_t j) { - nghttp2_pq_entry *t = pq->q[i]; - pq->q[i] = pq->q[j]; - pq->q[i]->index = i; - pq->q[j] = t; - pq->q[j]->index = j; + nghttp2_pq_entry *a = pq->q[i]; + nghttp2_pq_entry *b = pq->q[j]; + + pq->q[i] = b; + b->index = i; + pq->q[j] = a; + a->index = j; } static void bubble_up(nghttp2_pq *pq, size_t index) { diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 1946560d..1ca950c1 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -635,7 +635,7 @@ int nghttp2_session_reprioritize_stream( if (pri_spec->stream_id != 0) { dep_stream = nghttp2_session_get_stream_raw(session, pri_spec->stream_id); - if (session->server && !dep_stream && + if (!dep_stream && session_detect_idle_stream(session, pri_spec->stream_id)) { nghttp2_priority_spec_default_init(&pri_spec_default); @@ -671,15 +671,9 @@ int nghttp2_session_reprioritize_stream( assert(dep_stream); if (dep_stream == stream->dep_prev && !pri_spec->exclusive) { - /* This is minor optimization when just weight is changed. - Currently, we don't reschedule stream in this case, since we - don't retain enough information to do that - (descendant_last_cycle we used to schedule it). This means new - weight is only applied in the next scheduling, and if weight is - drastically increased, library is not responding very quickly. - If this is really an issue, we will do workaround for this. */ - dep_stream->sum_dep_weight += pri_spec->weight - stream->weight; - stream->weight = pri_spec->weight; + /* This is minor optimization when just weight is changed. */ + nghttp2_stream_change_weight(stream, pri_spec->weight); + return 0; } @@ -729,20 +723,6 @@ int nghttp2_session_add_item(nghttp2_session *session, break; } - if (stream && item->aux_data.headers.attach_stream) { - if (stream->item) { - return NGHTTP2_ERR_DATA_EXIST; - } - - rv = nghttp2_stream_attach_item(stream, item); - - if (rv != 0) { - return rv; - } - - break; - } - nghttp2_outbound_queue_push(&session->ob_reg, item); item->queued = 1; break; @@ -778,6 +758,10 @@ int nghttp2_session_add_item(nghttp2_session *session, return NGHTTP2_ERR_NOMEM; } + /* We don't have to call nghttp2_session_adjust_closed_stream() + here, since stream->stream_id is local stream_id, and it does + not affect closed stream count. */ + nghttp2_outbound_queue_push(&session->ob_reg, item); item->queued = 1; @@ -909,15 +893,6 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, return NULL; } } else { - if (session->server && initial_state != NGHTTP2_STREAM_IDLE && - !nghttp2_session_is_my_stream_id(session, stream_id)) { - - rv = nghttp2_session_adjust_closed_stream(session, 1); - if (rv != 0) { - return NULL; - } - } - stream = nghttp2_mem_malloc(mem, sizeof(nghttp2_stream)); if (stream == NULL) { return NULL; @@ -929,7 +904,7 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, if (pri_spec->stream_id != 0) { dep_stream = nghttp2_session_get_stream_raw(session, pri_spec->stream_id); - if (session->server && !dep_stream && + if (!dep_stream && session_detect_idle_stream(session, pri_spec->stream_id)) { /* Depends on idle stream, which does not exist in memory. Assign default priority for it. */ @@ -994,11 +969,7 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, case NGHTTP2_STREAM_IDLE: /* Idle stream does not count toward the concurrent streams limit. This is used as anchor node in dependency tree. */ - assert(session->server); - rv = nghttp2_session_keep_idle_stream(session, stream); - if (rv != 0) { - return NULL; - } + nghttp2_session_keep_idle_stream(session, stream); break; default: if (nghttp2_session_is_my_stream_id(session, stream_id)) { @@ -1103,7 +1074,9 @@ int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id, /* On server side, retain stream at most MAX_CONCURRENT_STREAMS combined with the current active incoming streams to make dependency tree work better. */ - rv = nghttp2_session_keep_closed_stream(session, stream); + nghttp2_session_keep_closed_stream(session, stream); + + rv = nghttp2_session_adjust_closed_stream(session); } else { rv = nghttp2_session_destroy_stream(session, stream); } @@ -1138,10 +1111,8 @@ int nghttp2_session_destroy_stream(nghttp2_session *session, return 0; } -int nghttp2_session_keep_closed_stream(nghttp2_session *session, - nghttp2_stream *stream) { - int rv; - +void nghttp2_session_keep_closed_stream(nghttp2_session *session, + nghttp2_stream *stream) { DEBUGF(fprintf(stderr, "stream: keep closed stream(%p)=%d, state=%d\n", stream, stream->stream_id, stream->state)); @@ -1154,19 +1125,10 @@ int nghttp2_session_keep_closed_stream(nghttp2_session *session, session->closed_stream_tail = stream; ++session->num_closed_streams; - - rv = nghttp2_session_adjust_closed_stream(session, 0); - if (rv != 0) { - return rv; - } - - return 0; } -int nghttp2_session_keep_idle_stream(nghttp2_session *session, - nghttp2_stream *stream) { - int rv; - +void nghttp2_session_keep_idle_stream(nghttp2_session *session, + nghttp2_stream *stream) { DEBUGF(fprintf(stderr, "stream: keep idle stream(%p)=%d, state=%d\n", stream, stream->stream_id, stream->state)); @@ -1179,13 +1141,6 @@ int nghttp2_session_keep_idle_stream(nghttp2_session *session, session->idle_stream_tail = stream; ++session->num_idle_streams; - - rv = nghttp2_session_adjust_idle_stream(session); - if (rv != 0) { - return rv; - } - - return 0; } void nghttp2_session_detach_idle_stream(nghttp2_session *session, @@ -1216,8 +1171,7 @@ void nghttp2_session_detach_idle_stream(nghttp2_session *session, --session->num_idle_streams; } -int nghttp2_session_adjust_closed_stream(nghttp2_session *session, - size_t offset) { +int nghttp2_session_adjust_closed_stream(nghttp2_session *session) { size_t num_stream_max; int rv; @@ -1231,7 +1185,7 @@ int nghttp2_session_adjust_closed_stream(nghttp2_session *session, num_stream_max)); while (session->num_closed_streams > 0 && - session->num_closed_streams + session->num_incoming_streams + offset > + session->num_closed_streams + session->num_incoming_streams > num_stream_max) { nghttp2_stream *head_stream; nghttp2_stream *next; @@ -1267,13 +1221,11 @@ int nghttp2_session_adjust_idle_stream(nghttp2_session *session) { size_t max; int rv; - /* Make minimum number of idle streams 2 so that allocating 2 - streams at once is easy. This happens when PRIORITY frame to - idle stream, which depends on idle stream which does not - exist. */ - max = - nghttp2_max(2, nghttp2_min(session->local_settings.max_concurrent_streams, - session->pending_local_max_concurrent_stream)); + /* Make minimum number of idle streams 16, which is arbitrary chosen + number. */ + max = nghttp2_max(16, + nghttp2_min(session->local_settings.max_concurrent_streams, + session->pending_local_max_concurrent_stream)); DEBUGF(fprintf(stderr, "stream: adjusting kept idle streams " "num_idle_streams=%zu, max=%zu\n", @@ -1331,6 +1283,15 @@ int nghttp2_session_close_stream_if_shut_rdwr(nghttp2_session *session, return 0; } +/* + * Returns nonzero if local endpoint allows reception of new stream + * from remote. + */ +static int session_allow_incoming_new_stream(nghttp2_session *session) { + return (session->goaway_flags & + (NGHTTP2_GOAWAY_TERM_ON_SEND | NGHTTP2_GOAWAY_SENT)) == 0; +} + /* * This function returns nonzero if session is closing. */ @@ -1850,6 +1811,9 @@ static int session_prep_frame(nghttp2_session *session, return NGHTTP2_ERR_NOMEM; } + /* We don't call nghttp2_session_adjust_closed_stream() here, + since we don't keep closed stream in client side */ + estimated_payloadlen = session_estimate_headers_payload( session, frame->headers.nva, frame->headers.nvlen, NGHTTP2_PRIORITY_SPECLEN); @@ -1899,7 +1863,7 @@ static int session_prep_frame(nghttp2_session *session, } if (rv != 0) { - // If stream was alreay closed, nghttp2_session_get_stream() + // If stream was already closed, nghttp2_session_get_stream() // returns NULL, but item is still attached to the stream. // Search stream including closed again. stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id); @@ -2061,7 +2025,7 @@ static int session_prep_frame(nghttp2_session *session, rv = nghttp2_session_predicate_data_send(session, stream); if (rv != 0) { - // If stream was alreay closed, nghttp2_session_get_stream() + // If stream was already closed, nghttp2_session_get_stream() // returns NULL, but item is still attached to the stream. // Search stream including closed again. stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id); @@ -2442,11 +2406,25 @@ static int session_after_frame_sent1(nghttp2_session *session) { stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id); if (!stream) { - break; + if (!session_detect_idle_stream(session, frame->hd.stream_id)) { + break; + } + + stream = nghttp2_session_open_stream( + session, frame->hd.stream_id, NGHTTP2_FLAG_NONE, + &frame->priority.pri_spec, NGHTTP2_STREAM_IDLE, NULL); + if (!stream) { + return NGHTTP2_ERR_NOMEM; + } + } else { + rv = nghttp2_session_reprioritize_stream(session, stream, + &frame->priority.pri_spec); + if (nghttp2_is_fatal(rv)) { + return rv; + } } - rv = nghttp2_session_reprioritize_stream(session, stream, - &frame->priority.pri_spec); + rv = nghttp2_session_adjust_idle_stream(session); if (nghttp2_is_fatal(rv)) { return rv; @@ -2612,6 +2590,8 @@ static int session_after_frame_sent2(nghttp2_session *session) { nghttp2_bufs *framebufs = &aob->framebufs; nghttp2_frame *frame; nghttp2_mem *mem; + nghttp2_stream *stream; + nghttp2_data_aux_data *aux_data; mem = &session->mem; frame = &item->frame; @@ -2634,50 +2614,46 @@ static int session_after_frame_sent2(nghttp2_session *session) { active_outbound_item_reset(&session->aob, mem); return 0; - } else { - nghttp2_stream *stream; - nghttp2_data_aux_data *aux_data; + } - aux_data = &item->aux_data.data; + /* DATA frame */ - /* On EOF, we have already detached data. Please note that - application may issue nghttp2_submit_data() in - on_frame_send_callback (call from session_after_frame_sent1), - which attach data to stream. We don't want to detach it. */ - if (aux_data->eof) { - active_outbound_item_reset(aob, mem); + aux_data = &item->aux_data.data; - return 0; - } - - /* Reset no_copy here because next write may not use this. */ - aux_data->no_copy = 0; - - stream = nghttp2_session_get_stream(session, frame->hd.stream_id); - - /* If session is closed or RST_STREAM was queued, we won't send - further data. */ - if (nghttp2_session_predicate_data_send(session, stream) != 0) { - if (stream) { - rv = nghttp2_stream_detach_item(stream); - - if (nghttp2_is_fatal(rv)) { - return rv; - } - } - - active_outbound_item_reset(aob, mem); - - return 0; - } - - aob->item = NULL; - active_outbound_item_reset(&session->aob, mem); + /* On EOF, we have already detached data. Please note that + application may issue nghttp2_submit_data() in + on_frame_send_callback (call from session_after_frame_sent1), + which attach data to stream. We don't want to detach it. */ + if (aux_data->eof) { + active_outbound_item_reset(aob, mem); return 0; } - /* Unreachable */ - assert(0); + + /* Reset no_copy here because next write may not use this. */ + aux_data->no_copy = 0; + + stream = nghttp2_session_get_stream(session, frame->hd.stream_id); + + /* If session is closed or RST_STREAM was queued, we won't send + further data. */ + if (nghttp2_session_predicate_data_send(session, stream) != 0) { + if (stream) { + rv = nghttp2_stream_detach_item(stream); + + if (nghttp2_is_fatal(rv)) { + return rv; + } + } + + active_outbound_item_reset(aob, mem); + + return 0; + } + + aob->item = NULL; + active_outbound_item_reset(&session->aob, mem); + return 0; } @@ -2722,6 +2698,14 @@ static ssize_t nghttp2_session_mem_send_internal(nghttp2_session *session, aob = &session->aob; framebufs = &aob->framebufs; + /* We may have idle streams more than we expect (e.g., + nghttp2_session_change_stream_priority() or + nghttp2_session_create_idle_stream()). Adjust them here. */ + rv = nghttp2_session_adjust_idle_stream(session); + if (nghttp2_is_fatal(rv)) { + return rv; + } + *data_ptr = NULL; for (;;) { switch (aob->state) { @@ -3519,35 +3503,44 @@ int nghttp2_session_on_request_headers_received(nghttp2_session *session, return NGHTTP2_ERR_IGN_HEADER_BLOCK; } + assert(session->server); + if (!session_is_new_peer_stream_id(session, frame->hd.stream_id)) { /* The spec says if an endpoint receives a HEADERS with invalid stream ID, it MUST issue connection error with error code PROTOCOL_ERROR. But we could get trailer HEADERS after we have sent RST_STREAM to this stream and peer have not received it. Then connection error is too harsh. It means that we only use - connection error if stream ID refers idle stream. OTherwise we + connection error if stream ID refers idle stream. Therwise we just ignore HEADERS for now. */ - if (session_detect_idle_stream(session, frame->hd.stream_id)) { + if (frame->hd.stream_id == 0 || + nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "request HEADERS: invalid stream_id"); } + stream = nghttp2_session_get_stream_raw(session, frame->hd.stream_id); + if (stream && (stream->shut_flags & NGHTTP2_SHUT_RD)) { + return session_inflate_handle_invalid_connection( + session, frame, NGHTTP2_ERR_STREAM_CLOSED, "HEADERS: stream closed"); + } + return NGHTTP2_ERR_IGN_HEADER_BLOCK; } session->last_recv_stream_id = frame->hd.stream_id; - if (session->goaway_flags & NGHTTP2_GOAWAY_SENT) { - /* We just ignore stream after GOAWAY was queued */ - return NGHTTP2_ERR_IGN_HEADER_BLOCK; - } - if (session_is_incoming_concurrent_streams_max(session)) { return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "request HEADERS: max concurrent streams exceeded"); } + if (!session_allow_incoming_new_stream(session)) { + /* We just ignore stream after GOAWAY was sent */ + return NGHTTP2_ERR_IGN_HEADER_BLOCK; + } + if (frame->headers.pri_spec.stream_id == frame->hd.stream_id) { return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "request HEADERS: depend on itself"); @@ -3564,7 +3557,14 @@ int nghttp2_session_on_request_headers_received(nghttp2_session *session, if (!stream) { return NGHTTP2_ERR_NOMEM; } + + rv = nghttp2_session_adjust_closed_stream(session); + if (nghttp2_is_fatal(rv)) { + return rv; + } + session->last_proc_stream_id = session->last_recv_stream_id; + rv = session_call_on_begin_headers(session, frame); if (rv != 0) { return rv; @@ -3590,9 +3590,11 @@ int nghttp2_session_on_response_headers_received(nghttp2_session *session, If an endpoint receives additional frames for a stream that is in this state it MUST respond with a stream error (Section 5.4.2) of type STREAM_CLOSED. + + We go further, and make it connection error. */ - return session_inflate_handle_invalid_stream(session, frame, - NGHTTP2_ERR_STREAM_CLOSED); + return session_inflate_handle_invalid_connection( + session, frame, NGHTTP2_ERR_STREAM_CLOSED, "HEADERS: stream closed"); } stream->state = NGHTTP2_STREAM_OPENED; rv = session_call_on_begin_headers(session, frame); @@ -3612,9 +3614,11 @@ int nghttp2_session_on_push_response_headers_received(nghttp2_session *session, session, frame, NGHTTP2_ERR_PROTO, "push response HEADERS: stream_id == 0"); } - if (session->goaway_flags) { - /* We don't accept new stream after GOAWAY is sent or received. */ - return NGHTTP2_ERR_IGN_HEADER_BLOCK; + + if (session->server) { + return session_inflate_handle_invalid_connection( + session, frame, NGHTTP2_ERR_PROTO, + "HEADERS: no HEADERS allowed from client in reserved state"); } if (session_is_incoming_concurrent_streams_max(session)) { @@ -3622,6 +3626,12 @@ int nghttp2_session_on_push_response_headers_received(nghttp2_session *session, session, frame, NGHTTP2_ERR_PROTO, "push response HEADERS: max concurrent streams exceeded"); } + + if (!session_allow_incoming_new_stream(session)) { + /* We don't accept new stream after GOAWAY was sent. */ + return NGHTTP2_ERR_IGN_HEADER_BLOCK; + } + if (session_is_incoming_concurrent_streams_pending_max(session)) { return session_inflate_handle_invalid_stream(session, frame, NGHTTP2_ERR_REFUSED_STREAM); @@ -3647,23 +3657,17 @@ int nghttp2_session_on_headers_received(nghttp2_session *session, return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "HEADERS: stream_id == 0"); } - if (stream->state == NGHTTP2_STREAM_RESERVED) { - /* reserved. The valid push response HEADERS is processed by - nghttp2_session_on_push_response_headers_received(). This - generic HEADERS is called invalid cases for HEADERS against - reserved state. */ - return session_inflate_handle_invalid_connection( - session, frame, NGHTTP2_ERR_PROTO, "HEADERS: stream in reserved"); - } if ((stream->shut_flags & NGHTTP2_SHUT_RD)) { /* half closed (remote): from the spec: If an endpoint receives additional frames for a stream that is in this state it MUST respond with a stream error (Section 5.4.2) of type STREAM_CLOSED. + + we go further, and make it connection error. */ - return session_inflate_handle_invalid_stream(session, frame, - NGHTTP2_ERR_STREAM_CLOSED); + return session_inflate_handle_invalid_connection( + session, frame, NGHTTP2_ERR_STREAM_CLOSED, "HEADERS: stream closed"); } if (nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { if (stream->state == NGHTTP2_STREAM_OPENED) { @@ -3672,15 +3676,9 @@ int nghttp2_session_on_headers_received(nghttp2_session *session, return rv; } return 0; - } else if (stream->state == NGHTTP2_STREAM_CLOSING) { - /* This is race condition. NGHTTP2_STREAM_CLOSING indicates - that we queued RST_STREAM but it has not been sent. It will - eventually sent, so we just ignore this frame. */ - return NGHTTP2_ERR_IGN_HEADER_BLOCK; - } else { - return session_inflate_handle_invalid_stream(session, frame, - NGHTTP2_ERR_PROTO); } + + return NGHTTP2_ERR_IGN_HEADER_BLOCK; } /* If this is remote peer initiated stream, it is OK unless it has sent END_STREAM frame already. But if stream is in @@ -3715,20 +3713,18 @@ static int session_process_headers_frame(nghttp2_session *session) { return nghttp2_session_on_request_headers_received(session, frame); } - if (nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { - if (stream->state == NGHTTP2_STREAM_OPENING) { - frame->headers.cat = NGHTTP2_HCAT_RESPONSE; - return nghttp2_session_on_response_headers_received(session, frame, - stream); - } - frame->headers.cat = NGHTTP2_HCAT_HEADERS; - return nghttp2_session_on_headers_received(session, frame, stream); - } if (stream->state == NGHTTP2_STREAM_RESERVED) { frame->headers.cat = NGHTTP2_HCAT_PUSH_RESPONSE; return nghttp2_session_on_push_response_headers_received(session, frame, stream); } + + if (stream->state == NGHTTP2_STREAM_OPENING && + nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { + frame->headers.cat = NGHTTP2_HCAT_RESPONSE; + return nghttp2_session_on_response_headers_received(session, frame, stream); + } + frame->headers.cat = NGHTTP2_HCAT_HEADERS; return nghttp2_session_on_headers_received(session, frame, stream); } @@ -3769,6 +3765,11 @@ int nghttp2_session_on_priority_received(nghttp2_session *session, if (stream == NULL) { return NGHTTP2_ERR_NOMEM; } + + rv = nghttp2_session_adjust_idle_stream(session); + if (nghttp2_is_fatal(rv)) { + return rv; + } } else { rv = nghttp2_session_reprioritize_stream(session, stream, &frame->priority.pri_spec); @@ -3776,6 +3777,11 @@ int nghttp2_session_on_priority_received(nghttp2_session *session, if (nghttp2_is_fatal(rv)) { return rv; } + + rv = nghttp2_session_adjust_idle_stream(session); + if (nghttp2_is_fatal(rv)) { + return rv; + } } return session_call_on_frame_received(session, frame); @@ -3805,6 +3811,9 @@ int nghttp2_session_on_rst_stream_received(nghttp2_session *session, return session_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "RST_STREAM: stream in idle"); } + } else { + /* We may use stream->shut_flags for strict error checking. */ + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); } rv = session_call_on_frame_received(session, frame); @@ -4220,17 +4229,17 @@ int nghttp2_session_on_push_promise_received(nghttp2_session *session, return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "PUSH_PROMISE: push disabled"); } - if (session->goaway_flags) { - /* We just dicard PUSH_PROMISE after GOAWAY is sent or - received. */ - return NGHTTP2_ERR_IGN_HEADER_BLOCK; - } if (!nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "PUSH_PROMISE: invalid stream_id"); } + if (!session_allow_incoming_new_stream(session)) { + /* We just discard PUSH_PROMISE after GOAWAY was sent */ + return NGHTTP2_ERR_IGN_HEADER_BLOCK; + } + if (!session_is_new_peer_stream_id(session, frame->push_promise.promised_stream_id)) { /* The spec says if an endpoint receives a PUSH_PROMISE with @@ -4251,7 +4260,11 @@ int nghttp2_session_on_push_promise_received(nghttp2_session *session, return session_inflate_handle_invalid_connection( session, frame, NGHTTP2_ERR_PROTO, "PUSH_PROMISE: stream in idle"); } + + /* Currently, client does not retain closed stream, so we don't + check NGHTTP2_SHUT_RD condition here. */ } + rv = nghttp2_session_add_rst_stream( session, frame->push_promise.promised_stream_id, NGHTTP2_CANCEL); if (rv != 0) { @@ -4259,25 +4272,13 @@ int nghttp2_session_on_push_promise_received(nghttp2_session *session, } return NGHTTP2_ERR_IGN_HEADER_BLOCK; } + if (stream->shut_flags & NGHTTP2_SHUT_RD) { - if (session->callbacks.on_invalid_frame_recv_callback) { - if (session->callbacks.on_invalid_frame_recv_callback( - session, frame, NGHTTP2_PROTOCOL_ERROR, session->user_data) != - 0) { - return NGHTTP2_ERR_CALLBACK_FAILURE; - } - } - rv = nghttp2_session_add_rst_stream(session, - frame->push_promise.promised_stream_id, - NGHTTP2_PROTOCOL_ERROR); - if (rv != 0) { - return rv; - } - return NGHTTP2_ERR_IGN_HEADER_BLOCK; + return session_inflate_handle_invalid_connection( + session, frame, NGHTTP2_ERR_STREAM_CLOSED, + "PUSH_PROMISE: stream closed"); } - /* TODO It is unclear reserved stream dpeneds on associated - stream with or without exclusive flag set */ nghttp2_priority_spec_init(&pri_spec, stream->stream_id, NGHTTP2_DEFAULT_WEIGHT, 0); @@ -4289,6 +4290,9 @@ int nghttp2_session_on_push_promise_received(nghttp2_session *session, return NGHTTP2_ERR_NOMEM; } + /* We don't call nghttp2_session_adjust_closed_stream(), since we + don't keep closed stream in client side */ + session->last_proc_stream_id = session->last_recv_stream_id; rv = session_call_on_begin_headers(session, frame); if (rv != 0) { @@ -4392,8 +4396,9 @@ session_on_connection_window_update_received(nghttp2_session *session, nghttp2_frame *frame) { /* Handle connection-level flow control */ if (frame->window_update.window_size_increment == 0) { - return session_handle_invalid_connection(session, frame, NGHTTP2_ERR_PROTO, - NULL); + return session_handle_invalid_connection( + session, frame, NGHTTP2_ERR_PROTO, + "WINDOW_UPDATE: window_size_increment == 0"); } if (NGHTTP2_MAX_WINDOW_SIZE - frame->window_update.window_size_increment < @@ -4423,7 +4428,9 @@ static int session_on_stream_window_update_received(nghttp2_session *session, session, frame, NGHTTP2_ERR_PROTO, "WINDOW_UPADATE to reserved stream"); } if (frame->window_update.window_size_increment == 0) { - return session_handle_invalid_stream(session, frame, NGHTTP2_ERR_PROTO); + return session_handle_invalid_connection( + session, frame, NGHTTP2_ERR_PROTO, + "WINDOW_UPDATE: window_size_increment == 0"); } if (NGHTTP2_MAX_WINDOW_SIZE - frame->window_update.window_size_increment < stream->remote_window_size) { @@ -4736,6 +4743,14 @@ static int session_on_data_received_fail_fast(nghttp2_session *session) { error_code = NGHTTP2_PROTOCOL_ERROR; goto fail; } + + stream = nghttp2_session_get_stream_raw(session, stream_id); + if (stream && (stream->shut_flags & NGHTTP2_SHUT_RD)) { + failure_reason = "DATA: stream closed"; + error_code = NGHTTP2_STREAM_CLOSED; + goto fail; + } + return NGHTTP2_ERR_IGN_PAYLOAD; } if (stream->shut_flags & NGHTTP2_SHUT_RD) { @@ -4920,6 +4935,14 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, mem = &session->mem; + /* We may have idle streams more than we expect (e.g., + nghttp2_session_change_stream_priority() or + nghttp2_session_create_idle_stream()). Adjust them here. */ + rv = nghttp2_session_adjust_idle_stream(session); + if (nghttp2_is_fatal(rv)) { + return rv; + } + for (;;) { switch (iframe->state) { case NGHTTP2_IB_READ_CLIENT_MAGIC: @@ -6671,6 +6694,10 @@ static int nghttp2_session_upgrade_internal(nghttp2_session *session, if (stream == NULL) { return NGHTTP2_ERR_NOMEM; } + + /* We don't call nghttp2_session_adjust_closed_stream(), since this + should be the first stream open. */ + if (session->server) { nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); session->last_recv_stream_id = 1; @@ -6885,6 +6912,7 @@ int nghttp2_session_check_server_session(nghttp2_session *session) { int nghttp2_session_change_stream_priority( nghttp2_session *session, int32_t stream_id, const nghttp2_priority_spec *pri_spec) { + int rv; nghttp2_stream *stream; nghttp2_priority_spec pri_spec_copy; @@ -6900,7 +6928,18 @@ int nghttp2_session_change_stream_priority( pri_spec_copy = *pri_spec; nghttp2_priority_spec_normalize_weight(&pri_spec_copy); - return nghttp2_session_reprioritize_stream(session, stream, &pri_spec_copy); + rv = nghttp2_session_reprioritize_stream(session, stream, &pri_spec_copy); + + if (nghttp2_is_fatal(rv)) { + return rv; + } + + /* We don't intentionally call nghttp2_session_adjust_idle_stream() + so that idle stream created by this function, and existing ones + are kept for application. We will adjust number of idle stream + in nghttp2_session_mem_send or nghttp2_session_mem_recv is + called. */ + return 0; } int nghttp2_session_create_idle_stream(nghttp2_session *session, @@ -6929,5 +6968,10 @@ int nghttp2_session_create_idle_stream(nghttp2_session *session, return NGHTTP2_ERR_NOMEM; } + /* We don't intentionally call nghttp2_session_adjust_idle_stream() + so that idle stream created by this function, and existing ones + are kept for application. We will adjust number of idle stream + in nghttp2_session_mem_send or nghttp2_session_mem_recv is + called. */ return 0; } diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index 76b69a20..50c29f7a 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -73,6 +73,10 @@ typedef struct { /* The default maximum number of incoming reserved streams */ #define NGHTTP2_MAX_INCOMING_RESERVED_STREAMS 200 +/* Even if we have less SETTINGS_MAX_CONCURRENT_STREAMS than this + number, we keep NGHTTP2_MIN_IDLE_STREAMS streams in idle state */ +#define NGHTTP2_MIN_IDLE_STREAMS 16 + /* The maximum number of items in outbound queue, which is considered as flooding caused by peer. All frames are not considered here. We only consider PING + ACK and SETTINGS + ACK. This is because @@ -450,6 +454,11 @@ int nghttp2_session_add_settings(nghttp2_session *session, uint8_t flags, * * This function returns a pointer to created new stream object, or * NULL. + * + * This function adjusts neither the number of closed streams or idle + * streams. The caller should manually call + * nghttp2_session_adjust_closed_stream() or + * nghttp2_session_adjust_idle_stream() respectively. */ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, int32_t stream_id, uint8_t flags, @@ -498,29 +507,17 @@ int nghttp2_session_destroy_stream(nghttp2_session *session, * limitation of maximum number of streams in memory, |stream| is not * closed and just deleted from memory (see * nghttp2_session_destroy_stream). - * - * This function returns 0 if it succeeds, or one the following - * negative error codes: - * - * NGHTTP2_ERR_NOMEM - * Out of memory */ -int nghttp2_session_keep_closed_stream(nghttp2_session *session, - nghttp2_stream *stream); +void nghttp2_session_keep_closed_stream(nghttp2_session *session, + nghttp2_stream *stream); /* * Appends |stream| to linked list |session->idle_stream_head|. We * apply fixed limit for list size. To fit into that limit, one or * more oldest streams are removed from list as necessary. - * - * This function returns 0 if it succeeds, or one the following - * negative error codes: - * - * NGHTTP2_ERR_NOMEM - * Out of memory */ -int nghttp2_session_keep_idle_stream(nghttp2_session *session, - nghttp2_stream *stream); +void nghttp2_session_keep_idle_stream(nghttp2_session *session, + nghttp2_stream *stream); /* * Detaches |stream| from idle streams linked list. @@ -531,9 +528,7 @@ void nghttp2_session_detach_idle_stream(nghttp2_session *session, /* * Deletes closed stream to ensure that number of incoming streams * including active and closed is in the maximum number of allowed - * stream. If |offset| is nonzero, it is decreased from the maximum - * number of allowed stream when comparing number of active and closed - * stream and the maximum number. + * stream. * * This function returns 0 if it succeeds, or one the following * negative error codes: @@ -541,8 +536,7 @@ void nghttp2_session_detach_idle_stream(nghttp2_session *session, * NGHTTP2_ERR_NOMEM * Out of memory */ -int nghttp2_session_adjust_closed_stream(nghttp2_session *session, - size_t offset); +int nghttp2_session_adjust_closed_stream(nghttp2_session *session); /* * Deletes idle stream to ensure that number of idle streams is in @@ -814,6 +808,9 @@ int nghttp2_session_update_local_settings(nghttp2_session *session, * |pri_spec|. Caller must ensure that stream->hd.stream_id != * pri_spec->stream_id. * + * This function does not adjust the number of idle streams. The + * caller should call nghttp2_session_adjust_idle_stream() later. + * * This function returns 0 if it succeeds, or one of the following * negative error codes: * diff --git a/lib/nghttp2_stream.c b/lib/nghttp2_stream.c index 54219a4d..3e848d8e 100644 --- a/lib/nghttp2_stream.c +++ b/lib/nghttp2_stream.c @@ -80,6 +80,7 @@ void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id, stream->queued = 0; stream->descendant_last_cycle = 0; stream->cycle = 0; + stream->pending_penalty = 0; stream->descendant_next_seq = 0; stream->seq = 0; stream->last_writelen = 0; @@ -115,9 +116,14 @@ static int stream_subtree_active(nghttp2_stream *stream) { /* * Returns next cycle for |stream|. */ -static uint64_t stream_next_cycle(nghttp2_stream *stream, uint64_t last_cycle) { - return last_cycle + - stream->last_writelen * NGHTTP2_MAX_WEIGHT / (uint32_t)stream->weight; +static void stream_next_cycle(nghttp2_stream *stream, uint64_t last_cycle) { + size_t penalty; + + penalty = + stream->last_writelen * NGHTTP2_MAX_WEIGHT + stream->pending_penalty; + + stream->cycle = last_cycle + penalty / (uint32_t)stream->weight; + stream->pending_penalty = (uint32_t)(penalty % (uint32_t)stream->weight); } static int stream_obq_push(nghttp2_stream *dep_stream, nghttp2_stream *stream) { @@ -125,8 +131,7 @@ static int stream_obq_push(nghttp2_stream *dep_stream, nghttp2_stream *stream) { for (; dep_stream && !stream->queued; stream = dep_stream, dep_stream = dep_stream->dep_prev) { - stream->cycle = - stream_next_cycle(stream, dep_stream->descendant_last_cycle); + stream_next_cycle(stream, dep_stream->descendant_last_cycle); stream->seq = dep_stream->descendant_next_seq++; DEBUGF(fprintf(stderr, "stream: stream=%d obq push cycle=%ld\n", @@ -169,6 +174,7 @@ static void stream_obq_remove(nghttp2_stream *stream) { stream->queued = 0; stream->cycle = 0; + stream->pending_penalty = 0; stream->descendant_last_cycle = 0; stream->last_writelen = 0; @@ -207,25 +213,12 @@ void nghttp2_stream_reschedule(nghttp2_stream *stream) { dep_stream = stream->dep_prev; for (; dep_stream; stream = dep_stream, dep_stream = dep_stream->dep_prev) { - if (nghttp2_pq_size(&dep_stream->obq) == 1) { - dep_stream->descendant_last_cycle = 0; - stream->cycle = 0; - } else { - /* We update descendant_last_cycle here, and we don't do it when - no data is written for stream. This effectively means that - we treat these streams as if they are not scheduled at all. - This does not cause disruption in scheduling machinery. It - just makes new streams scheduled a bit early. */ - dep_stream->descendant_last_cycle = stream->cycle; + nghttp2_pq_remove(&dep_stream->obq, &stream->pq_entry); - nghttp2_pq_remove(&dep_stream->obq, &stream->pq_entry); + stream_next_cycle(stream, dep_stream->descendant_last_cycle); + stream->seq = dep_stream->descendant_next_seq++; - stream->cycle = - stream_next_cycle(stream, dep_stream->descendant_last_cycle); - stream->seq = dep_stream->descendant_next_seq++; - - nghttp2_pq_push(&dep_stream->obq, &stream->pq_entry); - } + nghttp2_pq_push(&dep_stream->obq, &stream->pq_entry); DEBUGF(fprintf(stderr, "stream: stream=%d obq resched cycle=%ld\n", stream->stream_id, stream->cycle)); @@ -234,6 +227,61 @@ void nghttp2_stream_reschedule(nghttp2_stream *stream) { } } +void nghttp2_stream_change_weight(nghttp2_stream *stream, int32_t weight) { + nghttp2_stream *dep_stream; + uint64_t last_cycle; + int32_t old_weight; + size_t wlen_penalty; + + if (stream->weight == weight) { + return; + } + + old_weight = stream->weight; + stream->weight = weight; + + dep_stream = stream->dep_prev; + + if (!dep_stream) { + return; + } + + dep_stream->sum_dep_weight += weight - old_weight; + + if (!stream->queued) { + return; + } + + nghttp2_pq_remove(&dep_stream->obq, &stream->pq_entry); + + wlen_penalty = stream->last_writelen * NGHTTP2_MAX_WEIGHT; + + /* Compute old stream->pending_penalty we used to calculate + stream->cycle */ + stream->pending_penalty = + (uint32_t)((stream->pending_penalty + (uint32_t)old_weight - + (wlen_penalty % (uint32_t)old_weight)) % + (uint32_t)old_weight); + + last_cycle = stream->cycle - + (wlen_penalty + stream->pending_penalty) / (uint32_t)old_weight; + + /* Now we have old stream->pending_penalty and new stream->weight in + place */ + stream_next_cycle(stream, last_cycle); + + if (stream->cycle < dep_stream->descendant_last_cycle) { + stream->cycle = dep_stream->descendant_last_cycle; + } + + /* Continue to use same stream->seq */ + + nghttp2_pq_push(&dep_stream->obq, &stream->pq_entry); + + DEBUGF(fprintf(stderr, "stream: stream=%d obq resched cycle=%ld\n", + stream->stream_id, stream->cycle)); +} + static nghttp2_stream *stream_last_sib(nghttp2_stream *stream) { for (; stream->sib_next; stream = stream->sib_next) ; @@ -861,9 +909,15 @@ int nghttp2_stream_in_dep_tree(nghttp2_stream *stream) { nghttp2_outbound_item * nghttp2_stream_next_outbound_item(nghttp2_stream *stream) { nghttp2_pq_entry *ent; + nghttp2_stream *si; for (;;) { if (stream_active(stream)) { + /* Update ascendant's descendant_last_cycle here, so that we can + assure that new stream is scheduled based on it. */ + for (si = stream; si->dep_prev; si = si->dep_prev) { + si->dep_prev->descendant_last_cycle = si->cycle; + } return stream->item; } ent = nghttp2_pq_top(&stream->obq); diff --git a/lib/nghttp2_stream.h b/lib/nghttp2_stream.h index e2dddefa..b5d07592 100644 --- a/lib/nghttp2_stream.h +++ b/lib/nghttp2_stream.h @@ -197,6 +197,8 @@ struct nghttp2_stream { int32_t local_window_size; /* weight of this stream */ int32_t weight; + /* This is unpaid penalty (offset) when calculating cycle. */ + uint32_t pending_penalty; /* sum of weight of direct descendants */ int32_t sum_dep_weight; nghttp2_stream_state state; @@ -419,7 +421,14 @@ int nghttp2_stream_in_dep_tree(nghttp2_stream *stream); void nghttp2_stream_reschedule(nghttp2_stream *stream); /* - * Returns a stream which has highest priority. + * Changes |stream|'s weight to |weight|. If |stream| is queued, it + * will be rescheduled based on new weight. + */ +void nghttp2_stream_change_weight(nghttp2_stream *stream, int32_t weight); + +/* + * Returns a stream which has highest priority, updating + * descendant_last_cycle of selected stream's ancestors. */ nghttp2_outbound_item * nghttp2_stream_next_outbound_item(nghttp2_stream *stream); diff --git a/lib/nghttp2_submit.c b/lib/nghttp2_submit.c index 9050a057..d6f9530d 100644 --- a/lib/nghttp2_submit.c +++ b/lib/nghttp2_submit.c @@ -40,8 +40,7 @@ static int32_t submit_headers_shared(nghttp2_session *session, uint8_t flags, const nghttp2_priority_spec *pri_spec, nghttp2_nv *nva_copy, size_t nvlen, const nghttp2_data_provider *data_prd, - void *stream_user_data, - uint8_t attach_stream) { + void *stream_user_data) { int rv; uint8_t flags_copy; nghttp2_outbound_item *item = NULL; @@ -56,6 +55,16 @@ static int32_t submit_headers_shared(nghttp2_session *session, uint8_t flags, goto fail; } + if (stream_id == -1) { + if ((int32_t)session->next_stream_id == pri_spec->stream_id) { + rv = NGHTTP2_ERR_INVALID_ARGUMENT; + goto fail; + } + } else if (stream_id == pri_spec->stream_id) { + rv = NGHTTP2_ERR_INVALID_ARGUMENT; + goto fail; + } + item = nghttp2_mem_malloc(mem, sizeof(nghttp2_outbound_item)); if (item == NULL) { rv = NGHTTP2_ERR_NOMEM; @@ -69,7 +78,6 @@ static int32_t submit_headers_shared(nghttp2_session *session, uint8_t flags, } item->aux_data.headers.stream_user_data = stream_user_data; - item->aux_data.headers.attach_stream = attach_stream; flags_copy = (uint8_t)((flags & (NGHTTP2_FLAG_END_STREAM | NGHTTP2_FLAG_PRIORITY)) | @@ -122,8 +130,7 @@ static int32_t submit_headers_shared_nva(nghttp2_session *session, const nghttp2_priority_spec *pri_spec, const nghttp2_nv *nva, size_t nvlen, const nghttp2_data_provider *data_prd, - void *stream_user_data, - uint8_t attach_stream) { + void *stream_user_data) { int rv; nghttp2_nv *nva_copy; nghttp2_priority_spec copy_pri_spec; @@ -144,15 +151,14 @@ static int32_t submit_headers_shared_nva(nghttp2_session *session, } return submit_headers_shared(session, flags, stream_id, ©_pri_spec, - nva_copy, nvlen, data_prd, stream_user_data, - attach_stream); + nva_copy, nvlen, data_prd, stream_user_data); } int nghttp2_submit_trailer(nghttp2_session *session, int32_t stream_id, const nghttp2_nv *nva, size_t nvlen) { return (int)submit_headers_shared_nva(session, NGHTTP2_FLAG_END_STREAM, - stream_id, NULL, nva, nvlen, NULL, NULL, - 0); + stream_id, NULL, nva, nvlen, NULL, + NULL); } int32_t nghttp2_submit_headers(nghttp2_session *session, uint8_t flags, @@ -169,7 +175,7 @@ int32_t nghttp2_submit_headers(nghttp2_session *session, uint8_t flags, } return submit_headers_shared_nva(session, flags, stream_id, pri_spec, nva, - nvlen, NULL, stream_user_data, 0); + nvlen, NULL, stream_user_data); } int nghttp2_submit_ping(nghttp2_session *session, uint8_t flags _U_, @@ -398,7 +404,7 @@ int32_t nghttp2_submit_request(nghttp2_session *session, flags = set_request_flags(pri_spec, data_prd); return submit_headers_shared_nva(session, flags, -1, pri_spec, nva, nvlen, - data_prd, stream_user_data, 0); + data_prd, stream_user_data); } static uint8_t set_response_flags(const nghttp2_data_provider *data_prd) { @@ -414,7 +420,7 @@ int nghttp2_submit_response(nghttp2_session *session, int32_t stream_id, const nghttp2_data_provider *data_prd) { uint8_t flags = set_response_flags(data_prd); return submit_headers_shared_nva(session, flags, stream_id, NULL, nva, nvlen, - data_prd, NULL, 1); + data_prd, NULL); } int nghttp2_submit_data(nghttp2_session *session, uint8_t flags, diff --git a/src/HttpServer.cc b/src/HttpServer.cc index 14cb0395..b1e0b0fe 100644 --- a/src/HttpServer.cc +++ b/src/HttpServer.cc @@ -53,6 +53,7 @@ #include #include +#include #include @@ -105,7 +106,7 @@ Config::Config() max_concurrent_streams(100), header_table_size(-1), port(0), verbose(false), daemon(false), verify_client(false), no_tls(false), error_gzip(false), early_response(false), hexdump(false), - echo_upload(false) {} + echo_upload(false), no_content_length(false) {} Config::~Config() {} @@ -872,12 +873,14 @@ int Http2Handler::submit_file_response(const std::string &status, std::string last_modified_str; auto nva = make_array(http2::make_nv_ls(":status", status), http2::make_nv_ll("server", NGHTTPD_SERVER), - http2::make_nv_ls("content-length", content_length), http2::make_nv_ll("cache-control", "max-age=3600"), http2::make_nv_ls("date", sessions_->get_cached_date()), http2::make_nv_ll("", ""), http2::make_nv_ll("", ""), - http2::make_nv_ll("", "")); - size_t nvlen = 5; + http2::make_nv_ll("", ""), http2::make_nv_ll("", "")); + size_t nvlen = 4; + if (!get_config()->no_content_length) { + nva[nvlen++] = http2::make_nv_ls("content-length", content_length); + } if (last_modified != 0) { last_modified_str = util::http_date(last_modified); nva[nvlen++] = http2::make_nv_ls("last-modified", last_modified_str); @@ -1087,7 +1090,9 @@ void prepare_echo_response(Stream *stream, Http2Handler *hd) { Headers headers; headers.emplace_back("nghttpd-response", "echo"); - headers.emplace_back("content-length", util::utos(length)); + if (!hd->get_config()->no_content_length) { + headers.emplace_back("content-length", util::utos(length)); + } hd->submit_response("200", stream->stream_id, headers, &data_prd); } diff --git a/src/HttpServer.h b/src/HttpServer.h index 3c1dc73e..3e889711 100644 --- a/src/HttpServer.h +++ b/src/HttpServer.h @@ -76,6 +76,7 @@ struct Config { bool early_response; bool hexdump; bool echo_upload; + bool no_content_length; Config(); ~Config(); }; diff --git a/src/asio_client_session.cc b/src/asio_client_session.cc index aaee1e77..bc835c38 100644 --- a/src/asio_client_session.cc +++ b/src/asio_client_session.cc @@ -39,12 +39,16 @@ using boost::asio::ip::tcp; session::session(boost::asio::io_service &io_service, const std::string &host, const std::string &service) - : impl_(make_unique(io_service, host, service)) {} + : impl_(std::make_shared(io_service, host, service)) { + impl_->start_resolve(host, service); +} session::session(boost::asio::io_service &io_service, boost::asio::ssl::context &tls_ctx, const std::string &host, const std::string &service) - : impl_(make_unique(io_service, tls_ctx, host, service)) { + : impl_(std::make_shared(io_service, tls_ctx, host, + service)) { + impl_->start_resolve(host, service); } session::~session() {} @@ -93,6 +97,14 @@ const request *session::submit(boost::system::error_code &ec, return impl_->submit(ec, method, uri, std::move(cb), std::move(h)); } +void session::connect_timeout(const boost::posix_time::time_duration &t) { + impl_->connect_timeout(t); +} + +void session::read_timeout(const boost::posix_time::time_duration &t) { + impl_->read_timeout(t); +} + } // namespace client } // namespace asio_http2 } // nghttp2 diff --git a/src/asio_client_session_impl.cc b/src/asio_client_session_impl.cc index 3be7b20d..a4cce25a 100644 --- a/src/asio_client_session_impl.cc +++ b/src/asio_client_session_impl.cc @@ -40,8 +40,10 @@ namespace client { session_impl::session_impl(boost::asio::io_service &io_service) : wblen_(0), io_service_(io_service), resolver_(io_service), - session_(nullptr), data_pending_(nullptr), data_pendinglen_(0), - writing_(false), inside_callback_(false) {} + deadline_(io_service), connect_timeout_(boost::posix_time::seconds(60)), + read_timeout_(boost::posix_time::seconds(60)), session_(nullptr), + data_pending_(nullptr), data_pendinglen_(0), writing_(false), + inside_callback_(false), stopped_(false) {} session_impl::~session_impl() { // finish up all active stream @@ -56,9 +58,13 @@ session_impl::~session_impl() { void session_impl::start_resolve(const std::string &host, const std::string &service) { + deadline_.expires_from_now(connect_timeout_); + + auto self = this->shared_from_this(); + resolver_.async_resolve({host, service}, - [this](const boost::system::error_code &ec, - tcp::resolver::iterator endpoint_it) { + [this, self](const boost::system::error_code &ec, + tcp::resolver::iterator endpoint_it) { if (ec) { not_connected(ec); return; @@ -66,6 +72,25 @@ void session_impl::start_resolve(const std::string &host, start_connect(endpoint_it); }); + + deadline_.async_wait(std::bind(&session_impl::handle_deadline, self)); +} + +void session_impl::handle_deadline() { + if (stopped_) { + return; + } + + if (deadline_.expires_at() <= + boost::asio::deadline_timer::traits_type::now()) { + call_error_cb(boost::asio::error::timed_out); + stop(); + deadline_.expires_at(boost::posix_time::pos_infin); + return; + } + + deadline_.async_wait( + std::bind(&session_impl::handle_deadline, this->shared_from_this())); } void session_impl::connected(tcp::resolver::iterator endpoint_it) { @@ -86,6 +111,7 @@ void session_impl::connected(tcp::resolver::iterator endpoint_it) { void session_impl::not_connected(const boost::system::error_code &ec) { call_error_cb(ec); + stop(); } void session_impl::on_connect(connect_cb cb) { connect_cb_ = std::move(cb); } @@ -97,6 +123,9 @@ const connect_cb &session_impl::on_connect() const { return connect_cb_; } const error_cb &session_impl::on_error() const { return error_cb_; } void session_impl::call_error_cb(const boost::system::error_code &ec) { + if (stopped_) { + return; + } auto &error_cb = on_error(); if (!error_cb) { return; @@ -350,12 +379,20 @@ int session_impl::write_trailer(stream &strm, header_map h) { } void session_impl::cancel(stream &strm, uint32_t error_code) { + if (stopped_) { + return; + } + nghttp2_submit_rst_stream(session_, NGHTTP2_FLAG_NONE, strm.stream_id(), error_code); signal_write(); } void session_impl::resume(stream &strm) { + if (stopped_) { + return; + } + nghttp2_session_resume_data(session_, strm.stream_id()); signal_write(); } @@ -396,6 +433,11 @@ const request *session_impl::submit(boost::system::error_code &ec, header_map h) { ec.clear(); + if (stopped_) { + ec = make_error_code(static_cast(NGHTTP2_INTERNAL_ERROR)); + return nullptr; + } + http_parser_url u{}; // TODO Handle CONNECT method if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { @@ -485,6 +527,10 @@ const request *session_impl::submit(boost::system::error_code &ec, } void session_impl::shutdown() { + if (stopped_) { + return; + } + nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); signal_write(); } @@ -522,13 +568,21 @@ void session_impl::leave_callback() { } void session_impl::do_read() { - read_socket([this](const boost::system::error_code &ec, - std::size_t bytes_transferred) { + if (stopped_) { + return; + } + + deadline_.expires_from_now(read_timeout_); + + auto self = this->shared_from_this(); + + read_socket([this, self](const boost::system::error_code &ec, + std::size_t bytes_transferred) { if (ec) { if (!should_stop()) { call_error_cb(ec); - shutdown_socket(); } + stop(); return; } @@ -541,7 +595,7 @@ void session_impl::do_read() { if (rv != static_cast(bytes_transferred)) { call_error_cb(make_error_code( static_cast(rv < 0 ? rv : NGHTTP2_ERR_PROTO))); - shutdown_socket(); + stop(); return; } } @@ -549,7 +603,7 @@ void session_impl::do_read() { do_write(); if (should_stop()) { - shutdown_socket(); + stop(); return; } @@ -558,6 +612,10 @@ void session_impl::do_read() { } void session_impl::do_write() { + if (stopped_) { + return; + } + if (writing_) { return; } @@ -579,7 +637,7 @@ void session_impl::do_write() { auto n = nghttp2_session_mem_send(session_, &data); if (n < 0) { call_error_cb(make_error_code(static_cast(n))); - shutdown_socket(); + stop(); return; } @@ -601,23 +659,51 @@ void session_impl::do_write() { } if (wblen_ == 0) { + if (should_stop()) { + stop(); + } return; } writing_ = true; - write_socket([this](const boost::system::error_code &ec, std::size_t n) { - if (ec) { - call_error_cb(ec); - shutdown_socket(); - return; - } + // Reset read deadline here, because normally client is sending + // something, it does not expect timeout while doing it. + deadline_.expires_from_now(read_timeout_); - wblen_ = 0; - writing_ = false; + auto self = this->shared_from_this(); - do_write(); - }); + write_socket( + [this, self](const boost::system::error_code &ec, std::size_t n) { + if (ec) { + call_error_cb(ec); + stop(); + return; + } + + wblen_ = 0; + writing_ = false; + + do_write(); + }); +} + +void session_impl::stop() { + if (stopped_) { + return; + } + + shutdown_socket(); + deadline_.cancel(); + stopped_ = true; +} + +void session_impl::connect_timeout(const boost::posix_time::time_duration &t) { + connect_timeout_ = t; +} + +void session_impl::read_timeout(const boost::posix_time::time_duration &t) { + read_timeout_ = t; } } // namespace client diff --git a/src/asio_client_session_impl.h b/src/asio_client_session_impl.h index 37032d68..5e936c37 100644 --- a/src/asio_client_session_impl.h +++ b/src/asio_client_session_impl.h @@ -41,7 +41,7 @@ class stream; using boost::asio::ip::tcp; -class session_impl { +class session_impl : public std::enable_shared_from_this { public: session_impl(boost::asio::io_service &io_service); virtual ~session_impl(); @@ -91,6 +91,11 @@ public: void do_read(); void do_write(); + void connect_timeout(const boost::posix_time::time_duration &t); + void read_timeout(const boost::posix_time::time_duration &t); + + void stop(); + protected: boost::array rb_; boost::array wb_; @@ -100,6 +105,7 @@ private: bool should_stop() const; bool setup_session(); void call_error_cb(const boost::system::error_code &ec); + void handle_deadline(); boost::asio::io_service &io_service_; tcp::resolver resolver_; @@ -109,6 +115,10 @@ private: connect_cb connect_cb_; error_cb error_cb_; + boost::asio::deadline_timer deadline_; + boost::posix_time::time_duration connect_timeout_; + boost::posix_time::time_duration read_timeout_; + nghttp2_session *session_; const uint8_t *data_pending_; @@ -116,6 +126,7 @@ private: bool writing_; bool inside_callback_; + bool stopped_; }; } // namespace client diff --git a/src/asio_client_session_tcp_impl.cc b/src/asio_client_session_tcp_impl.cc index c20a2303..478f1c16 100644 --- a/src/asio_client_session_tcp_impl.cc +++ b/src/asio_client_session_tcp_impl.cc @@ -31,9 +31,7 @@ namespace client { session_tcp_impl::session_tcp_impl(boost::asio::io_service &io_service, const std::string &host, const std::string &service) - : session_impl(io_service), socket_(io_service) { - start_resolve(host, service); -} + : session_impl(io_service), socket_(io_service) {} session_tcp_impl::~session_tcp_impl() {} @@ -62,7 +60,10 @@ void session_tcp_impl::write_socket( boost::asio::async_write(socket_, boost::asio::buffer(wb_, wblen_), h); } -void session_tcp_impl::shutdown_socket() { socket_.close(); } +void session_tcp_impl::shutdown_socket() { + boost::system::error_code ignored_ec; + socket_.close(ignored_ec); +} } // namespace client } // namespace asio_http2 diff --git a/src/asio_client_session_tls_impl.cc b/src/asio_client_session_tls_impl.cc index 8dc32d57..486c52d7 100644 --- a/src/asio_client_session_tls_impl.cc +++ b/src/asio_client_session_tls_impl.cc @@ -38,8 +38,6 @@ session_tls_impl::session_tls_impl(boost::asio::io_service &io_service, // ssl::context::set_verify_mode(boost::asio::ssl::verify_peer) is // not used, which is what we want. socket_.set_verify_callback(boost::asio::ssl::rfc2818_verification(host)); - - start_resolve(host, service); } session_tls_impl::~session_tls_impl() {} @@ -85,7 +83,8 @@ void session_tls_impl::write_socket( } void session_tls_impl::shutdown_socket() { - socket_.async_shutdown([](const boost::system::error_code &ec) {}); + boost::system::error_code ignored_ec; + socket_.lowest_layer().close(ignored_ec); } } // namespace client diff --git a/src/asio_io_service_pool.cc b/src/asio_io_service_pool.cc index ba405412..b35a8c82 100644 --- a/src/asio_io_service_pool.cc +++ b/src/asio_io_service_pool.cc @@ -92,6 +92,11 @@ boost::asio::io_service &io_service_pool::get_io_service() { return io_service; } +const std::vector> & +io_service_pool::io_services() const { + return io_services_; +} + } // namespace asio_http2 } // namespace nghttp2 diff --git a/src/asio_io_service_pool.h b/src/asio_io_service_pool.h index 242cac8f..c9ce1de4 100644 --- a/src/asio_io_service_pool.h +++ b/src/asio_io_service_pool.h @@ -70,6 +70,10 @@ public: /// Get an io_service to use. boost::asio::io_service &get_io_service(); + /// Get access to all io_service objects. + const std::vector> & + io_services() const; + private: /// The pool of io_services. std::vector> io_services_; diff --git a/src/asio_server.cc b/src/asio_server.cc index 531677f3..ef5becb2 100644 --- a/src/asio_server.cc +++ b/src/asio_server.cc @@ -44,8 +44,12 @@ namespace nghttp2 { namespace asio_http2 { namespace server { -server::server(std::size_t io_service_pool_size) - : io_service_pool_(io_service_pool_size) {} +server::server(std::size_t io_service_pool_size, + const boost::posix_time::time_duration &tls_handshake_timeout, + const boost::posix_time::time_duration &read_timeout) + : io_service_pool_(io_service_pool_size), + tls_handshake_timeout_(tls_handshake_timeout), + read_timeout_(read_timeout) {} boost::system::error_code server::listen_and_serve(boost::system::error_code &ec, @@ -121,7 +125,8 @@ boost::system::error_code server::bind_and_listen(boost::system::error_code &ec, void server::start_accept(boost::asio::ssl::context &tls_context, tcp::acceptor &acceptor, serve_mux &mux) { auto new_connection = std::make_shared>( - mux, io_service_pool_.get_io_service(), tls_context); + mux, tls_handshake_timeout_, read_timeout_, + io_service_pool_.get_io_service(), tls_context); acceptor.async_accept( new_connection->socket().lowest_layer(), @@ -130,14 +135,17 @@ void server::start_accept(boost::asio::ssl::context &tls_context, if (!e) { new_connection->socket().lowest_layer().set_option( tcp::no_delay(true)); + new_connection->start_tls_handshake_deadline(); new_connection->socket().async_handshake( boost::asio::ssl::stream_base::server, [new_connection](const boost::system::error_code &e) { if (e) { + new_connection->stop(); return; } if (!tls_h2_negotiated(new_connection->socket())) { + new_connection->stop(); return; } @@ -151,13 +159,15 @@ void server::start_accept(boost::asio::ssl::context &tls_context, void server::start_accept(tcp::acceptor &acceptor, serve_mux &mux) { auto new_connection = std::make_shared>( - mux, io_service_pool_.get_io_service()); + mux, tls_handshake_timeout_, read_timeout_, + io_service_pool_.get_io_service()); acceptor.async_accept( new_connection->socket(), [this, &acceptor, &mux, new_connection]( const boost::system::error_code &e) { if (!e) { new_connection->socket().set_option(tcp::no_delay(true)); + new_connection->start_read_deadline(); new_connection->start(); } @@ -169,6 +179,11 @@ void server::stop() { io_service_pool_.stop(); } void server::join() { io_service_pool_.join(); } +const std::vector> & +server::io_services() const { + return io_service_pool_.io_services(); +} + } // namespace server } // namespace asio_http2 } // namespace nghttp2 diff --git a/src/asio_server.h b/src/asio_server.h index 7f601201..ba840348 100644 --- a/src/asio_server.h +++ b/src/asio_server.h @@ -63,7 +63,9 @@ using ssl_socket = boost::asio::ssl::stream; class server : private boost::noncopyable { public: - explicit server(std::size_t io_service_pool_size); + explicit server(std::size_t io_service_pool_size, + const boost::posix_time::time_duration &tls_handshake_timeout, + const boost::posix_time::time_duration &read_timeout); boost::system::error_code listen_and_serve(boost::system::error_code &ec, @@ -73,6 +75,10 @@ public: void join(); void stop(); + /// Get access to all io_service objects. + const std::vector> & + io_services() const; + private: /// Initiate an asynchronous accept operation. void start_accept(tcp::acceptor &acceptor, serve_mux &mux); @@ -94,6 +100,9 @@ private: std::vector acceptors_; std::unique_ptr ssl_ctx_; + + boost::posix_time::time_duration tls_handshake_timeout_; + boost::posix_time::time_duration read_timeout_; }; } // namespace server diff --git a/src/asio_server_connection.h b/src/asio_server_connection.h index 3aba7336..4cab44b4 100644 --- a/src/asio_server_connection.h +++ b/src/asio_server_connection.h @@ -64,15 +64,23 @@ class connection : public std::enable_shared_from_this>, public: /// Construct a connection with the given io_service. template - explicit connection(serve_mux &mux, SocketArgs &&... args) - : socket_(std::forward(args)...), mux_(mux), writing_(false) { - } + explicit connection( + serve_mux &mux, + const boost::posix_time::time_duration &tls_handshake_timeout, + const boost::posix_time::time_duration &read_timeout, + SocketArgs &&... args) + : socket_(std::forward(args)...), mux_(mux), + deadline_(socket_.get_io_service()), + tls_handshake_timeout_(tls_handshake_timeout), + read_timeout_(read_timeout), writing_(false), stopped_(false) {} /// Start the first asynchronous operation for the connection. void start() { - handler_ = std::make_shared(socket_.get_io_service(), - [this]() { do_write(); }, mux_); + handler_ = std::make_shared( + socket_.get_io_service(), socket_.lowest_layer().remote_endpoint(), + [this]() { do_write(); }, mux_); if (handler_->start() != 0) { + stop(); return; } do_read(); @@ -80,27 +88,62 @@ public: socket_type &socket() { return socket_; } + void start_tls_handshake_deadline() { + deadline_.expires_from_now(tls_handshake_timeout_); + deadline_.async_wait( + std::bind(&connection::handle_deadline, this->shared_from_this())); + } + + void start_read_deadline() { + deadline_.expires_from_now(read_timeout_); + deadline_.async_wait( + std::bind(&connection::handle_deadline, this->shared_from_this())); + } + + void handle_deadline() { + if (stopped_) { + return; + } + + if (deadline_.expires_at() <= + boost::asio::deadline_timer::traits_type::now()) { + stop(); + deadline_.expires_at(boost::posix_time::pos_infin); + return; + } + + deadline_.async_wait( + std::bind(&connection::handle_deadline, this->shared_from_this())); + } + void do_read() { auto self = this->shared_from_this(); + deadline_.expires_from_now(read_timeout_); + socket_.async_read_some( boost::asio::buffer(buffer_), [this, self](const boost::system::error_code &e, std::size_t bytes_transferred) { - if (!e) { - if (handler_->on_read(buffer_, bytes_transferred) != 0) { - return; - } - - do_write(); - - if (!writing_ && handler_->should_stop()) { - return; - } - - do_read(); + if (e) { + stop(); + return; } + if (handler_->on_read(buffer_, bytes_transferred) != 0) { + stop(); + return; + } + + do_write(); + + if (!writing_ && handler_->should_stop()) { + stop(); + return; + } + + do_read(); + // If an error occurs then no new asynchronous operations are // started. This means that all shared_ptr references to the // connection object will disappear and the object will be @@ -122,23 +165,34 @@ public: rv = handler_->on_write(outbuf_, nwrite); if (rv != 0) { + stop(); return; } if (nwrite == 0) { + if (handler_->should_stop()) { + stop(); + } return; } writing_ = true; + // Reset read deadline here, because normally client is sending + // something, it does not expect timeout while doing it. + deadline_.expires_from_now(read_timeout_); + boost::asio::async_write( socket_, boost::asio::buffer(outbuf_, nwrite), [this, self](const boost::system::error_code &e, std::size_t) { - if (!e) { - writing_ = false; - - do_write(); + if (e) { + stop(); + return; } + + writing_ = false; + + do_write(); }); // No new asynchronous operations are started. This means that all @@ -147,6 +201,17 @@ public: // returns. The connection class's destructor closes the socket. } + void stop() { + if (stopped_) { + return; + } + + stopped_ = true; + boost::system::error_code ignored_ec; + socket_.lowest_layer().close(ignored_ec); + deadline_.cancel(); + } + private: socket_type socket_; @@ -159,7 +224,12 @@ private: boost::array outbuf_; + boost::asio::deadline_timer deadline_; + boost::posix_time::time_duration tls_handshake_timeout_; + boost::posix_time::time_duration read_timeout_; + bool writing_; + bool stopped_; }; } // namespace server diff --git a/src/asio_server_http2.cc b/src/asio_server_http2.cc index 7d162e34..997fd402 100644 --- a/src/asio_server_http2.cc +++ b/src/asio_server_http2.cc @@ -69,6 +69,14 @@ void http2::num_threads(size_t num_threads) { impl_->num_threads(num_threads); } void http2::backlog(int backlog) { impl_->backlog(backlog); } +void http2::tls_handshake_timeout(const boost::posix_time::time_duration &t) { + impl_->tls_handshake_timeout(t); +} + +void http2::read_timeout(const boost::posix_time::time_duration &t) { + impl_->read_timeout(t); +} + bool http2::handle(std::string pattern, request_cb cb) { return impl_->handle(std::move(pattern), std::move(cb)); } @@ -77,6 +85,11 @@ void http2::stop() { impl_->stop(); } void http2::join() { return impl_->join(); } +const std::vector> & +http2::io_services() const { + return impl_->io_services(); +} + } // namespace server } // namespace asio_http2 diff --git a/src/asio_server_http2_handler.cc b/src/asio_server_http2_handler.cc index 47cd18c5..48e0bd14 100644 --- a/src/asio_server_http2_handler.cc +++ b/src/asio_server_http2_handler.cc @@ -136,6 +136,9 @@ int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, break; } + auto &req = strm->request().impl(); + req.remote_endpoint(handler->remote_endpoint()); + handler->call_on_request(*strm); if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { @@ -225,8 +228,9 @@ int on_frame_not_send_callback(nghttp2_session *session, } // namespace http2_handler::http2_handler(boost::asio::io_service &io_service, + boost::asio::ip::tcp::endpoint ep, connection_write writefun, serve_mux &mux) - : writefun_(writefun), mux_(mux), io_service_(io_service), + : writefun_(writefun), mux_(mux), io_service_(io_service), remote_ep_(ep), session_(nullptr), buf_(nullptr), buflen_(0), inside_callback_(false), tstamp_cached_(time(nullptr)), formatted_date_(util::http_date(tstamp_cached_)) {} @@ -449,6 +453,10 @@ response *http2_handler::push_promise(boost::system::error_code &ec, boost::asio::io_service &http2_handler::io_service() { return io_service_; } +const boost::asio::ip::tcp::endpoint &http2_handler::remote_endpoint() { + return remote_ep_; +} + callback_guard::callback_guard(http2_handler &h) : handler(h) { handler.enter_callback(); } diff --git a/src/asio_server_http2_handler.h b/src/asio_server_http2_handler.h index 068d4527..a907e78f 100644 --- a/src/asio_server_http2_handler.h +++ b/src/asio_server_http2_handler.h @@ -53,7 +53,8 @@ using connection_write = std::function; class http2_handler : public std::enable_shared_from_this { public: - http2_handler(boost::asio::io_service &io_service, connection_write writefun, + http2_handler(boost::asio::io_service &io_service, + boost::asio::ip::tcp::endpoint ep, connection_write writefun, serve_mux &mux); ~http2_handler(); @@ -89,6 +90,8 @@ public: boost::asio::io_service &io_service(); + const boost::asio::ip::tcp::endpoint &remote_endpoint(); + const std::string &http_date(); template @@ -152,6 +155,7 @@ private: connection_write writefun_; serve_mux &mux_; boost::asio::io_service &io_service_; + boost::asio::ip::tcp::endpoint remote_ep_; nghttp2_session *session_; const uint8_t *buf_; std::size_t buflen_; diff --git a/src/asio_server_http2_impl.cc b/src/asio_server_http2_impl.cc index 79994a83..6a047f66 100644 --- a/src/asio_server_http2_impl.cc +++ b/src/asio_server_http2_impl.cc @@ -37,12 +37,16 @@ namespace asio_http2 { namespace server { -http2_impl::http2_impl() : num_threads_(1), backlog_(-1) {} +http2_impl::http2_impl() + : num_threads_(1), backlog_(-1), + tls_handshake_timeout_(boost::posix_time::seconds(60)), + read_timeout_(boost::posix_time::seconds(60)) {} boost::system::error_code http2_impl::listen_and_serve( boost::system::error_code &ec, boost::asio::ssl::context *tls_context, const std::string &address, const std::string &port, bool asynchronous) { - server_.reset(new server(num_threads_)); + server_.reset( + new server(num_threads_, tls_handshake_timeout_, read_timeout_)); return server_->listen_and_serve(ec, tls_context, address, port, backlog_, mux_, asynchronous); } @@ -51,6 +55,15 @@ void http2_impl::num_threads(size_t num_threads) { num_threads_ = num_threads; } void http2_impl::backlog(int backlog) { backlog_ = backlog; } +void http2_impl::tls_handshake_timeout( + const boost::posix_time::time_duration &t) { + tls_handshake_timeout_ = t; +} + +void http2_impl::read_timeout(const boost::posix_time::time_duration &t) { + read_timeout_ = t; +} + bool http2_impl::handle(std::string pattern, request_cb cb) { return mux_.handle(std::move(pattern), std::move(cb)); } @@ -59,6 +72,11 @@ void http2_impl::stop() { return server_->stop(); } void http2_impl::join() { return server_->join(); } +const std::vector> & +http2_impl::io_services() const { + return server_->io_services(); +} + } // namespace server } // namespace asio_http2 diff --git a/src/asio_server_http2_impl.h b/src/asio_server_http2_impl.h index e4068694..b55b68c5 100644 --- a/src/asio_server_http2_impl.h +++ b/src/asio_server_http2_impl.h @@ -47,15 +47,21 @@ public: const std::string &address, const std::string &port, bool asynchronous); void num_threads(size_t num_threads); void backlog(int backlog); + void tls_handshake_timeout(const boost::posix_time::time_duration &t); + void read_timeout(const boost::posix_time::time_duration &t); bool handle(std::string pattern, request_cb cb); void stop(); void join(); + const std::vector> & + io_services() const; private: std::unique_ptr server_; std::size_t num_threads_; int backlog_; serve_mux mux_; + boost::posix_time::time_duration tls_handshake_timeout_; + boost::posix_time::time_duration read_timeout_; }; } // namespace server diff --git a/src/asio_server_request.cc b/src/asio_server_request.cc index 0241c489..9612363b 100644 --- a/src/asio_server_request.cc +++ b/src/asio_server_request.cc @@ -50,6 +50,10 @@ void request::on_data(data_cb cb) const { request_impl &request::impl() const { return *impl_; } +const boost::asio::ip::tcp::endpoint &request::remote_endpoint() const { + return impl_->remote_endpoint(); +} + } // namespace server } // namespace asio_http2 } // namespace nghttp2 diff --git a/src/asio_server_request_impl.cc b/src/asio_server_request_impl.cc index 3a78208f..64866fa2 100644 --- a/src/asio_server_request_impl.cc +++ b/src/asio_server_request_impl.cc @@ -54,6 +54,14 @@ void request_impl::call_on_data(const uint8_t *data, std::size_t len) { } } +const boost::asio::ip::tcp::endpoint &request_impl::remote_endpoint() const { + return remote_ep_; +} + +void request_impl::remote_endpoint(boost::asio::ip::tcp::endpoint ep) { + remote_ep_ = std::move(ep); +} + } // namespace server } // namespace asio_http2 } // namespace nghttp2 diff --git a/src/asio_server_request_impl.h b/src/asio_server_request_impl.h index 8d4b0149..b4a37ff1 100644 --- a/src/asio_server_request_impl.h +++ b/src/asio_server_request_impl.h @@ -28,6 +28,7 @@ #include "nghttp2_config.h" #include +#include namespace nghttp2 { namespace asio_http2 { @@ -54,12 +55,16 @@ public: void stream(class stream *s); void call_on_data(const uint8_t *data, std::size_t len); + const boost::asio::ip::tcp::endpoint &remote_endpoint() const; + void remote_endpoint(boost::asio::ip::tcp::endpoint ep); + private: class stream *strm_; header_map header_; std::string method_; uri_ref uri_; data_cb on_data_cb_; + boost::asio::ip::tcp::endpoint remote_ep_; }; } // namespace server diff --git a/src/h2load.cc b/src/h2load.cc index 3f642eb6..e04700c6 100644 --- a/src/h2load.cc +++ b/src/h2load.cc @@ -44,6 +44,7 @@ #include #include #include +#include #ifdef HAVE_SPDYLAY #include @@ -96,15 +97,53 @@ bool Config::is_rate_mode() const { return (this->rate != 0); } bool Config::has_base_uri() const { return (!this->base_uri.empty()); } Config config; -RequestStat::RequestStat() : data_offset(0), completed(false) {} +namespace { +constexpr size_t MAX_SAMPLES = 1000000; +} // namespace Stats::Stats(size_t req_todo, size_t nclients) - : req_todo(0), req_started(0), req_done(0), req_success(0), + : req_todo(req_todo), req_started(0), req_done(0), req_success(0), req_status_success(0), req_failed(0), req_error(0), req_timedout(0), bytes_total(0), bytes_head(0), bytes_head_decomp(0), bytes_body(0), - status(), req_stats(req_todo), client_stats(nclients) {} + status() {} -Stream::Stream() : status_success(-1) {} +Stream::Stream() : req_stat{}, status_success(-1) {} + +namespace { +std::random_device rd; +} // namespace + +namespace { +std::mt19937 gen(rd()); +} // namespace + +namespace { +void sampling_init(Sampling &smp, size_t total, size_t max_samples) { + smp.n = 0; + + if (total <= max_samples) { + smp.interval = 0.; + smp.point = 0.; + return; + } + + smp.interval = static_cast(total) / max_samples; + + std::uniform_real_distribution<> dis(0., smp.interval); + + smp.point = dis(gen); +} +} // namespace + +namespace { +bool sampling_should_pick(Sampling &smp) { + return smp.interval == 0. || smp.n == ceil(smp.point); +} +} // namespace + +namespace { +void sampling_advance_point(Sampling &smp) { smp.point += smp.interval; } +} // namespace namespace { void writecb(struct ev_loop *loop, ev_io *w, int revents) { @@ -118,12 +157,14 @@ void writecb(struct ev_loop *loop, ev_io *w, int revents) { rv = client->connect(); if (rv != 0) { client->fail(); + delete client; return; } return; } if (rv != 0) { client->fail(); + delete client; } } } // namespace @@ -134,6 +175,7 @@ void readcb(struct ev_loop *loop, ev_io *w, int revents) { client->restart_timeout(); if (client->do_read() != 0) { client->fail(); + delete client; return; } writecb(loop, &client->wev, revents); @@ -155,14 +197,18 @@ void rate_period_timeout_w_cb(struct ev_loop *loop, ev_timer *w, int revents) { ++req_todo; --worker->nreqs_rem; } - worker->clients.push_back( - make_unique(worker->next_client_id++, worker, req_todo)); - auto &client = worker->clients.back(); + auto client = + make_unique(worker->next_client_id++, worker, req_todo); + + ++worker->nconns_made; + if (client->connect() != 0) { std::cerr << "client could not connect to host" << std::endl; client->fail(); + } else { + client.release(); } - ++worker->nconns_made; + worker->report_rate_progress(); } if (worker->nconns_made >= worker->nclients) { ev_timer_stop(worker->loop, w); @@ -240,7 +286,7 @@ void client_request_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { } // namespace Client::Client(uint32_t id, Worker *worker, size_t req_todo) - : worker(worker), ssl(nullptr), next_addr(config.addrs), + : cstat{}, worker(worker), ssl(nullptr), next_addr(config.addrs), current_addr(nullptr), reqidx(0), state(CLIENT_IDLE), req_todo(req_todo), req_started(0), req_done(0), id(id), fd(-1), new_connection_requested(false) { @@ -268,6 +314,12 @@ Client::~Client() { if (ssl) { SSL_free(ssl); } + + if (sampling_should_pick(worker->client_smp)) { + sampling_advance_point(worker->client_smp); + worker->sample_client_stat(&cstat); + } + ++worker->client_smp.n; } int Client::do_read() { return readfn(*this); } @@ -420,9 +472,8 @@ void Client::disconnect() { } int Client::submit_request() { - auto req_stat = &worker->stats.req_stats[worker->stats.req_started++]; - - if (session->submit_request(req_stat) != 0) { + ++worker->stats.req_started; + if (session->submit_request() != 0) { return -1; } @@ -475,15 +526,6 @@ void Client::process_request_failure() { } } -void Client::report_progress() { - if (!worker->config->is_rate_mode() && worker->id == 0 && - worker->stats.req_done % worker->progress_interval == 0) { - std::cout << "progress: " - << worker->stats.req_done * 100 / worker->stats.req_todo - << "% done" << std::endl; - } -} - namespace { void print_server_tmp_key(SSL *ssl) { // libressl does not have SSL_get_server_tmp_key @@ -607,28 +649,40 @@ void Client::on_status_code(int32_t stream_id, uint16_t status) { } } -void Client::on_stream_close(int32_t stream_id, bool success, - RequestStat *req_stat, bool final) { +void Client::on_stream_close(int32_t stream_id, bool success, bool final) { + auto req_stat = get_req_stat(stream_id); + if (!req_stat) { + return; + } + req_stat->stream_close_time = std::chrono::steady_clock::now(); if (success) { req_stat->completed = true; ++worker->stats.req_success; - auto &cstat = worker->stats.client_stats[id]; ++cstat.req_success; - } - ++worker->stats.req_done; - ++req_done; - if (success) { + if (streams[stream_id].status_success == 1) { ++worker->stats.req_status_success; } else { ++worker->stats.req_failed; } + + if (sampling_should_pick(worker->request_times_smp)) { + sampling_advance_point(worker->request_times_smp); + worker->sample_req_stat(req_stat); + } + + // Count up in successful cases only + ++worker->request_times_smp.n; } else { ++worker->stats.req_failed; ++worker->stats.req_error; } - report_progress(); + + ++worker->stats.req_done; + ++req_done; + + worker->report_progress(); streams.erase(stream_id); if (req_done == req_todo) { terminate_session(); @@ -645,6 +699,15 @@ void Client::on_stream_close(int32_t stream_id, bool success, } } +RequestStat *Client::get_req_stat(int32_t stream_id) { + auto it = streams.find(stream_id); + if (it == std::end(streams)) { + return nullptr; + } + + return &(*it).second.req_stat; +} + int Client::connection_made() { if (ssl) { report_tls_info(); @@ -750,7 +813,6 @@ int Client::connection_made() { if (!config.timing_script) { auto nreq = std::min(req_todo - req_started, (size_t)config.max_concurrent_streams); - for (; nreq > 0; --nreq) { if (submit_request() != 0) { process_request_failure(); @@ -985,17 +1047,14 @@ void Client::record_request_time(RequestStat *req_stat) { } void Client::record_connect_start_time() { - auto &cstat = worker->stats.client_stats[id]; cstat.connect_start_time = std::chrono::steady_clock::now(); } void Client::record_connect_time() { - auto &cstat = worker->stats.client_stats[id]; cstat.connect_time = std::chrono::steady_clock::now(); } void Client::record_ttfb() { - auto &cstat = worker->stats.client_stats[id]; if (recorded(cstat.ttfb)) { return; } @@ -1004,16 +1063,12 @@ void Client::record_ttfb() { } void Client::clear_connect_times() { - auto &cstat = worker->stats.client_stats[id]; - cstat.connect_start_time = std::chrono::steady_clock::time_point(); cstat.connect_time = std::chrono::steady_clock::time_point(); cstat.ttfb = std::chrono::steady_clock::time_point(); } void Client::record_client_start_time() { - auto &cstat = worker->stats.client_stats[id]; - // Record start time only once at the very first connection is going // to be made. if (recorded(cstat.client_start_time)) { @@ -1024,8 +1079,6 @@ void Client::record_client_start_time() { } void Client::record_client_end_time() { - auto &cstat = worker->stats.client_stats[id]; - // Unlike client_start_time, we overwrite client_end_time. This // handles multiple connect/disconnect for HTTP/1.1 benchmark. cstat.client_end_time = std::chrono::steady_clock::now(); @@ -1036,20 +1089,36 @@ void Client::signal_write() { ev_io_start(worker->loop, &wev); } void Client::try_new_connection() { new_connection_requested = true; } Worker::Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t req_todo, size_t nclients, - size_t rate, Config *config) + size_t rate, size_t max_samples, Config *config) : stats(req_todo, nclients), loop(ev_loop_new(0)), ssl_ctx(ssl_ctx), config(config), id(id), tls_info_report_done(false), app_info_report_done(false), nconns_made(0), nclients(nclients), nreqs_per_client(req_todo / nclients), nreqs_rem(req_todo % nclients), - rate(rate), next_client_id(0) { - stats.req_todo = req_todo; - progress_interval = std::max(static_cast(1), req_todo / 10); + rate(rate), max_samples(max_samples), next_client_id(0) { + if (!config->is_rate_mode()) { + progress_interval = std::max(static_cast(1), req_todo / 10); + } else { + progress_interval = std::max(static_cast(1), nclients / 10); + } // create timer that will go off every rate_period ev_timer_init(&timeout_watcher, rate_period_timeout_w_cb, 0., config->rate_period); timeout_watcher.data = this; + stats.req_stats.reserve(std::min(req_todo, max_samples)); + stats.client_stats.reserve(std::min(nclients, max_samples)); + + sampling_init(request_times_smp, req_todo, max_samples); + sampling_init(client_smp, nclients, max_samples); +} + +Worker::~Worker() { + ev_timer_stop(loop, &timeout_watcher); + ev_loop_destroy(loop); +} + +void Worker::run() { if (!config->is_rate_mode()) { for (size_t i = 0; i < nclients; ++i) { auto req_todo = nreqs_per_client; @@ -1057,26 +1126,12 @@ Worker::Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t req_todo, size_t nclients, ++req_todo; --nreqs_rem; } - clients.push_back(make_unique(next_client_id++, this, req_todo)); - } - } -} - -Worker::~Worker() { - ev_timer_stop(loop, &timeout_watcher); - - // first clear clients so that io watchers are stopped before - // destructing ev_loop. - clients.clear(); - ev_loop_destroy(loop); -} - -void Worker::run() { - if (!config->is_rate_mode()) { - for (auto &client : clients) { + auto client = make_unique(next_client_id++, this, req_todo); if (client->connect() != 0) { std::cerr << "client could not connect to host" << std::endl; client->fail(); + } else { + client.release(); } } } else { @@ -1088,6 +1143,34 @@ void Worker::run() { ev_run(loop, 0); } +void Worker::sample_req_stat(RequestStat *req_stat) { + stats.req_stats.push_back(*req_stat); + assert(stats.req_stats.size() <= max_samples); +} + +void Worker::sample_client_stat(ClientStat *cstat) { + stats.client_stats.push_back(*cstat); + assert(stats.client_stats.size() <= max_samples); +} + +void Worker::report_progress() { + if (id != 0 || config->is_rate_mode() || stats.req_done % progress_interval) { + return; + } + + std::cout << "progress: " << stats.req_done * 100 / stats.req_todo << "% done" + << std::endl; +} + +void Worker::report_rate_progress() { + if (id != 0 || nconns_made % progress_interval) { + return; + } + + std::cout << "progress: " << nconns_made * 100 / nclients + << "% of clients started" << std::endl; +} + namespace { // Returns percentage of number of samples within mean +/- sd. double within_sd(const std::vector &samples, double mean, double sd) { @@ -1106,12 +1189,15 @@ double within_sd(const std::vector &samples, double mean, double sd) { namespace { // Computes statistics using |samples|. The min, max, mean, sd, and // percentage of number of samples within mean +/- sd are computed. -SDStat compute_time_stat(const std::vector &samples) { +// If |sampling| is true, this computes sample variance. Otherwise, +// population variance. +SDStat compute_time_stat(const std::vector &samples, + bool sampling = false) { if (samples.empty()) { return {0.0, 0.0, 0.0, 0.0, 0.0}; } // standard deviation calculated using Rapid calculation method: - // http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods + // https://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods double a = 0, q = 0; size_t n = 0; double sum = 0; @@ -1130,7 +1216,7 @@ SDStat compute_time_stat(const std::vector &samples) { assert(n > 0); res.mean = sum / n; - res.sd = sqrt(q / n); + res.sd = sqrt(q / (sampling && n > 1 ? n - 1 : n)); res.within_sd = within_sd(samples, res.mean, res.sd); return res; @@ -1140,18 +1226,29 @@ SDStat compute_time_stat(const std::vector &samples) { namespace { SDStats process_time_stats(const std::vector> &workers) { + auto request_times_sampling = false; + auto client_times_sampling = false; size_t nrequest_times = 0; + size_t nclient_times = 0; for (const auto &w : workers) { nrequest_times += w->stats.req_stats.size(); + if (w->request_times_smp.interval != 0.) { + request_times_sampling = true; + } + + nclient_times += w->stats.client_stats.size(); + if (w->client_smp.interval != 0.) { + client_times_sampling = true; + } } std::vector request_times; request_times.reserve(nrequest_times); std::vector connect_times, ttfb_times, rps_values; - connect_times.reserve(config.nclients); - ttfb_times.reserve(config.nclients); - rps_values.reserve(config.nclients); + connect_times.reserve(nclient_times); + ttfb_times.reserve(nclient_times); + rps_values.reserve(nclient_times); for (const auto &w : workers) { for (const auto &req_stat : w->stats.req_stats) { @@ -1195,8 +1292,10 @@ process_time_stats(const std::vector> &workers) { } } - return {compute_time_stat(request_times), compute_time_stat(connect_times), - compute_time_stat(ttfb_times), compute_time_stat(rps_values)}; + return {compute_time_stat(request_times, request_times_sampling), + compute_time_stat(connect_times, client_times_sampling), + compute_time_stat(ttfb_times, client_times_sampling), + compute_time_stat(rps_values, client_times_sampling)}; } } // namespace @@ -1374,7 +1473,7 @@ void read_script_from_file(std::istream &infile, namespace { std::unique_ptr create_worker(uint32_t id, SSL_CTX *ssl_ctx, size_t nreqs, size_t nclients, - size_t rate) { + size_t rate, size_t max_samples) { std::stringstream rate_report; if (config.is_rate_mode() && nclients > rate) { rate_report << "Up to " << rate << " client(s) will be created every " @@ -1385,7 +1484,8 @@ std::unique_ptr create_worker(uint32_t id, SSL_CTX *ssl_ctx, << " total client(s). " << rate_report.str() << nreqs << " total requests" << std::endl; - return make_unique(id, ssl_ctx, nreqs, nclients, rate, &config); + return make_unique(id, ssl_ctx, nreqs, nclients, rate, max_samples, + &config); } } // namespace @@ -1448,10 +1548,11 @@ Options: URIs, if present, are ignored. Those in the first URI are used solely. Definition of a base URI overrides all scheme, host or port values. - -m, --max-concurrent-streams=(auto|) - Max concurrent streams to issue per session. If "auto" - is given, the number of given URIs is used. - Default: auto + -m, --max-concurrent-streams= + Max concurrent streams to issue per session. When + http/1.1 is used, this specifies the number of HTTP + pipelining requests in-flight. + Default: 1 -w, --window-bits= Sets the stream level initial window size to (2**)-1. For SPDY, 2** is used instead. @@ -1626,11 +1727,7 @@ int main(int argc, char **argv) { #endif // NOTHREADS break; case 'm': - if (util::strieq("auto", optarg)) { - config.max_concurrent_streams = -1; - } else { - config.max_concurrent_streams = strtoul(optarg, nullptr, 10); - } + config.max_concurrent_streams = strtoul(optarg, nullptr, 10); break; case 'w': case 'W': { @@ -1853,11 +1950,6 @@ int main(int argc, char **argv) { exit(EXIT_FAILURE); } - if (config.max_concurrent_streams == -1) { - config.max_concurrent_streams = reqlines.size(); - } - - assert(config.max_concurrent_streams > 0); if (config.nreqs == 0) { std::cerr << "-n: the number of requests must be strictly greater than 0." << std::endl; @@ -2099,6 +2191,9 @@ int main(int argc, char **argv) { size_t rate_per_thread = config.rate / config.nthreads; ssize_t rate_per_thread_rem = config.rate % config.nthreads; + size_t max_samples_per_thread = + std::max(static_cast(256), MAX_SAMPLES / config.nthreads); + std::mutex mu; std::condition_variable cv; auto ready = false; @@ -2131,7 +2226,8 @@ int main(int argc, char **argv) { } } - workers.push_back(create_worker(i, ssl_ctx, nreqs, nclients, rate)); + workers.push_back(create_worker(i, ssl_ctx, nreqs, nclients, rate, + max_samples_per_thread)); auto &worker = workers.back(); futures.push_back( std::async(std::launch::async, [&worker, &mu, &cv, &ready]() { @@ -2161,7 +2257,8 @@ int main(int argc, char **argv) { auto nreqs = config.timing_script ? config.nreqs * config.nclients : config.nreqs; - workers.push_back(create_worker(0, ssl_ctx, nreqs, nclients, rate)); + workers.push_back( + create_worker(0, ssl_ctx, nreqs, nclients, rate, MAX_SAMPLES)); auto start = std::chrono::steady_clock::now(); @@ -2253,7 +2350,7 @@ time for request: )" << std::setw(10) << util::format_duration(ts.request.min) << util::format_duration(ts.ttfb.mean) << " " << std::setw(10) << util::format_duration(ts.ttfb.sd) << std::setw(9) << util::dtos(ts.ttfb.within_sd) << "%" - << "\nreq/s (client) : " << std::setw(10) << ts.rps.min << " " + << "\nreq/s : " << std::setw(10) << ts.rps.min << " " << std::setw(10) << ts.rps.max << " " << std::setw(10) << ts.rps.mean << " " << std::setw(10) << ts.rps.sd << std::setw(9) << util::dtos(ts.rps.within_sd) << "%" << std::endl; diff --git a/src/h2load.h b/src/h2load.h index a7e07ea4..0d95b8cd 100644 --- a/src/h2load.h +++ b/src/h2load.h @@ -112,7 +112,6 @@ struct Config { }; struct RequestStat { - RequestStat(); // time point when request was sent std::chrono::steady_clock::time_point request_time; // time point when stream was closed @@ -208,9 +207,21 @@ enum ClientState { CLIENT_IDLE, CLIENT_CONNECTED }; struct Client; +// We use systematic sampling method +struct Sampling { + // sampling interval + double interval; + // cumulative value of interval, and the next point is the integer + // rounded up from this value. + double point; + // number of samples seen, including discarded samples. + size_t n; +}; + struct Worker { - std::vector> clients; Stats stats; + Sampling request_times_smp; + Sampling client_smp; struct ev_loop *loop; SSL_CTX *ssl_ctx; Config *config; @@ -226,24 +237,32 @@ struct Worker { // at most nreqs_rem clients get an extra request size_t nreqs_rem; size_t rate; + // maximum number of samples in this worker thread + size_t max_samples; ev_timer timeout_watcher; // The next client ID this worker assigns uint32_t next_client_id; Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t nreq_todo, size_t nclients, - size_t rate, Config *config); + size_t rate, size_t max_samples, Config *config); ~Worker(); Worker(Worker &&o) = default; void run(); + void sample_req_stat(RequestStat *req_stat); + void sample_client_stat(ClientStat *cstat); + void report_progress(); + void report_rate_progress(); }; struct Stream { + RequestStat req_stat; int status_success; Stream(); }; struct Client { std::unordered_map streams; + ClientStat cstat; std::unique_ptr session; ev_io wev; ev_io rev; @@ -288,7 +307,6 @@ struct Client { void process_request_failure(); void process_timedout_streams(); void process_abandoned_streams(); - void report_progress(); void report_tls_info(); void report_app_info(); void terminate_session(); @@ -318,8 +336,12 @@ struct Client { // |success| == true means that the request/response was exchanged // |successfully, but it does not mean response carried successful // |HTTP status code. - void on_stream_close(int32_t stream_id, bool success, RequestStat *req_stat, - bool final = false); + void on_stream_close(int32_t stream_id, bool success, bool final = false); + // Returns RequestStat for |stream_id|. This function must be + // called after on_request(stream_id), and before + // on_stream_close(stream_id, ...). Otherwise, this will return + // nullptr. + RequestStat *get_req_stat(int32_t stream_id); void record_request_time(RequestStat *req_stat); void record_connect_start_time(); diff --git a/src/h2load_http1_session.cc b/src/h2load_http1_session.cc index aa43f90d..a295c625 100644 --- a/src/h2load_http1_session.cc +++ b/src/h2load_http1_session.cc @@ -80,9 +80,11 @@ int htp_msg_completecb(http_parser *htp) { auto client = session->get_client(); auto final = http_should_keep_alive(htp) == 0; - client->on_stream_close(session->stream_resp_counter_, true, - session->req_stats_[session->stream_resp_counter_], - final); + auto req_stat = client->get_req_stat(session->stream_resp_counter_); + + assert(req_stat); + + client->on_stream_close(session->stream_resp_counter_, true, final); session->stream_resp_counter_ += 2; @@ -150,7 +152,7 @@ http_parser_settings htp_hooks = { void Http1Session::on_connect() { client_->signal_write(); } -int Http1Session::submit_request(RequestStat *req_stat) { +int Http1Session::submit_request() { auto config = client_->worker->config; const auto &req = config->h1reqs[client_->reqidx]; client_->reqidx++; @@ -159,13 +161,13 @@ int Http1Session::submit_request(RequestStat *req_stat) { client_->reqidx = 0; } - assert(req_stat); + client_->on_request(stream_req_counter_); + + auto req_stat = client_->get_req_stat(stream_req_counter_); + client_->record_request_time(req_stat); client_->wb.write(req.c_str(), req.size()); - client_->on_request(stream_req_counter_); - req_stats_[stream_req_counter_] = req_stat; - // increment for next request stream_req_counter_ += 2; diff --git a/src/h2load_http1_session.h b/src/h2load_http1_session.h index a19ba65e..1b48c9c4 100644 --- a/src/h2load_http1_session.h +++ b/src/h2load_http1_session.h @@ -38,14 +38,13 @@ public: Http1Session(Client *client); virtual ~Http1Session(); virtual void on_connect(); - virtual int submit_request(RequestStat *req_stat); + virtual int submit_request(); virtual int on_read(const uint8_t *data, size_t len); virtual int on_write(); virtual void terminate(); Client *get_client(); int32_t stream_req_counter_; int32_t stream_resp_counter_; - std::unordered_map req_stats_; private: Client *client_; diff --git a/src/h2load_http2_session.cc b/src/h2load_http2_session.cc index 0bbbe8e8..7883131d 100644 --- a/src/h2load_http2_session.cc +++ b/src/h2load_http2_session.cc @@ -89,12 +89,25 @@ namespace { int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data) { auto client = static_cast(user_data); - auto req_stat = static_cast( - nghttp2_session_get_stream_user_data(session, stream_id)); - if (!req_stat) { + client->on_stream_close(stream_id, error_code == NGHTTP2_NO_ERROR); + + return 0; +} +} // namespace + +namespace { +int on_frame_not_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, int lib_error_code, + void *user_data) { + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { return 0; } - client->on_stream_close(stream_id, error_code == NGHTTP2_NO_ERROR, req_stat); + + auto client = static_cast(user_data); + // request was not sent. Mark it as error. + client->on_stream_close(frame->hd.stream_id, false); + return 0; } } // namespace @@ -108,9 +121,7 @@ int before_frame_send_callback(nghttp2_session *session, } auto client = static_cast(user_data); - client->on_request(frame->hd.stream_id); - auto req_stat = static_cast( - nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + auto req_stat = client->get_req_stat(frame->hd.stream_id); assert(req_stat); client->record_request_time(req_stat); @@ -124,8 +135,7 @@ ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, nghttp2_data_source *source, void *user_data) { auto client = static_cast(user_data); auto config = client->worker->config; - auto req_stat = static_cast( - nghttp2_session_get_stream_user_data(session, stream_id)); + auto req_stat = client->get_req_stat(stream_id); assert(req_stat); ssize_t nread; while ((nread = pread(config->data_fd, buf, length, req_stat->data_offset)) == @@ -183,6 +193,9 @@ void Http2Session::on_connect() { nghttp2_session_callbacks_set_on_header_callback(callbacks, on_header_callback); + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, on_frame_not_send_callback); + nghttp2_session_callbacks_set_before_frame_send_callback( callbacks, before_frame_send_callback); @@ -212,7 +225,7 @@ void Http2Session::on_connect() { client_->signal_write(); } -int Http2Session::submit_request(RequestStat *req_stat) { +int Http2Session::submit_request() { if (nghttp2_session_check_request_allowed(session_) == 0) { return -1; } @@ -228,11 +241,13 @@ int Http2Session::submit_request(RequestStat *req_stat) { auto stream_id = nghttp2_submit_request(session_, nullptr, nva.data(), nva.size(), - config->data_fd == -1 ? nullptr : &prd, req_stat); + config->data_fd == -1 ? nullptr : &prd, nullptr); if (stream_id < 0) { return -1; } + client_->on_request(stream_id); + return 0; } diff --git a/src/h2load_http2_session.h b/src/h2load_http2_session.h index c3a3344b..2d8b1fa8 100644 --- a/src/h2load_http2_session.h +++ b/src/h2load_http2_session.h @@ -38,7 +38,7 @@ public: Http2Session(Client *client); virtual ~Http2Session(); virtual void on_connect(); - virtual int submit_request(RequestStat *req_stat); + virtual int submit_request(); virtual int on_read(const uint8_t *data, size_t len); virtual int on_write(); virtual void terminate(); diff --git a/src/h2load_session.h b/src/h2load_session.h index 1f8ed60a..17103aa9 100644 --- a/src/h2load_session.h +++ b/src/h2load_session.h @@ -41,7 +41,7 @@ public: // Called when the connection was made. virtual void on_connect() = 0; // Called when one request must be issued. - virtual int submit_request(RequestStat *req_stat) = 0; + virtual int submit_request() = 0; // Called when incoming bytes are available. The subclass has to // return the number of bytes read. virtual int on_read(const uint8_t *data, size_t len) = 0; diff --git a/src/h2load_spdy_session.cc b/src/h2load_spdy_session.cc index a3b85591..4afa13e1 100644 --- a/src/h2load_spdy_session.cc +++ b/src/h2load_spdy_session.cc @@ -48,9 +48,7 @@ void before_ctrl_send_callback(spdylay_session *session, return; } client->on_request(frame->syn_stream.stream_id); - auto req_stat = - static_cast(spdylay_session_get_stream_user_data( - session, frame->syn_stream.stream_id)); + auto req_stat = client->get_req_stat(frame->syn_stream.stream_id); client->record_request_time(req_stat); } } // namespace @@ -104,9 +102,7 @@ void on_stream_close_callback(spdylay_session *session, int32_t stream_id, spdylay_status_code status_code, void *user_data) { auto client = static_cast(user_data); - auto req_stat = static_cast( - spdylay_session_get_stream_user_data(session, stream_id)); - client->on_stream_close(stream_id, status_code == SPDYLAY_OK, req_stat); + client->on_stream_close(stream_id, status_code == SPDYLAY_OK); } } // namespace @@ -130,8 +126,7 @@ ssize_t file_read_callback(spdylay_session *session, int32_t stream_id, spdylay_data_source *source, void *user_data) { auto client = static_cast(user_data); auto config = client->worker->config; - auto req_stat = static_cast( - spdylay_session_get_stream_user_data(session, stream_id)); + auto req_stat = client->get_req_stat(stream_id); ssize_t nread; while ((nread = pread(config->data_fd, buf, length, req_stat->data_offset)) == @@ -185,7 +180,7 @@ void SpdySession::on_connect() { client_->signal_write(); } -int SpdySession::submit_request(RequestStat *req_stat) { +int SpdySession::submit_request() { int rv; auto config = client_->worker->config; auto &nv = config->nv[client_->reqidx++]; @@ -197,7 +192,7 @@ int SpdySession::submit_request(RequestStat *req_stat) { spdylay_data_provider prd{{0}, file_read_callback}; rv = spdylay_submit_request(session_, 0, nv.data(), - config->data_fd == -1 ? nullptr : &prd, req_stat); + config->data_fd == -1 ? nullptr : &prd, nullptr); if (rv != 0) { return -1; diff --git a/src/h2load_spdy_session.h b/src/h2load_spdy_session.h index 288be1df..44fd4d55 100644 --- a/src/h2load_spdy_session.h +++ b/src/h2load_spdy_session.h @@ -40,7 +40,7 @@ public: SpdySession(Client *client, uint16_t spdy_version); virtual ~SpdySession(); virtual void on_connect(); - virtual int submit_request(RequestStat *req_stat); + virtual int submit_request(); virtual int on_read(const uint8_t *data, size_t len); virtual int on_write(); virtual void terminate(); diff --git a/src/http2.cc b/src/http2.cc index 671b1a92..6f9128e1 100644 --- a/src/http2.cc +++ b/src/http2.cc @@ -113,6 +113,8 @@ std::string get_status_string(unsigned int status_code) { return "429 Too Many Requests"; case 431: return "431 Request Header Fields Too Large"; + case 451: + return "451 Unavailable For Legal Reasons"; case 500: return "500 Internal Server Error"; case 501: @@ -215,6 +217,8 @@ const char *stringify_status(unsigned int status_code) { return "429"; case 431: return "431"; + case 451: + return "451"; case 500: return "500"; case 501: diff --git a/src/includes/nghttp2/asio_http2_client.h b/src/includes/nghttp2/asio_http2_client.h index a2b51ad7..55ef9f68 100644 --- a/src/includes/nghttp2/asio_http2_client.h +++ b/src/includes/nghttp2/asio_http2_client.h @@ -144,6 +144,12 @@ public: // and session is terminated. void on_error(error_cb cb) const; + // Sets connect timeout, which defaults to 60 seconds. + void connect_timeout(const boost::posix_time::time_duration &t); + + // Sets read timeout, which defaults to 60 seconds. + void read_timeout(const boost::posix_time::time_duration &t); + // Shutdowns connection. void shutdown() const; @@ -177,7 +183,7 @@ public: generator_cb cb, header_map h = header_map{}) const; private: - std::unique_ptr impl_; + std::shared_ptr impl_; }; // configure |tls_ctx| for client use. Currently, we just set NPN diff --git a/src/includes/nghttp2/asio_http2_server.h b/src/includes/nghttp2/asio_http2_server.h index 1c5a71fd..5818e301 100644 --- a/src/includes/nghttp2/asio_http2_server.h +++ b/src/includes/nghttp2/asio_http2_server.h @@ -59,6 +59,9 @@ public: // Application must not call this directly. request_impl &impl() const; + // Returns the remote endpoint of the request + const boost::asio::ip::tcp::endpoint &remote_endpoint() const; + private: std::unique_ptr impl_; }; @@ -195,12 +198,22 @@ public: // connections. void backlog(int backlog); + // Sets TLS handshake timeout, which defaults to 60 seconds. + void tls_handshake_timeout(const boost::posix_time::time_duration &t); + + // Sets read timeout, which defaults to 60 seconds. + void read_timeout(const boost::posix_time::time_duration &t); + // Gracefully stop http2 server void stop(); // Join on http2 server and wait for it to fully stop void join(); + // Get access to the io_service objects. + const std::vector> & + io_services() const; + private: std::unique_ptr impl_; }; diff --git a/src/nghttpd.cc b/src/nghttpd.cc index 58179aea..8add1951 100644 --- a/src/nghttpd.cc +++ b/src/nghttpd.cc @@ -164,6 +164,8 @@ Options: Path to file that contains MIME media types and the extensions that represent them. Default: )" << config.mime_types_file << R"( + --no-content-length + Don't send content-length header field. --version Display version information and exit. -h, --help Display this help and exit. @@ -209,6 +211,7 @@ int main(int argc, char **argv) { {"hexdump", no_argument, &flag, 7}, {"echo-upload", no_argument, &flag, 8}, {"mime-types-file", required_argument, &flag, 9}, + {"no-content-length", no_argument, &flag, 10}, {nullptr, 0, nullptr, 0}}; int option_index = 0; int c = getopt_long(argc, argv, "DVb:c:d:ehm:n:p:va:", long_options, @@ -340,6 +343,10 @@ int main(int argc, char **argv) { mime_types_file_set_manually = true; config.mime_types_file = optarg; break; + case 10: + // no-content-length option + config.no_content_length = true; + break; } break; default: diff --git a/src/shrpx.cc b/src/shrpx.cc index a83062f3..16daa620 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -1610,14 +1610,15 @@ HTTP: used several times to specify multiple header fields. Example: --add-response-header="foo: bar" --header-field-buffer= - Set maximum buffer size for incoming HTTP header field - list. This is the sum of header name and value in + Set maximum buffer size for incoming HTTP request header + field list. This is the sum of header name and value in bytes. Default: )" << util::utos_with_unit(get_config()->header_field_buffer) << R"( --max-header-fields= - Set maximum number of incoming HTTP header fields, which - appear in one request or response header field list. + Set maximum number of incoming HTTP request header + fields, which appear in one request or response header + field list. Default: )" << get_config()->max_header_fields << R"( Debug: diff --git a/src/shrpx.h b/src/shrpx.h index dfc52701..12e17179 100644 --- a/src/shrpx.h +++ b/src/shrpx.h @@ -44,4 +44,8 @@ #define DIE() _Exit(EXIT_FAILURE) +#if defined(HAVE_DECL_INITGROUPS) && !HAVE_DECL_INITGROUPS +inline int initgroups(const char *user, gid_t group) { return 0; } +#endif // defined(HAVE_DECL_INITGROUPS) && !HAVE_DECL_INITGROUPS + #endif // SHRPX_H diff --git a/src/shrpx_client_handler.cc b/src/shrpx_client_handler.cc index 8ace9d4a..b6efe4d7 100644 --- a/src/shrpx_client_handler.cc +++ b/src/shrpx_client_handler.cc @@ -718,6 +718,34 @@ void ClientHandler::direct_http2_upgrade() { int ClientHandler::perform_http2_upgrade(HttpsUpstream *http) { auto upstream = make_unique(this); + + auto output = upstream->get_response_buf(); + + // We might have written non-final header in response_buf, in this + // case, response_state is still INITIAL. If this non-final header + // and upgrade header fit in output buffer, do upgrade. Otherwise, + // to avoid to send this non-final header as response body in HTTP/2 + // upstream, fail upgrade. + auto downstream = http->get_downstream(); + auto input = downstream->get_response_buf(); + + static constexpr char res[] = + "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: Upgrade\r\n" + "Upgrade: " NGHTTP2_CLEARTEXT_PROTO_VERSION_ID "\r\n" + "\r\n"; + + auto required_size = str_size(res) + input->rleft(); + + if (output->wleft() < required_size) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) + << "HTTP Upgrade failed because of insufficient buffer space: need " + << required_size << ", available " << output->wleft(); + } + return -1; + } + if (upstream->upgrade_upstream(http) != 0) { return -1; } @@ -729,11 +757,11 @@ int ClientHandler::perform_http2_upgrade(HttpsUpstream *http) { on_read_ = &ClientHandler::upstream_http2_connhd_read; write_ = &ClientHandler::write_clear; - static char res[] = "HTTP/1.1 101 Switching Protocols\r\n" - "Connection: Upgrade\r\n" - "Upgrade: " NGHTTP2_CLEARTEXT_PROTO_VERSION_ID "\r\n" - "\r\n"; - upstream->get_response_buf()->write(res, sizeof(res) - 1); + auto nread = + downstream->get_response_buf()->remove(output->last, output->wleft()); + output->write(nread); + + output->write(res, str_size(res)); upstream_ = std::move(upstream); signal_write(); diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index 565d3118..2281d4ab 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -276,11 +276,9 @@ std::string read_passwd_from_file(const char *filename) { } std::pair parse_header(const char *optarg) { - // We skip possible ":" at the start of optarg. - const auto *colon = strchr(optarg + 1, ':'); + const auto *colon = strchr(optarg, ':'); - // name = ":" is not allowed - if (colon == nullptr || (optarg[0] == ':' && colon == optarg + 1)) { + if (colon == nullptr || colon == optarg) { return {"", ""}; } @@ -291,7 +289,14 @@ std::pair parse_header(const char *optarg) { auto p = std::make_pair(std::string(optarg, colon), std::string(value, strlen(value))); util::inp_strlower(p.first); - util::inp_strlower(p.second); + + if (!nghttp2_check_header_name( + reinterpret_cast(p.first.c_str()), p.first.size()) || + !nghttp2_check_header_value( + reinterpret_cast(p.second.c_str()), + p.second.size())) { + return {"", ""}; + } return p; } @@ -1800,7 +1805,7 @@ int parse_config(const char *opt, const char *optarg, case SHRPX_OPTID_ADD_RESPONSE_HEADER: { auto p = parse_header(optarg); if (p.first.empty()) { - LOG(ERROR) << opt << ": header field name is empty: " << optarg; + LOG(ERROR) << opt << ": invalid header field: " << optarg; return -1; } if (optid == SHRPX_OPTID_ADD_REQUEST_HEADER) { diff --git a/src/shrpx_config_test.cc b/src/shrpx_config_test.cc index 5138dba4..8ac0cda0 100644 --- a/src/shrpx_config_test.cc +++ b/src/shrpx_config_test.cc @@ -46,8 +46,7 @@ void test_shrpx_config_parse_header(void) { CU_ASSERT("b" == p.second); p = parse_header(":a: b"); - CU_ASSERT(":a" == p.first); - CU_ASSERT("b" == p.second); + CU_ASSERT(p.first.empty()); p = parse_header("a: :b"); CU_ASSERT("a" == p.first); @@ -59,6 +58,12 @@ void test_shrpx_config_parse_header(void) { p = parse_header("alpha: bravo charlie"); CU_ASSERT("alpha" == p.first); CU_ASSERT("bravo charlie" == p.second); + + p = parse_header("a,: b"); + CU_ASSERT(p.first.empty()); + + p = parse_header("a: b\x0a"); + CU_ASSERT(p.first.empty()); } void test_shrpx_config_parse_log_format(void) { diff --git a/src/shrpx_http2_session.cc b/src/shrpx_http2_session.cc index dd6ed02d..7ab65ed8 100644 --- a/src/shrpx_http2_session.cc +++ b/src/shrpx_http2_session.cc @@ -757,26 +757,6 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, auto trailer = frame->headers.cat == NGHTTP2_HCAT_HEADERS && !downstream->get_expect_final_response(); - if (downstream->get_response_headers_sum() + namelen + valuelen > - get_config()->header_field_buffer || - downstream->get_response_headers().size() >= - get_config()->max_header_fields) { - if (LOG_ENABLED(INFO)) { - DLOG(INFO, downstream) - << "Too large or many header field size=" - << downstream->get_response_headers_sum() + namelen + valuelen - << ", num=" << downstream->get_response_headers().size() + 1; - } - - if (trailer) { - // we don't care trailer part exceeds header size limit; just - // discard it. - return 0; - } - - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - } - if (trailer) { // just store header fields for trailer part downstream->add_response_trailer(name, namelen, value, valuelen, @@ -803,21 +783,6 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, assert(promised_downstream); - if (promised_downstream->get_request_headers_sum() + namelen + valuelen > - get_config()->header_field_buffer || - promised_downstream->get_request_headers().size() >= - get_config()->max_header_fields) { - if (LOG_ENABLED(INFO)) { - DLOG(INFO, promised_downstream) - << "Too large or many header field size=" - << promised_downstream->get_request_headers_sum() + namelen + - valuelen << ", num=" - << promised_downstream->get_request_headers().size() + 1; - } - - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - } - auto token = http2::lookup_token(name, namelen); promised_downstream->add_request_header(name, namelen, value, valuelen, flags & NGHTTP2_NV_FLAG_NO_INDEX, diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index bc0de69c..de1dec18 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -169,7 +169,8 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, if (downstream->get_request_headers_sum() + namelen + valuelen > get_config()->header_field_buffer || - downstream->get_request_headers().size() >= + downstream->get_request_headers().size() + + downstream->get_request_trailers().size() >= get_config()->max_header_fields) { if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { return 0; diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc index 720f5a34..bab1c902 100644 --- a/src/shrpx_http_downstream_connection.cc +++ b/src/shrpx_http_downstream_connection.cc @@ -562,29 +562,10 @@ int htp_hdrs_completecb(http_parser *htp) { namespace { int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { auto downstream = static_cast(htp->data); - - if (downstream->get_response_headers_sum() + len > - get_config()->header_field_buffer) { - if (LOG_ENABLED(INFO)) { - DLOG(INFO, downstream) << "Too large header block size=" - << downstream->get_response_headers_sum() + len; - } - return -1; - } - if (downstream->get_response_state() == Downstream::INITIAL) { if (downstream->get_response_header_key_prev()) { downstream->append_last_response_header_key(data, len); } else { - if (downstream->get_response_headers().size() >= - get_config()->max_header_fields) { - if (LOG_ENABLED(INFO)) { - DLOG(INFO, downstream) - << "Too many header field num=" - << downstream->get_response_headers().size() + 1; - } - return -1; - } downstream->add_response_header(std::string(data, len), ""); } } else { @@ -592,15 +573,6 @@ int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { if (downstream->get_response_trailer_key_prev()) { downstream->append_last_response_trailer_key(data, len); } else { - if (downstream->get_response_headers().size() >= - get_config()->max_header_fields) { - if (LOG_ENABLED(INFO)) { - DLOG(INFO, downstream) - << "Too many header field num=" - << downstream->get_response_headers().size() + 1; - } - return -1; - } downstream->add_response_trailer(std::string(data, len), ""); } } @@ -611,14 +583,6 @@ int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { namespace { int htp_hdr_valcb(http_parser *htp, const char *data, size_t len) { auto downstream = static_cast(htp->data); - if (downstream->get_response_headers_sum() + len > - get_config()->header_field_buffer) { - if (LOG_ENABLED(INFO)) { - DLOG(INFO, downstream) << "Too large header block size=" - << downstream->get_response_headers_sum() + len; - } - return -1; - } if (downstream->get_response_state() == Downstream::INITIAL) { if (downstream->get_response_header_key_prev()) { downstream->set_last_response_header_value(data, len); diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc index 2f9cd831..8f584749 100644 --- a/src/shrpx_https_upstream.cc +++ b/src/shrpx_https_upstream.cc @@ -146,7 +146,8 @@ int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { if (downstream->get_request_trailer_key_prev()) { downstream->append_last_request_trailer_key(data, len); } else { - if (downstream->get_request_headers().size() >= + if (downstream->get_request_headers().size() + + downstream->get_request_trailers().size() >= get_config()->max_header_fields) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "Too many header field num=" @@ -409,11 +410,6 @@ int htp_msg_completecb(http_parser *htp) { if (handler->get_http2_upgrade_allowed() && downstream->get_http2_upgrade_request() && - // we may write non-final header in response_buf, in this case, - // response_state is still INITIAL. So don't upgrade in this - // case, otherwise we end up send this non-final header as - // response body in HTTP/2 upstream. - downstream->get_response_buf()->rleft() == 0 && handler->perform_http2_upgrade(upstream) != 0) { if (LOG_ENABLED(INFO)) { ULOG(INFO, upstream) << "HTTP Upgrade to HTTP/2 failed"; diff --git a/src/shrpx_spdy_upstream.cc b/src/shrpx_spdy_upstream.cc index d7d2269a..604fb612 100644 --- a/src/shrpx_spdy_upstream.cc +++ b/src/shrpx_spdy_upstream.cc @@ -169,6 +169,8 @@ void on_ctrl_recv_callback(spdylay_session *session, spdylay_frame_type type, header_buffer += strlen(nv[i]) + strlen(nv[i + 1]); } + // spdy does not define usage of trailer fields, and we ignores + // them. if (header_buffer > get_config()->header_field_buffer || num_headers > get_config()->max_header_fields) { upstream->rst_stream(downstream, SPDYLAY_INTERNAL_ERROR); diff --git a/src/shrpx_ssl.cc b/src/shrpx_ssl.cc index 0a7ca6ac..61e61cbf 100644 --- a/src/shrpx_ssl.cc +++ b/src/shrpx_ssl.cc @@ -41,6 +41,7 @@ #include #include #include +#include #include diff --git a/src/ssl.cc b/src/ssl.cc index bfb531f8..d5bb3562 100644 --- a/src/ssl.cc +++ b/src/ssl.cc @@ -80,12 +80,7 @@ LibsslGlobalLock::LibsslGlobalLock() { LibsslGlobalLock::~LibsslGlobalLock() { ssl_global_locks.clear(); } const char *get_tls_protocol(SSL *ssl) { - auto session = SSL_get_session(ssl); - if (!session) { - return "unknown"; - } - - switch (session->ssl_version) { + switch (SSL_version(ssl)) { case SSL2_VERSION: return "SSLv2"; case SSL3_VERSION: @@ -113,10 +108,12 @@ TLSSessionInfo *get_tls_session_info(TLSSessionInfo *tls_info, SSL *ssl) { tls_info->cipher = SSL_get_cipher_name(ssl); tls_info->protocol = get_tls_protocol(ssl); - tls_info->session_id = session->session_id; - tls_info->session_id_length = session->session_id_length; tls_info->session_reused = SSL_session_reused(ssl); + unsigned int session_id_length; + tls_info->session_id = SSL_SESSION_get_id(session, &session_id_length); + tls_info->session_id_length = session_id_length; + return tls_info; } diff --git a/tests/main.c b/tests/main.c index db7ef68c..6a985317 100644 --- a/tests/main.c +++ b/tests/main.c @@ -89,6 +89,8 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_session_recv_headers_with_priority) || !CU_add_test(pSuite, "session_recv_headers_early_response", test_nghttp2_session_recv_headers_early_response) || + !CU_add_test(pSuite, "session_server_recv_push_response", + test_nghttp2_session_server_recv_push_response) || !CU_add_test(pSuite, "session_recv_premature_headers", test_nghttp2_session_recv_premature_headers) || !CU_add_test(pSuite, "session_recv_unknown_frame", @@ -128,6 +130,8 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_session_on_window_update_received) || !CU_add_test(pSuite, "session_on_data_received", test_nghttp2_session_on_data_received) || + !CU_add_test(pSuite, "session_on_data_received_fail_fast", + test_nghttp2_session_on_data_received_fail_fast) || !CU_add_test(pSuite, "session_send_headers_start_stream", test_nghttp2_session_send_headers_start_stream) || !CU_add_test(pSuite, "session_send_headers_reply", @@ -205,8 +209,6 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_session_reply_fail) || !CU_add_test(pSuite, "session_max_concurrent_streams", test_nghttp2_session_max_concurrent_streams) || - !CU_add_test(pSuite, "session_stream_close_on_headers_push", - test_nghttp2_session_stream_close_on_headers_push) || !CU_add_test(pSuite, "session_stop_data_with_rst_stream", test_nghttp2_session_stop_data_with_rst_stream) || !CU_add_test(pSuite, "session_defer_data", @@ -291,6 +293,10 @@ int main(int argc _U_, char *argv[] _U_) { !CU_add_test(pSuite, "session_flooding", test_nghttp2_session_flooding) || !CU_add_test(pSuite, "session_change_stream_priority", test_nghttp2_session_change_stream_priority) || + !CU_add_test(pSuite, "session_repeated_priority_change", + test_nghttp2_session_repeated_priority_change) || + !CU_add_test(pSuite, "session_repeated_priority_submission", + test_nghttp2_session_repeated_priority_submission) || !CU_add_test(pSuite, "http_mandatory_headers", test_nghttp2_http_mandatory_headers) || !CU_add_test(pSuite, "http_content_length", diff --git a/tests/nghttp2_session_test.c b/tests/nghttp2_session_test.c index 1033ea4d..88240dcd 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -342,16 +342,6 @@ static int send_data_callback(nghttp2_session *session _U_, return 0; } -/* static void no_stream_user_data_stream_close_callback */ -/* (nghttp2_session *session, */ -/* int32_t stream_id, */ -/* nghttp2_error_code error_code, */ -/* void *user_data) */ -/* { */ -/* my_user_data* my_data = (my_user_data*)user_data; */ -/* ++my_data->stream_close_cb_called; */ -/* } */ - static ssize_t block_count_send_callback(nghttp2_session *session _U_, const uint8_t *data _U_, size_t len, int flags _U_, void *user_data) { @@ -1487,6 +1477,58 @@ void test_nghttp2_session_recv_headers_early_response(void) { nghttp2_bufs_free(&bufs); } +void test_nghttp2_session_server_recv_push_response(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_bufs bufs; + nghttp2_buf *buf; + ssize_t rv; + my_user_data ud; + nghttp2_mem *mem; + nghttp2_frame frame; + nghttp2_hd_deflater deflater; + nghttp2_nv *nva; + size_t nvlen; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.on_invalid_frame_recv_callback = on_invalid_frame_recv_callback; + + nghttp2_session_server_new(&session, &callbacks, &ud); + + nghttp2_hd_deflate_init(&deflater, mem); + + nghttp2_session_open_stream(session, 2, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, NGHTTP2_STREAM_RESERVED, NULL); + + nvlen = ARRLEN(resnv); + nghttp2_nv_array_copy(&nva, resnv, nvlen, mem); + nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 2, + NGHTTP2_HCAT_HEADERS, &pri_spec_default, nva, + nvlen); + rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); + + CU_ASSERT(0 == rv); + CU_ASSERT(nghttp2_bufs_len(&bufs) > 0); + + nghttp2_frame_headers_free(&frame.headers, mem); + + buf = &bufs.head->buf; + + ud.invalid_frame_recv_cb_called = 0; + + rv = nghttp2_session_mem_recv(session, buf->pos, nghttp2_buf_len(buf)); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) == rv); + CU_ASSERT(1 == ud.invalid_frame_recv_cb_called); + + nghttp2_bufs_free(&bufs); + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); +} + void test_nghttp2_session_recv_premature_headers(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; @@ -2329,6 +2371,25 @@ void test_nghttp2_session_on_request_headers_received(void) { nghttp2_frame_headers_free(&frame.headers, mem); nghttp2_session_del(session); + + nghttp2_session_server_new(&session, &callbacks, &user_data); + + /* HEADERS to closed stream */ + stream = open_stream(session, 1); + session->last_recv_stream_id = 1; + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + nghttp2_session_close_stream(session, 1, NGHTTP2_NO_ERROR); + + nghttp2_frame_headers_init(&frame.headers, NGHTTP2_FLAG_END_HEADERS, 1, + NGHTTP2_HCAT_REQUEST, NULL, NULL, 0); + + CU_ASSERT(NGHTTP2_ERR_IGN_HEADER_BLOCK == + nghttp2_session_on_request_headers_received(session, &frame)); + CU_ASSERT(session->goaway_flags & NGHTTP2_GOAWAY_TERM_ON_SEND); + + nghttp2_frame_headers_free(&frame.headers, mem); + + nghttp2_session_del(session); } void test_nghttp2_session_on_response_headers_received(void) { @@ -2822,14 +2883,20 @@ void test_nghttp2_session_on_push_promise_received(void) { CU_ASSERT(1 == session->num_incoming_reserved_streams); CU_ASSERT(NULL == nghttp2_session_get_stream(session, 4)); item = nghttp2_session_get_next_ob_item(session); - CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); - CU_ASSERT(4 == item->frame.hd.stream_id); - CU_ASSERT(NGHTTP2_PROTOCOL_ERROR == item->frame.rst_stream.error_code); + CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type); + CU_ASSERT(NGHTTP2_STREAM_CLOSED == item->frame.goaway.error_code); CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(4 == session->last_recv_stream_id); + nghttp2_session_del(session); + + nghttp2_session_client_new(&session, &callbacks, &user_data); + + stream = nghttp2_session_open_stream(session, 1, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_default, + NGHTTP2_STREAM_OPENING, NULL); + /* Attempt to PUSH_PROMISE against stream in closing state */ - stream->shut_flags = NGHTTP2_SHUT_NONE; stream->state = NGHTTP2_STREAM_CLOSING; frame.push_promise.promised_stream_id = 6; @@ -2839,7 +2906,7 @@ void test_nghttp2_session_on_push_promise_received(void) { nghttp2_session_on_push_promise_received(session, &frame)); CU_ASSERT(0 == user_data.begin_headers_cb_called); - CU_ASSERT(1 == session->num_incoming_reserved_streams); + CU_ASSERT(0 == session->num_incoming_reserved_streams); CU_ASSERT(NULL == nghttp2_session_get_stream(session, 6)); item = nghttp2_session_get_next_ob_item(session); CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); @@ -2847,7 +2914,7 @@ void test_nghttp2_session_on_push_promise_received(void) { CU_ASSERT(NGHTTP2_CANCEL == item->frame.rst_stream.error_code); CU_ASSERT(0 == nghttp2_session_send(session)); - /* Attempt to PUSH_PROMISE against non-existent stream */ + /* Attempt to PUSH_PROMISE against idle stream */ frame.hd.stream_id = 3; frame.push_promise.promised_stream_id = 8; @@ -2857,7 +2924,7 @@ void test_nghttp2_session_on_push_promise_received(void) { nghttp2_session_on_push_promise_received(session, &frame)); CU_ASSERT(0 == user_data.begin_headers_cb_called); - CU_ASSERT(1 == session->num_incoming_reserved_streams); + CU_ASSERT(0 == session->num_incoming_reserved_streams); CU_ASSERT(NULL == nghttp2_session_get_stream(session, 8)); item = nghttp2_session_get_next_ob_item(session); CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type); @@ -3241,6 +3308,53 @@ void test_nghttp2_session_on_data_received(void) { nghttp2_session_del(session); } +void test_nghttp2_session_on_data_received_fail_fast(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + uint8_t buf[9]; + nghttp2_stream *stream; + nghttp2_frame_hd hd; + nghttp2_outbound_item *item; + + memset(&callbacks, 0, sizeof(callbacks)); + + nghttp2_frame_hd_init(&hd, 0, NGHTTP2_DATA, NGHTTP2_FLAG_NONE, 1); + nghttp2_frame_pack_frame_hd(buf, &hd); + + nghttp2_session_server_new(&session, &callbacks, NULL); + + /* DATA to closed (remote) */ + stream = open_stream(session, 1); + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + + CU_ASSERT((ssize_t)sizeof(buf) == + nghttp2_session_mem_recv(session, buf, sizeof(buf))); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NULL != item); + CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type); + + nghttp2_session_del(session); + + nghttp2_session_server_new(&session, &callbacks, NULL); + + /* DATA to closed stream with explicit closed (remote) */ + stream = open_stream(session, 1); + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + nghttp2_session_close_stream(session, 1, NGHTTP2_NO_ERROR); + + CU_ASSERT((ssize_t)sizeof(buf) == + nghttp2_session_mem_recv(session, buf, sizeof(buf))); + + item = nghttp2_session_get_next_ob_item(session); + + CU_ASSERT(NULL != item); + CU_ASSERT(NGHTTP2_GOAWAY == item->frame.hd.type); + + nghttp2_session_del(session); +} + void test_nghttp2_session_send_headers_start_stream(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; @@ -3984,6 +4098,7 @@ void test_nghttp2_submit_request_without_data(void) { nva_out out; nghttp2_bufs bufs; nghttp2_mem *mem; + nghttp2_priority_spec pri_spec; mem = nghttp2_mem_default(); frame_pack_bufs_init(&bufs); @@ -4017,6 +4132,15 @@ void test_nghttp2_submit_request_without_data(void) { nghttp2_bufs_free(&bufs); nghttp2_hd_inflate_free(&inflater); + + /* Try to depend on itself is error */ + nghttp2_priority_spec_init(&pri_spec, (int32_t)session->next_stream_id, 16, + 0); + + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == + nghttp2_submit_request(session, &pri_spec, reqnv, ARRLEN(reqnv), + NULL, NULL)); + nghttp2_session_del(session); } @@ -4308,6 +4432,7 @@ void test_nghttp2_submit_headers(void) { nva_out out; nghttp2_bufs bufs; nghttp2_mem *mem; + nghttp2_priority_spec pri_spec; mem = nghttp2_mem_default(); frame_pack_bufs_init(&bufs); @@ -4362,6 +4487,21 @@ void test_nghttp2_submit_headers(void) { nghttp2_frame_headers_free(&frame.headers, mem); nghttp2_hd_inflate_free(&inflater); + + /* Try to depend on itself */ + nghttp2_priority_spec_init(&pri_spec, 3, 16, 0); + + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == + nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, 3, &pri_spec, + reqnv, ARRLEN(reqnv), NULL)); + + session->next_stream_id = 5; + nghttp2_priority_spec_init(&pri_spec, 5, 16, 0); + + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == + nghttp2_submit_headers(session, NGHTTP2_FLAG_NONE, -1, &pri_spec, + reqnv, ARRLEN(reqnv), NULL)); + nghttp2_session_del(session); } @@ -5318,38 +5458,6 @@ void test_nghttp2_session_max_concurrent_streams(void) { nghttp2_session_del(session); } -/* - * Check that on_stream_close_callback is called when server pushed - * HEADERS have NGHTTP2_FLAG_END_STREAM. - */ -void test_nghttp2_session_stream_close_on_headers_push(void) { - /* nghttp2_session *session; */ - /* nghttp2_session_callbacks callbacks; */ - /* const char *nv[] = { NULL }; */ - /* my_user_data ud; */ - /* nghttp2_frame frame; */ - - /* memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); */ - /* callbacks.on_stream_close_callback = */ - /* no_stream_user_data_stream_close_callback; */ - /* ud.stream_close_cb_called = 0; */ - - /* nghttp2_session_client_new(&session, NGHTTP2_PROTO_SPDY2, &callbacks, &ud); - */ - /* nghttp2_session_open_stream(session, 1, NGHTTP2_CTRL_FLAG_NONE, 3, */ - /* NGHTTP2_STREAM_OPENING, NULL); */ - /* nghttp2_frame_syn_stream_init(&frame.syn_stream, NGHTTP2_PROTO_SPDY2, */ - /* NGHTTP2_CTRL_FLAG_FIN | */ - /* NGHTTP2_CTRL_FLAG_UNIDIRECTIONAL, */ - /* 2, 1, 3, dup_nv(nv)); */ - - /* CU_ASSERT(0 == nghttp2_session_on_request_headers_received(session, - * &frame)); */ - - /* nghttp2_frame_syn_stream_free(&frame.syn_stream); */ - /* nghttp2_session_del(session); */ -} - void test_nghttp2_session_stop_data_with_rst_stream(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; @@ -7746,7 +7854,7 @@ void test_nghttp2_session_keep_closed_stream(void) { nghttp2_session_callbacks callbacks; const size_t max_concurrent_streams = 5; nghttp2_settings_entry iv = {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, - max_concurrent_streams}; + (uint32_t)max_concurrent_streams}; size_t i; memset(&callbacks, 0, sizeof(callbacks)); @@ -7781,6 +7889,7 @@ void test_nghttp2_session_keep_closed_stream(void) { CU_ASSERT(NULL == session->closed_stream_head->closed_prev); open_stream(session, 11); + nghttp2_session_adjust_closed_stream(session); CU_ASSERT(1 == session->num_closed_streams); CU_ASSERT(5 == session->closed_stream_tail->stream_id); @@ -7789,6 +7898,7 @@ void test_nghttp2_session_keep_closed_stream(void) { CU_ASSERT(NULL == session->closed_stream_head->closed_next); open_stream(session, 13); + nghttp2_session_adjust_closed_stream(session); CU_ASSERT(0 == session->num_closed_streams); CU_ASSERT(NULL == session->closed_stream_tail); @@ -7801,6 +7911,7 @@ void test_nghttp2_session_keep_closed_stream(void) { /* server initiated stream is not counted to max concurrent limit */ open_stream(session, 2); + nghttp2_session_adjust_closed_stream(session); CU_ASSERT(1 == session->num_closed_streams); CU_ASSERT(3 == session->closed_stream_head->stream_id); @@ -7818,8 +7929,9 @@ void test_nghttp2_session_keep_idle_stream(void) { nghttp2_session_callbacks callbacks; const size_t max_concurrent_streams = 1; nghttp2_settings_entry iv = {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, - max_concurrent_streams}; + (uint32_t)max_concurrent_streams}; int i; + int32_t stream_id; memset(&callbacks, 0, sizeof(callbacks)); callbacks.send_callback = null_send_callback; @@ -7828,25 +7940,29 @@ void test_nghttp2_session_keep_idle_stream(void) { nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, &iv, 1); - /* We at least allow 2 idle streams even if max concurrent streams - is very low. */ - for (i = 0; i < 2; ++i) { + /* We at least allow NGHTTP2_MIN_IDLE_STREAM idle streams even if + max concurrent streams is very low. */ + for (i = 0; i < NGHTTP2_MIN_IDLE_STREAMS; ++i) { nghttp2_session_open_stream(session, i * 2 + 1, NGHTTP2_STREAM_FLAG_NONE, &pri_spec_default, NGHTTP2_STREAM_IDLE, NULL); + nghttp2_session_adjust_idle_stream(session); } - CU_ASSERT(2 == session->num_idle_streams); + CU_ASSERT(NGHTTP2_MIN_IDLE_STREAMS == session->num_idle_streams); + stream_id = (NGHTTP2_MIN_IDLE_STREAMS - 1) * 2 + 1; CU_ASSERT(1 == session->idle_stream_head->stream_id); - CU_ASSERT(3 == session->idle_stream_tail->stream_id); + CU_ASSERT(stream_id == session->idle_stream_tail->stream_id); - nghttp2_session_open_stream(session, 5, NGHTTP2_FLAG_NONE, &pri_spec_default, - NGHTTP2_STREAM_IDLE, NULL); + stream_id += 2; - CU_ASSERT(2 == session->num_idle_streams); + nghttp2_session_open_stream(session, stream_id, NGHTTP2_FLAG_NONE, + &pri_spec_default, NGHTTP2_STREAM_IDLE, NULL); + nghttp2_session_adjust_idle_stream(session); + CU_ASSERT(NGHTTP2_MIN_IDLE_STREAMS == session->num_idle_streams); CU_ASSERT(3 == session->idle_stream_head->stream_id); - CU_ASSERT(5 == session->idle_stream_tail->stream_id); + CU_ASSERT(stream_id == session->idle_stream_tail->stream_id); nghttp2_session_del(session); } @@ -8639,7 +8755,7 @@ void test_nghttp2_session_flooding(void) { void test_nghttp2_session_change_stream_priority(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; - nghttp2_stream *stream1, *stream2, *stream3; + nghttp2_stream *stream1, *stream2, *stream3, *stream5; nghttp2_priority_spec pri_spec; int rv; @@ -8672,6 +8788,41 @@ void test_nghttp2_session_change_stream_priority(void) { rv = nghttp2_session_change_stream_priority(session, 0, &pri_spec); CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + /* Depends on the non-existing idle stream. This creates that idle + stream. */ + nghttp2_priority_spec_init(&pri_spec, 5, 9, 1); + + rv = nghttp2_session_change_stream_priority(session, 2, &pri_spec); + + CU_ASSERT(0 == rv); + + stream5 = nghttp2_session_get_stream_raw(session, 5); + + CU_ASSERT(NULL != stream5); + CU_ASSERT(&session->root == stream5->dep_prev); + CU_ASSERT(stream5 == stream2->dep_prev); + CU_ASSERT(9 == stream2->weight); + + nghttp2_session_del(session); + + /* Check that this works in client session too */ + nghttp2_session_client_new(&session, &callbacks, NULL); + + stream1 = open_stream(session, 1); + + nghttp2_priority_spec_init(&pri_spec, 5, 9, 1); + + rv = nghttp2_session_change_stream_priority(session, 1, &pri_spec); + + CU_ASSERT(0 == rv); + + stream5 = nghttp2_session_get_stream_raw(session, 5); + + CU_ASSERT(NULL != stream5); + CU_ASSERT(&session->root == stream5->dep_prev); + CU_ASSERT(stream5 == stream1->dep_prev); + CU_ASSERT(9 == stream1->weight); + nghttp2_session_del(session); } @@ -8681,8 +8832,10 @@ void test_nghttp2_session_create_idle_stream(void) { nghttp2_stream *stream2, *stream4, *stream8, *stream10; nghttp2_priority_spec pri_spec; int rv; + int i; memset(&callbacks, 0, sizeof(callbacks)); + callbacks.send_callback = null_send_callback; nghttp2_session_server_new(&session, &callbacks, NULL); @@ -8744,6 +8897,170 @@ void test_nghttp2_session_create_idle_stream(void) { CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); nghttp2_session_del(session); + + /* Check that this works in client session too */ + nghttp2_session_client_new(&session, &callbacks, NULL); + + nghttp2_priority_spec_init(&pri_spec, 4, 99, 1); + + rv = nghttp2_session_create_idle_stream(session, 2, &pri_spec); + + CU_ASSERT(0 == rv); + + stream4 = nghttp2_session_get_stream_raw(session, 4); + stream2 = nghttp2_session_get_stream_raw(session, 2); + + CU_ASSERT(NULL != stream4); + CU_ASSERT(NULL != stream2); + CU_ASSERT(&session->root == stream4->dep_prev); + CU_ASSERT(NGHTTP2_DEFAULT_WEIGHT == stream4->weight); + CU_ASSERT(stream4 == stream2->dep_prev); + CU_ASSERT(99 == stream2->weight); + + nghttp2_session_del(session); + + /* Check that idle stream is reduced when nghttp2_session_send() is + called. */ + nghttp2_session_server_new(&session, &callbacks, NULL); + + session->local_settings.max_concurrent_streams = 30; + + nghttp2_priority_spec_init(&pri_spec, 0, 16, 0); + for (i = 0; i < 100; ++i) { + rv = nghttp2_session_create_idle_stream(session, i * 2 + 1, &pri_spec); + + CU_ASSERT(0 == rv); + + nghttp2_priority_spec_init(&pri_spec, i * 2 + 1, 16, 0); + } + + CU_ASSERT(100 == session->num_idle_streams); + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(30 == session->num_idle_streams); + CU_ASSERT(141 == session->idle_stream_head->stream_id); + + nghttp2_session_del(session); + + /* Check that idle stream is reduced when nghttp2_session_mem_recv() is + called. */ + nghttp2_session_client_new(&session, &callbacks, NULL); + + session->local_settings.max_concurrent_streams = 30; + + nghttp2_priority_spec_init(&pri_spec, 0, 16, 0); + for (i = 0; i < 100; ++i) { + rv = nghttp2_session_create_idle_stream(session, i * 2 + 1, &pri_spec); + + CU_ASSERT(0 == rv); + + nghttp2_priority_spec_init(&pri_spec, i * 2 + 1, 16, 0); + } + + CU_ASSERT(100 == session->num_idle_streams); + CU_ASSERT(0 == nghttp2_session_mem_recv(session, NULL, 0)); + CU_ASSERT(30 == session->num_idle_streams); + CU_ASSERT(141 == session->idle_stream_head->stream_id); + + nghttp2_session_del(session); +} + +void test_nghttp2_session_repeated_priority_change(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_frame frame; + nghttp2_priority_spec pri_spec; + int32_t stream_id, last_stream_id; + int32_t max_streams = 20; + + memset(&callbacks, 0, sizeof(callbacks)); + + nghttp2_session_server_new(&session, &callbacks, NULL); + + session->local_settings.max_concurrent_streams = (uint32_t)max_streams; + + /* 1 -> 0 */ + nghttp2_priority_spec_init(&pri_spec, 0, 16, 0); + nghttp2_frame_priority_init(&frame.priority, 1, &pri_spec); + + CU_ASSERT(0 == nghttp2_session_on_priority_received(session, &frame)); + + nghttp2_frame_priority_free(&frame.priority); + + last_stream_id = max_streams * 2 + 1; + + for (stream_id = 3; stream_id < last_stream_id; stream_id += 2) { + /* 1 -> stream_id */ + nghttp2_priority_spec_init(&pri_spec, stream_id, 16, 0); + nghttp2_frame_priority_init(&frame.priority, 1, &pri_spec); + + CU_ASSERT(0 == nghttp2_session_on_priority_received(session, &frame)); + + nghttp2_frame_priority_free(&frame.priority); + } + + CU_ASSERT(20 == session->num_idle_streams); + CU_ASSERT(1 == session->idle_stream_head->stream_id); + + /* 1 -> last_stream_id */ + nghttp2_priority_spec_init(&pri_spec, last_stream_id, 16, 0); + nghttp2_frame_priority_init(&frame.priority, 1, &pri_spec); + + CU_ASSERT(0 == nghttp2_session_on_priority_received(session, &frame)); + + nghttp2_frame_priority_free(&frame.priority); + + CU_ASSERT(20 == session->num_idle_streams); + CU_ASSERT(3 == session->idle_stream_head->stream_id); + + nghttp2_session_del(session); +} + +void test_nghttp2_session_repeated_priority_submission(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_priority_spec pri_spec; + int32_t stream_id, last_stream_id; + uint32_t max_streams = NGHTTP2_MIN_IDLE_STREAMS; + + memset(&callbacks, 0, sizeof(callbacks)); + + callbacks.send_callback = null_send_callback; + + nghttp2_session_client_new(&session, &callbacks, NULL); + + session->local_settings.max_concurrent_streams = max_streams; + + /* 1 -> 0 */ + nghttp2_priority_spec_init(&pri_spec, 0, 16, 0); + + CU_ASSERT(0 == + nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec)); + + last_stream_id = (int32_t)(max_streams * 2 + 1); + + for (stream_id = 3; stream_id < last_stream_id; stream_id += 2) { + /* 1 -> stream_id */ + nghttp2_priority_spec_init(&pri_spec, stream_id, 16, 0); + + CU_ASSERT( + 0 == nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec)); + } + + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(max_streams == session->num_idle_streams); + CU_ASSERT(1 == session->idle_stream_head->stream_id); + + /* 1 -> last_stream_id */ + nghttp2_priority_spec_init(&pri_spec, last_stream_id, 16, 0); + + CU_ASSERT(0 == + nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec)); + + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(max_streams == session->num_idle_streams); + CU_ASSERT(3 == session->idle_stream_head->stream_id); + + nghttp2_session_del(session); } static void check_nghttp2_http_recv_headers_fail( diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index 9b6b2228..3d1d8f28 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -34,6 +34,7 @@ void test_nghttp2_session_recv_data_no_auto_flow_control(void); void test_nghttp2_session_recv_continuation(void); void test_nghttp2_session_recv_headers_with_priority(void); void test_nghttp2_session_recv_headers_early_response(void); +void test_nghttp2_session_server_recv_push_response(void); void test_nghttp2_session_recv_premature_headers(void); void test_nghttp2_session_recv_unknown_frame(void); void test_nghttp2_session_recv_unexpected_continuation(void); @@ -54,6 +55,7 @@ void test_nghttp2_session_on_ping_received(void); void test_nghttp2_session_on_goaway_received(void); void test_nghttp2_session_on_window_update_received(void); void test_nghttp2_session_on_data_received(void); +void test_nghttp2_session_on_data_received_fail_fast(void); void test_nghttp2_session_send_headers_start_stream(void); void test_nghttp2_session_send_headers_reply(void); void test_nghttp2_session_send_headers_frame_size_error(void); @@ -95,7 +97,6 @@ void test_nghttp2_session_get_next_ob_item(void); void test_nghttp2_session_pop_next_ob_item(void); void test_nghttp2_session_reply_fail(void); void test_nghttp2_session_max_concurrent_streams(void); -void test_nghttp2_session_stream_close_on_headers_push(void); void test_nghttp2_session_stop_data_with_rst_stream(void); void test_nghttp2_session_defer_data(void); void test_nghttp2_session_flow_control(void); @@ -139,6 +140,8 @@ void test_nghttp2_session_detach_item_from_closed_stream(void); void test_nghttp2_session_flooding(void); void test_nghttp2_session_change_stream_priority(void); void test_nghttp2_session_create_idle_stream(void); +void test_nghttp2_session_repeated_priority_change(void); +void test_nghttp2_session_repeated_priority_submission(void); void test_nghttp2_http_mandatory_headers(void); void test_nghttp2_http_content_length(void); void test_nghttp2_http_content_length_mismatch(void); diff --git a/third-party/neverbleed b/third-party/neverbleed index 81eff20b..5c47587b 160000 --- a/third-party/neverbleed +++ b/third-party/neverbleed @@ -1 +1 @@ -Subproject commit 81eff20bd84b4d0dce2cbbd1a5ad1384d086423b +Subproject commit 5c47587bc2855f2b9577a9bd369ed70088b77fec