diff --git a/.travis.yml b/.travis.yml index 1dee67b8..7239bd90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,12 +27,26 @@ before_install: - $CC --version - if [ "$CXX" = "g++" ]; then export CXX="g++-4.9" CC="gcc-4.9"; fi - $CC --version + - go version before_script: + # First build spdylay, since integration tests require it. + # spdylay is going to be built under third-party/spdylay + - cd third-party + - git clone https://github.com/tatsuhiro-t/spdylay.git + - cd spdylay + - autoreconf -i + - ./configure --disable-src --disable-examples + - make check + - export SPDYLAY_HOME=$PWD + - cd ../.. + # Now build nghttp2 - autoreconf -i - - automake - - autoconf - git submodule update --init - - ./configure --enable-werror --with-mruby + - ./configure --enable-werror --with-mruby --with-neverbleed LIBSPDYLAY_CFLAGS="-I$SPDYLAY_HOME/lib/includes" LIBSPDYLAY_LIBS="-L$SPDYLAY_HOME/lib/.libs -lspdylay" script: - make - make check + - cd integration-tests + - export GOPATH="$PWD/integration-tests/golang" + - make itprep-local + - make it-local diff --git a/configure.ac b/configure.ac index 69c50ddd..6bc25f4d 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.4.1-DEV], [t-tujikawa@users.sourceforge.net]) +AC_INIT([nghttp2], [1.5.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, 16) +AC_SUBST(LT_CURRENT, 17) AC_SUBST(LT_REVISION, 0) -AC_SUBST(LT_AGE, 2) +AC_SUBST(LT_AGE, 3) 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"` diff --git a/doc/Makefile.am b/doc/Makefile.am index 0d223476..065b4c24 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -89,6 +89,7 @@ APIDOCS= \ nghttp2_session_consume.rst \ nghttp2_session_consume_connection.rst \ nghttp2_session_consume_stream.rst \ + nghttp2_session_create_idle_stream.rst \ nghttp2_session_del.rst \ nghttp2_session_find_stream.rst \ nghttp2_session_get_effective_local_window_size.rst \ @@ -108,7 +109,9 @@ APIDOCS= \ nghttp2_session_mem_recv.rst \ nghttp2_session_mem_send.rst \ nghttp2_session_recv.rst \ + nghttp2_session_change_stream_priority.rst \ nghttp2_session_check_request_allowed.rst \ + nghttp2_session_check_server_session.rst \ nghttp2_session_resume_data.rst \ nghttp2_session_send.rst \ nghttp2_session_server_new.rst \ diff --git a/doc/bash_completion/h2load b/doc/bash_completion/h2load index f63fc4a4..9746d3a4 100644 --- a/doc/bash_completion/h2load +++ b/doc/bash_completion/h2load @@ -8,7 +8,7 @@ _h2load() _get_comp_words_by_ref cur prev case $cur in -*) - COMPREPLY=( $( compgen -W '--threads --connection-window-bits --rate-period --input-file --npn-list --help --requests --timing-script-file --data --verbose --base-uri --ciphers --connection-active-timeout --rate --connection-inactivity-timeout --window-bits --clients --no-tls-proto --version --header --max-concurrent-streams ' -- "$cur" ) ) + COMPREPLY=( $( compgen -W '--connection-window-bits --clients --verbose --ciphers --rate --no-tls-proto --requests --base-uri --h1 --threads --npn-list --rate-period --data --version --connection-inactivity-timeout --timing-script-file --max-concurrent-streams --connection-active-timeout --input-file --header --window-bits --help ' -- "$cur" ) ) ;; *) _filedir diff --git a/doc/bash_completion/nghttpd b/doc/bash_completion/nghttpd index 6de3d267..7bbef245 100644 --- a/doc/bash_completion/nghttpd +++ b/doc/bash_completion/nghttpd @@ -8,7 +8,7 @@ _nghttpd() _get_comp_words_by_ref cur prev case $cur in -*) - COMPREPLY=( $( compgen -W '--error-gzip --push --header-table-size --trailer --htdocs --address --padding --verbose --version --help --hexdump --dh-param-file --daemon --verify-client --echo-upload --workers --no-tls --color --early-response --max-concurrent-streams ' -- "$cur" ) ) + COMPREPLY=( $( compgen -W '--mime-types-file --error-gzip --push --header-table-size --trailer --htdocs --address --padding --verbose --version --help --hexdump --dh-param-file --daemon --verify-client --echo-upload --workers --no-tls --color --early-response --max-concurrent-streams ' -- "$cur" ) ) ;; *) _filedir diff --git a/doc/h2load.1 b/doc/h2load.1 index 4e639968..241d6505 100644 --- a/doc/h2load.1 +++ b/doc/h2load.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "H2LOAD" "1" "November 12, 2015" "1.4.1-DEV" "nghttp2" +.TH "H2LOAD" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" .SH NAME h2load \- HTTP/2 benchmarking tool . @@ -227,6 +227,13 @@ Default: \fBh2,h2\-16,h2\-14,spdy/3.1,spdy/3,spdy/2,http/1.1\fP .UNINDENT .INDENT 0.0 .TP +.B \-\-h1 +Short hand for \fI\%\-\-npn\-list\fP=http/1.1 +\fI\%\-\-no\-tls\-proto\fP=http/1.1, which effectively force +http/1.1 for both http and https URI. +.UNINDENT +.INDENT 0.0 +.TP .B \-v, \-\-verbose Output debug information. .UNINDENT diff --git a/doc/h2load.1.rst b/doc/h2load.1.rst index 5a9f8321..448c4276 100644 --- a/doc/h2load.1.rst +++ b/doc/h2load.1.rst @@ -188,6 +188,12 @@ OPTIONS Default: ``h2,h2-16,h2-14,spdy/3.1,spdy/3,spdy/2,http/1.1`` +.. option:: --h1 + + Short hand for :option:`--npn-list`\=http/1.1 + :option:`--no-tls-proto`\=http/1.1, which effectively force + http/1.1 for both http and https URI. + .. option:: -v, --verbose Output debug information. diff --git a/doc/h2load.h2r b/doc/h2load.h2r index 60826931..e022aa8d 100644 --- a/doc/h2load.h2r +++ b/doc/h2load.h2r @@ -86,6 +86,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 (client) + 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/nghttp.1 b/doc/nghttp.1 index 1029cb57..b246da69 100644 --- a/doc/nghttp.1 +++ b/doc/nghttp.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "NGHTTP" "1" "November 12, 2015" "1.4.1-DEV" "nghttp2" +.TH "NGHTTP" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" .SH NAME nghttp \- HTTP/2 client . diff --git a/doc/nghttpd.1 b/doc/nghttpd.1 index 06ac32d6..ad8d93da 100644 --- a/doc/nghttpd.1 +++ b/doc/nghttpd.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "NGHTTPD" "1" "November 12, 2015" "1.4.1-DEV" "nghttp2" +.TH "NGHTTPD" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" .SH NAME nghttpd \- HTTP/2 server . diff --git a/doc/nghttpx.1 b/doc/nghttpx.1 index 8c0a577f..a0982c49 100644 --- a/doc/nghttpx.1 +++ b/doc/nghttpx.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "NGHTTPX" "1" "November 12, 2015" "1.4.1-DEV" "nghttp2" +.TH "NGHTTPX" "1" "November 29, 2015" "1.5.1-DEV" "nghttp2" .SH NAME nghttpx \- HTTP/2 proxy . @@ -685,9 +685,14 @@ protocol security. .INDENT 0.0 .TP .B \-\-no\-server\-push -Disable HTTP/2 server push. Server push is only -supported by default mode and HTTP/2 frontend. SPDY -frontend does not support server push. +Disable HTTP/2 server push. Server push is supported by +default mode and HTTP/2 frontend via Link header field. +It is also supported if both frontend and backend are +HTTP/2 (which implies \fI\%\-\-http2\-bridge\fP or \fI\%\-\-client\fP mode). +In this case, server push from backend session is +relayed to frontend, and server push via Link header +field is also supported. HTTP SPDY frontend does not +support server push. .UNINDENT .SS Mode .INDENT 0.0 @@ -1053,8 +1058,8 @@ than master process. .UNINDENT .SH SERVER PUSH .sp -nghttpx supports HTTP/2 server push in default mode. nghttpx looks -for Link header field (\fI\%RFC 5988\fP) in response headers from +nghttpx supports HTTP/2 server push in default mode with Link header +field. nghttpx looks for Link header field (\fI\%RFC 5988\fP) in response headers from backend server and extracts URI\-reference with parameter \fBrel=preload\fP (see \fI\%preload\fP) and pushes those URIs to the frontend client. Here is a sample Link @@ -1079,6 +1084,14 @@ associated stream\(aqs status code must be 200. .UNINDENT .sp This limitation may be loosened in the future release. +.sp +nghttpx also supports server push if both frontend and backend are +HTTP/2 (which implies \fI\%\-\-http2\-bridge\fP or \fI\%\-\-client\fP). +In this case, in addition to server push via Link header field, server +push from backend is relayed to frontend HTTP/2 session. +.sp +HTTP/2 server push will be disabled if \fI\%\-\-http2\-proxy\fP or +\fI\%\-\-client\-proxy\fP is used. .SH UNIX DOMAIN SOCKET .sp nghttpx supports UNIX domain socket with a filename for both frontend diff --git a/doc/nghttpx.1.rst b/doc/nghttpx.1.rst index 33b7b4f2..e87377cd 100644 --- a/doc/nghttpx.1.rst +++ b/doc/nghttpx.1.rst @@ -613,9 +613,14 @@ HTTP/2 and SPDY .. option:: --no-server-push - Disable HTTP/2 server push. Server push is only - supported by default mode and HTTP/2 frontend. SPDY - frontend does not support server push. + Disable HTTP/2 server push. Server push is supported by + default mode and HTTP/2 frontend via Link header field. + It is also supported if both frontend and backend are + HTTP/2 (which implies :option:`--http2-bridge` or :option:`\--client` mode). + In this case, server push from backend session is + relayed to frontend, and server push via Link header + field is also supported. HTTP SPDY frontend does not + support server push. Mode @@ -954,8 +959,8 @@ SIGUSR2 SERVER PUSH ----------- -nghttpx supports HTTP/2 server push in default mode. nghttpx looks -for Link header field (`RFC 5988 +nghttpx supports HTTP/2 server push in default mode with Link header +field. nghttpx looks for Link header field (`RFC 5988 `_) in response headers from backend server and extracts URI-reference with parameter ``rel=preload`` (see `preload @@ -975,6 +980,14 @@ Currently, the following restriction is applied for server push: This limitation may be loosened in the future release. +nghttpx also supports server push if both frontend and backend are +HTTP/2 (which implies :option:`--http2-bridge` or :option:`--client`). +In this case, in addition to server push via Link header field, server +push from backend is relayed to frontend HTTP/2 session. + +HTTP/2 server push will be disabled if :option:`--http2-proxy` or +:option:`--client-proxy` is used. + UNIX DOMAIN SOCKET ------------------ @@ -1035,7 +1048,9 @@ has to deploy key generator program to update keys frequently (e.g., every 1 hour). The example key generator tlsticketupdate.go is available under contrib directory in nghttp2 archive. The memcached entry key is ``nghttpx:tls-ticket-key``. The data format stored in -memcached is the binary format described below:: +memcached is the binary format described below: + +.. code-block:: text +--------------+-------+----------------+ | VERSION (4) |LEN (2)|KEY(48 or 80) ... diff --git a/doc/nghttpx.h2r b/doc/nghttpx.h2r index 656baddb..abbc4c9a 100644 --- a/doc/nghttpx.h2r +++ b/doc/nghttpx.h2r @@ -60,8 +60,8 @@ SIGUSR2 SERVER PUSH ----------- -nghttpx supports HTTP/2 server push in default mode. nghttpx looks -for Link header field (`RFC 5988 +nghttpx supports HTTP/2 server push in default mode with Link header +field. nghttpx looks for Link header field (`RFC 5988 `_) in response headers from backend server and extracts URI-reference with parameter ``rel=preload`` (see `preload @@ -81,6 +81,14 @@ Currently, the following restriction is applied for server push: This limitation may be loosened in the future release. +nghttpx also supports server push if both frontend and backend are +HTTP/2 (which implies :option:`--http2-bridge` or :option:`--client`). +In this case, in addition to server push via Link header field, server +push from backend is relayed to frontend HTTP/2 session. + +HTTP/2 server push will be disabled if :option:`--http2-proxy` or +:option:`--client-proxy` is used. + UNIX DOMAIN SOCKET ------------------ @@ -141,7 +149,9 @@ has to deploy key generator program to update keys frequently (e.g., every 1 hour). The example key generator tlsticketupdate.go is available under contrib directory in nghttp2 archive. The memcached entry key is ``nghttpx:tls-ticket-key``. The data format stored in -memcached is the binary format described below:: +memcached is the binary format described below: + +.. code-block:: text +--------------+-------+----------------+ | VERSION (4) |LEN (2)|KEY(48 or 80) ... diff --git a/lib/includes/nghttp2/nghttp2.h b/lib/includes/nghttp2/nghttp2.h index 09e2dba5..104159fc 100644 --- a/lib/includes/nghttp2/nghttp2.h +++ b/lib/includes/nghttp2/nghttp2.h @@ -613,7 +613,16 @@ typedef enum { /** * @macro - * Default maximum concurrent streams. + * + * Default maximum number of incoming concurrent streams. Use + * `nghttp2_submit_settings()` with + * :enum:`NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS` to change the + * maximum number of incoming concurrent streams. + * + * .. note:: + * + * The maximum number of outgoing concurrent streams is 100 by + * default. */ #define NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS ((1U << 31) - 1) @@ -2988,6 +2997,85 @@ NGHTTP2_EXTERN int nghttp2_session_consume_stream(nghttp2_session *session, int32_t stream_id, size_t size); +/** + * @function + * + * Changes priority of existing stream denoted by |stream_id|. The + * new priority specification is |pri_spec|. + * + * The priority is changed silently and instantly, and no PRIORITY + * frame will be sent to notify the peer of this change. This + * function may be useful for server to change the priority of pushed + * stream. + * + * If |session| is initialized as server, and ``pri_spec->stream_id`` + * points to the idle stream, the idle stream is created if it does + * not exist. The created idle stream will depend on root stream + * (stream 0) with weight 16. + * + * Otherwise, if stream denoted by ``pri_spec->stream_id`` is not + * found, we use default priority instead of given |pri_spec|. That + * is make stream depend on root stream with weight 16. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * :enum:`NGHTTP2_ERR_NOMEM` + * Out of memory. + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * Attempted to depend on itself; or no stream exist for the given + * |stream_id|; or |stream_id| is 0 + */ +NGHTTP2_EXTERN int +nghttp2_session_change_stream_priority(nghttp2_session *session, + int32_t stream_id, + const nghttp2_priority_spec *pri_spec); + +/** + * @function + * + * Creates idle stream with the given |stream_id|, and priority + * |pri_spec|. + * + * The stream creation is done without sending PRIORITY frame, which + * means that peer does not know about the existence of this idle + * stream in the local endpoint. + * + * RFC 7540 does not disallow the use of creation of idle stream with + * odd or even stream ID regardless of client or server. So this + * function can create odd or even stream ID regardless of client or + * server. But probably it is a bit safer to use the stream ID the + * local endpoint can initiate (in other words, use odd stream ID for + * client, and even stream ID for server), to avoid potential + * collision from peer's instruction. Also we can use + * `nghttp2_session_set_next_stream_id()` to avoid to open created + * idle streams accidentally if we follow this recommendation. + * + * If |session| is initialized as server, and ``pri_spec->stream_id`` + * points to the idle stream, the idle stream is created if it does + * not exist. The created idle stream will depend on root stream + * (stream 0) with weight 16. + * + * Otherwise, if stream denoted by ``pri_spec->stream_id`` is not + * found, we use default priority instead of given |pri_spec|. That + * is make stream depend on root stream with weight 16. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * :enum:`NGHTTP2_ERR_NOMEM` + * Out of memory. + * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` + * Attempted to depend on itself; or stream denoted by |stream_id| + * already exists; or |stream_id| cannot be used to create idle + * stream (in other words, local endpoint has already opened + * stream ID greater than or equal to the given stream ID; or + * |stream_id| is 0 + */ +NGHTTP2_EXTERN int +nghttp2_session_create_idle_stream(nghttp2_session *session, int32_t stream_id, + const nghttp2_priority_spec *pri_spec); + /** * @function * @@ -3629,14 +3717,20 @@ NGHTTP2_EXTERN int nghttp2_submit_settings(nghttp2_session *session, * :enum:`NGHTTP2_ERR_INVALID_ARGUMENT` * 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. * * .. warning:: * * This function returns assigned promised stream ID if it succeeds. - * But that stream is not opened yet. The application must not - * submit frame to that stream ID before - * :type:`nghttp2_before_frame_send_callback` is called for this - * frame. + * As of 1.16.0, stream object for pushed resource is created when + * this function succeeds. In that case, the application can submit + * push response for the promised frame. + * + * In 1.15.0 or prior versions, pushed stream is not opened yet when + * this function succeeds. The application must not submit frame to + * that stream ID before :type:`nghttp2_before_frame_send_callback` + * is called for this frame. * */ NGHTTP2_EXTERN int32_t @@ -3751,6 +3845,14 @@ nghttp2_session_get_last_proc_stream_id(nghttp2_session *session); NGHTTP2_EXTERN int nghttp2_session_check_request_allowed(nghttp2_session *session); +/** + * @function + * + * Returns nonzero if |session| is initialized as server side session. + */ +NGHTTP2_EXTERN int +nghttp2_session_check_server_session(nghttp2_session *session); + /** * @function * diff --git a/lib/nghttp2_priority_spec.c b/lib/nghttp2_priority_spec.c index cd254b1f..c2196e30 100644 --- a/lib/nghttp2_priority_spec.c +++ b/lib/nghttp2_priority_spec.c @@ -42,3 +42,11 @@ int nghttp2_priority_spec_check_default(const nghttp2_priority_spec *pri_spec) { return pri_spec->stream_id == 0 && pri_spec->weight == NGHTTP2_DEFAULT_WEIGHT && pri_spec->exclusive == 0; } + +void nghttp2_priority_spec_normalize_weight(nghttp2_priority_spec *pri_spec) { + if (pri_spec->weight < NGHTTP2_MIN_WEIGHT) { + pri_spec->weight = NGHTTP2_MIN_WEIGHT; + } else if (pri_spec->weight > NGHTTP2_MAX_WEIGHT) { + pri_spec->weight = NGHTTP2_MAX_WEIGHT; + } +} diff --git a/lib/nghttp2_priority_spec.h b/lib/nghttp2_priority_spec.h index a325a581..98fac210 100644 --- a/lib/nghttp2_priority_spec.h +++ b/lib/nghttp2_priority_spec.h @@ -31,4 +31,12 @@ #include +/* + * This function normalizes pri_spec->weight if it is out of range. + * If pri_spec->weight is less than NGHTTP2_MIN_WEIGHT, it is set to + * NGHTTP2_MIN_WEIGHT. If pri_spec->weight is larger than + * NGHTTP2_MAX_WEIGHT, it is set to NGHTTP2_MAX_WEIGHT. + */ +void nghttp2_priority_spec_normalize_weight(nghttp2_priority_spec *pri_spec); + #endif /* NGHTTP2_PRIORITY_SPEC_H */ diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 84f90a5b..1946560d 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -372,6 +372,9 @@ static int session_new(nghttp2_session **session_ptr, (*session_ptr)->max_incoming_reserved_streams = NGHTTP2_MAX_INCOMING_RESERVED_STREAMS; + /* Limit max outgoing concurrent streams to sensible value */ + (*session_ptr)->remote_settings.max_concurrent_streams = 100; + if (option) { if ((option->opt_set_mask & NGHTTP2_OPT_NO_AUTO_WINDOW_UPDATE) && option->no_auto_window_update) { @@ -665,6 +668,21 @@ 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; + return 0; + } + nghttp2_stream_dep_remove_subtree(stream); /* We have to update weight after removing stream from tree */ @@ -740,6 +758,31 @@ int nghttp2_session_add_item(nghttp2_session *session, nghttp2_outbound_queue_push(&session->ob_reg, item); item->queued = 1; break; + case NGHTTP2_PUSH_PROMISE: { + nghttp2_headers_aux_data *aux_data; + nghttp2_priority_spec pri_spec; + + aux_data = &item->aux_data.headers; + + if (!stream) { + return NGHTTP2_ERR_STREAM_CLOSED; + } + + nghttp2_priority_spec_init(&pri_spec, stream->stream_id, + NGHTTP2_DEFAULT_WEIGHT, 0); + + if (!nghttp2_session_open_stream( + session, frame->push_promise.promised_stream_id, + NGHTTP2_STREAM_FLAG_NONE, &pri_spec, NGHTTP2_STREAM_RESERVED, + aux_data->stream_user_data)) { + return NGHTTP2_ERR_NOMEM; + } + + nghttp2_outbound_queue_push(&session->ob_reg, item); + item->queued = 1; + + break; + } case NGHTTP2_WINDOW_UPDATE: if (stream) { stream->window_update_queued = 1; @@ -965,15 +1008,6 @@ nghttp2_stream *nghttp2_session_open_stream(nghttp2_session *session, } } - /* We don't have to track dependency of received reserved stream */ - if (stream->shut_flags & NGHTTP2_SHUT_WR) { - return stream; - } - - /* TODO Client does not have to track dependencies of streams except - for those which have upload data. Currently, we just track - everything. */ - if (pri_spec->stream_id == 0) { dep_stream = &session->root; } @@ -997,6 +1031,7 @@ int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id, int rv; nghttp2_stream *stream; nghttp2_mem *mem; + int is_my_stream_id; mem = &session->mem; stream = nghttp2_session_get_stream(session, stream_id); @@ -1044,14 +1079,16 @@ int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id, } } + is_my_stream_id = nghttp2_session_is_my_stream_id(session, stream_id); + /* pushed streams which is not opened yet is not counted toward max concurrent limits */ if ((stream->flags & NGHTTP2_STREAM_FLAG_PUSH)) { - if (!nghttp2_session_is_my_stream_id(session, stream_id)) { + if (!is_my_stream_id) { --session->num_incoming_reserved_streams; } } else { - if (nghttp2_session_is_my_stream_id(session, stream_id)) { + if (is_my_stream_id) { --session->num_outgoing_streams; } else { --session->num_incoming_streams; @@ -1061,7 +1098,8 @@ int nghttp2_session_close_stream(nghttp2_session *session, int32_t stream_id, /* Closes both directions just in case they are not closed yet */ stream->flags |= NGHTTP2_STREAM_FLAG_CLOSED; - if (session->server && nghttp2_stream_in_dep_tree(stream)) { + if (session->server && !is_my_stream_id && + nghttp2_stream_in_dep_tree(stream)) { /* On server side, retain stream at most MAX_CONCURRENT_STREAMS combined with the current active incoming streams to make dependency tree work better. */ @@ -1420,6 +1458,9 @@ static int session_predicate_response_headers_send(nghttp2_session *session, * RST_STREAM was queued for this stream. * NGHTTP2_ERR_SESSION_CLOSING * This session is closing. + * NGHTTP2_ERR_START_STREAM_NOT_ALLOWED + * New stream cannot be created because GOAWAY is already sent or + * received. */ static int session_predicate_push_response_headers_send(nghttp2_session *session, @@ -1437,6 +1478,9 @@ session_predicate_push_response_headers_send(nghttp2_session *session, if (stream->state == NGHTTP2_STREAM_CLOSING) { return NGHTTP2_ERR_STREAM_CLOSING; } + if (session->goaway_flags & NGHTTP2_GOAWAY_RECV) { + return NGHTTP2_ERR_START_STREAM_NOT_ALLOWED; + } return 0; } @@ -1835,40 +1879,41 @@ static int session_prep_frame(nghttp2_session *session, stream = nghttp2_session_get_stream(session, frame->hd.stream_id); - if (session_predicate_push_response_headers_send(session, stream) == - 0) { - frame->headers.cat = NGHTTP2_HCAT_PUSH_RESPONSE; + if (stream && stream->state == NGHTTP2_STREAM_RESERVED) { + rv = session_predicate_push_response_headers_send(session, stream); + if (rv == 0) { + frame->headers.cat = NGHTTP2_HCAT_PUSH_RESPONSE; - if (aux_data->stream_user_data) { - stream->stream_user_data = aux_data->stream_user_data; + if (aux_data->stream_user_data) { + stream->stream_user_data = aux_data->stream_user_data; + } } } else if (session_predicate_response_headers_send(session, stream) == 0) { frame->headers.cat = NGHTTP2_HCAT_RESPONSE; + rv = 0; } else { frame->headers.cat = NGHTTP2_HCAT_HEADERS; rv = session_predicate_headers_send(session, stream); + } - if (rv != 0) { - // If stream was alreay 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); - if (stream && stream->item == item) { - int rv2; + if (rv != 0) { + // If stream was alreay 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); + if (stream && stream->item == item) { + int rv2; - rv2 = nghttp2_stream_detach_item(stream); + rv2 = nghttp2_stream_detach_item(stream); - if (nghttp2_is_fatal(rv2)) { - return rv2; - } + if (nghttp2_is_fatal(rv2)) { + return rv2; } - - return rv; } + + return rv; } } @@ -1930,30 +1975,8 @@ static int session_prep_frame(nghttp2_session *session, } case NGHTTP2_PUSH_PROMISE: { nghttp2_stream *stream; - nghttp2_headers_aux_data *aux_data; - nghttp2_priority_spec pri_spec; size_t estimated_payloadlen; - aux_data = &item->aux_data.headers; - - stream = nghttp2_session_get_stream(session, frame->hd.stream_id); - - /* stream could be NULL if associated stream was already - closed. */ - if (stream) { - nghttp2_priority_spec_init(&pri_spec, stream->stream_id, - NGHTTP2_DEFAULT_WEIGHT, 0); - } else { - nghttp2_priority_spec_default_init(&pri_spec); - } - - if (!nghttp2_session_open_stream( - session, frame->push_promise.promised_stream_id, - NGHTTP2_STREAM_FLAG_NONE, &pri_spec, NGHTTP2_STREAM_RESERVED, - aux_data->stream_user_data)) { - return NGHTTP2_ERR_NOMEM; - } - estimated_payloadlen = session_estimate_headers_payload( session, frame->push_promise.nva, frame->push_promise.nvlen, 0); @@ -1961,6 +1984,10 @@ static int session_prep_frame(nghttp2_session *session, return NGHTTP2_ERR_FRAME_SIZE_ERROR; } + /* stream could be NULL if associated stream was already + closed. */ + stream = nghttp2_session_get_stream(session, frame->hd.stream_id); + /* predicte should fail if stream is NULL. */ rv = session_predicate_push_promise_send(session, stream); if (rv != 0) { @@ -2243,16 +2270,19 @@ static int session_close_stream_on_goaway(nghttp2_session *session, nghttp2_stream *stream, *next_stream; nghttp2_close_stream_on_goaway_arg arg = {session, NULL, last_stream_id, incoming}; + uint32_t error_code; rv = nghttp2_map_each(&session->streams, find_stream_on_goaway_func, &arg); assert(rv == 0); + error_code = + session->server && incoming ? NGHTTP2_REFUSED_STREAM : NGHTTP2_CANCEL; + stream = arg.head; while (stream) { next_stream = stream->closed_next; stream->closed_next = NULL; - rv = nghttp2_session_close_stream(session, stream->stream_id, - NGHTTP2_REFUSED_STREAM); + rv = nghttp2_session_close_stream(session, stream->stream_id, error_code); /* stream may be deleted here */ @@ -3360,9 +3390,7 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame, } /* - * Decompress header blocks of incoming request HEADERS and also call - * additional callbacks. This function can be called again if this - * function returns NGHTTP2_ERR_PAUSE. + * Call this function when HEADERS frame was completely received. * * This function returns 0 if it succeeds, or one of negative error * codes: @@ -3372,71 +3400,20 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame, * NGHTTP2_ERR_NOMEM * Out of memory. */ -int nghttp2_session_end_request_headers_received(nghttp2_session *session _U_, - nghttp2_frame *frame, - nghttp2_stream *stream) { - if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { - nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); - } - /* Here we assume that stream is not shutdown in NGHTTP2_SHUT_WR */ - return 0; -} - -/* - * Decompress header blocks of incoming (push-)response HEADERS and - * also call additional callbacks. This function can be called again - * if this function returns NGHTTP2_ERR_PAUSE. - * - * This function returns 0 if it succeeds, or one of negative error - * codes: - * - * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. - * NGHTTP2_ERR_NOMEM - * Out of memory. - */ -int nghttp2_session_end_response_headers_received(nghttp2_session *session, - nghttp2_frame *frame, - nghttp2_stream *stream) { +static int session_end_stream_headers_received(nghttp2_session *session, + nghttp2_frame *frame, + nghttp2_stream *stream) { int rv; - if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { - /* This is the last frame of this stream, so disallow - further receptions. */ - nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); - rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream); - if (nghttp2_is_fatal(rv)) { - return rv; - } + if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { + return 0; } - return 0; -} -/* - * Decompress header blocks of incoming HEADERS and also call - * additional callbacks. This function can be called again if this - * function returns NGHTTP2_ERR_PAUSE. - * - * This function returns 0 if it succeeds, or one of negative error - * codes: - * - * NGHTTP2_ERR_CALLBACK_FAILURE - * The callback function failed. - * NGHTTP2_ERR_NOMEM - * Out of memory. - */ -int nghttp2_session_end_headers_received(nghttp2_session *session, - nghttp2_frame *frame, - nghttp2_stream *stream) { - int rv; - if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { - if (!nghttp2_session_is_my_stream_id(session, frame->hd.stream_id)) { - } - nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); - rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream); - if (nghttp2_is_fatal(rv)) { - return rv; - } + nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); + rv = nghttp2_session_close_stream_if_shut_rdwr(session, stream); + if (nghttp2_is_fatal(rv)) { + return rv; } + return 0; } @@ -3517,19 +3494,7 @@ static int session_after_header_block_received(nghttp2_session *session) { return 0; } - switch (frame->headers.cat) { - case NGHTTP2_HCAT_REQUEST: - return nghttp2_session_end_request_headers_received(session, frame, stream); - case NGHTTP2_HCAT_RESPONSE: - case NGHTTP2_HCAT_PUSH_RESPONSE: - return nghttp2_session_end_response_headers_received(session, frame, - stream); - case NGHTTP2_HCAT_HEADERS: - return nghttp2_session_end_headers_received(session, frame, stream); - default: - assert(0); - } - return 0; + return session_end_stream_headers_received(session, frame, stream); } int nghttp2_session_on_request_headers_received(nghttp2_session *session, @@ -4287,9 +4252,8 @@ int nghttp2_session_on_push_promise_received(nghttp2_session *session, session, frame, NGHTTP2_ERR_PROTO, "PUSH_PROMISE: stream in idle"); } } - rv = nghttp2_session_add_rst_stream(session, - frame->push_promise.promised_stream_id, - NGHTTP2_REFUSED_STREAM); + rv = nghttp2_session_add_rst_stream( + session, frame->push_promise.promised_stream_id, NGHTTP2_CANCEL); if (rv != 0) { return rv; } @@ -6903,9 +6867,67 @@ int32_t nghttp2_session_get_last_proc_stream_id(nghttp2_session *session) { nghttp2_stream *nghttp2_session_find_stream(nghttp2_session *session, int32_t stream_id) { + if (stream_id == 0) { + return &session->root; + } + return nghttp2_session_get_stream_raw(session, stream_id); } nghttp2_stream *nghttp2_session_get_root_stream(nghttp2_session *session) { return &session->root; } + +int nghttp2_session_check_server_session(nghttp2_session *session) { + return session->server; +} + +int nghttp2_session_change_stream_priority( + nghttp2_session *session, int32_t stream_id, + const nghttp2_priority_spec *pri_spec) { + nghttp2_stream *stream; + nghttp2_priority_spec pri_spec_copy; + + if (stream_id == 0 || stream_id == pri_spec->stream_id) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + + stream = nghttp2_session_get_stream_raw(session, stream_id); + if (!stream) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + + pri_spec_copy = *pri_spec; + nghttp2_priority_spec_normalize_weight(&pri_spec_copy); + + return nghttp2_session_reprioritize_stream(session, stream, &pri_spec_copy); +} + +int nghttp2_session_create_idle_stream(nghttp2_session *session, + int32_t stream_id, + const nghttp2_priority_spec *pri_spec) { + nghttp2_stream *stream; + nghttp2_priority_spec pri_spec_copy; + + if (stream_id == 0 || stream_id == pri_spec->stream_id || + !session_detect_idle_stream(session, stream_id)) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + + stream = nghttp2_session_get_stream_raw(session, stream_id); + if (stream) { + return NGHTTP2_ERR_INVALID_ARGUMENT; + } + + pri_spec_copy = *pri_spec; + nghttp2_priority_spec_normalize_weight(&pri_spec_copy); + + stream = + nghttp2_session_open_stream(session, stream_id, NGHTTP2_STREAM_FLAG_NONE, + &pri_spec_copy, NGHTTP2_STREAM_IDLE, NULL); + if (!stream) { + return NGHTTP2_ERR_NOMEM; + } + + return 0; +} diff --git a/lib/nghttp2_session.h b/lib/nghttp2_session.h index 0e67acf2..76b69a20 100644 --- a/lib/nghttp2_session.h +++ b/lib/nghttp2_session.h @@ -191,11 +191,13 @@ struct nghttp2_session { updated when one frame was written. */ uint64_t last_cycle; void *user_data; - /* Points to the latest closed stream. NULL if there is no closed - stream. Only used when session is initialized as server. */ + /* Points to the latest incoming closed stream. NULL if there is no + closed stream. Only used when session is initialized as + server. */ nghttp2_stream *closed_stream_head; - /* Points to the oldest closed stream. NULL if there is no closed - stream. Only used when session is initialized as server. */ + /* Points to the oldest incoming closed stream. NULL if there is no + closed stream. Only used when session is initialized as + server. */ nghttp2_stream *closed_stream_tail; /* Points to the latest idle stream. NULL if there is no idle stream. Only used when session is initialized as server .*/ @@ -341,7 +343,7 @@ int nghttp2_session_is_my_stream_id(nghttp2_session *session, * NGHTTP2_ERR_NOMEM * Out of memory. * NGHTTP2_ERR_STREAM_CLOSED - * Stream already closed (DATA frame only) + * Stream already closed (DATA and PUSH_PROMISE frame only) */ int nghttp2_session_add_item(nghttp2_session *session, nghttp2_outbound_item *item); @@ -567,18 +569,6 @@ int nghttp2_session_adjust_idle_stream(nghttp2_session *session); int nghttp2_session_close_stream_if_shut_rdwr(nghttp2_session *session, nghttp2_stream *stream); -int nghttp2_session_end_request_headers_received(nghttp2_session *session, - nghttp2_frame *frame, - nghttp2_stream *stream); - -int nghttp2_session_end_response_headers_received(nghttp2_session *session, - nghttp2_frame *frame, - nghttp2_stream *stream); - -int nghttp2_session_end_headers_received(nghttp2_session *session, - nghttp2_frame *frame, - nghttp2_stream *stream); - int nghttp2_session_on_request_headers_received(nghttp2_session *session, nghttp2_frame *frame); diff --git a/lib/nghttp2_stream.c b/lib/nghttp2_stream.c index 1c67fe40..54219a4d 100644 --- a/lib/nghttp2_stream.c +++ b/lib/nghttp2_stream.c @@ -30,13 +30,14 @@ #include "nghttp2_session.h" #include "nghttp2_helper.h" -static int stream_weight_less(const void *lhsx, const void *rhsx) { +static int stream_less(const void *lhsx, const void *rhsx) { const nghttp2_stream *lhs, *rhs; lhs = nghttp2_struct_of(lhsx, nghttp2_stream, pq_entry); rhs = nghttp2_struct_of(rhsx, nghttp2_stream, pq_entry); - return lhs->cycle < rhs->cycle; + return lhs->cycle < rhs->cycle || + (lhs->cycle == rhs->cycle && lhs->seq < rhs->seq); } void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id, @@ -45,7 +46,7 @@ void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id, int32_t local_initial_window_size, void *stream_user_data, nghttp2_mem *mem) { nghttp2_map_entry_init(&stream->map_entry, (key_type)stream_id); - nghttp2_pq_init(&stream->obq, stream_weight_less, mem); + nghttp2_pq_init(&stream->obq, stream_less, mem); stream->stream_id = stream_id; stream->flags = flags; @@ -79,6 +80,8 @@ void nghttp2_stream_init(nghttp2_stream *stream, int32_t stream_id, stream->queued = 0; stream->descendant_last_cycle = 0; stream->cycle = 0; + stream->descendant_next_seq = 0; + stream->seq = 0; stream->last_writelen = 0; } @@ -124,6 +127,7 @@ static int stream_obq_push(nghttp2_stream *dep_stream, nghttp2_stream *stream) { stream = dep_stream, dep_stream = dep_stream->dep_prev) { stream->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", stream->stream_id, stream->cycle)); @@ -166,6 +170,7 @@ static void stream_obq_remove(nghttp2_stream *stream) { stream->queued = 0; stream->cycle = 0; stream->descendant_last_cycle = 0; + stream->last_writelen = 0; if (stream_subtree_active(dep_stream)) { return; @@ -206,12 +211,19 @@ void nghttp2_stream_reschedule(nghttp2_stream *stream) { 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); + stream->cycle = stream_next_cycle(stream, dep_stream->descendant_last_cycle); + stream->seq = dep_stream->descendant_next_seq++; - nghttp2_pq_remove(&dep_stream->obq, &stream->pq_entry); nghttp2_pq_push(&dep_stream->obq, &stream->pq_entry); } diff --git a/lib/nghttp2_stream.h b/lib/nghttp2_stream.h index f73febbb..e2dddefa 100644 --- a/lib/nghttp2_stream.h +++ b/lib/nghttp2_stream.h @@ -146,6 +146,15 @@ struct nghttp2_stream { int64_t content_length; /* Received body so far */ int64_t recv_content_length; + /* Base last_cycle for direct descendent streams. */ + uint64_t descendant_last_cycle; + /* Next scheduled time to sent item */ + uint64_t cycle; + /* Next seq used for direct descendant streams */ + uint64_t descendant_next_seq; + /* Secondary key for prioritization to break a tie for cycle. This + value is monotonically increased for single parent stream. */ + uint64_t seq; /* pointers to form dependency tree. If multiple streams depend on a stream, only one stream (left most) has non-NULL dep_prev which points to the stream it depends on. The remaining streams are @@ -164,6 +173,8 @@ struct nghttp2_stream { void *stream_user_data; /* Item to send */ nghttp2_outbound_item *item; + /* Last written length of frame payload */ + size_t last_writelen; /* stream ID */ int32_t stream_id; /* Current remote window size. This value is computed against the @@ -202,12 +213,6 @@ struct nghttp2_stream { then its ancestors, except for root, are also queued. This invariant may break in fatal error condition. */ uint8_t queued; - /* Base last_cycle for direct descendent streams. */ - uint64_t descendant_last_cycle; - /* Next scheduled time to sent item */ - uint64_t cycle; - /* Last written length of frame payload */ - size_t last_writelen; /* This flag is used to reduce excessive queuing of WINDOW_UPDATE to this stream. The nonzero does not necessarily mean WINDOW_UPDATE is not queued. */ diff --git a/lib/nghttp2_submit.c b/lib/nghttp2_submit.c index 7dc29ed8..9050a057 100644 --- a/lib/nghttp2_submit.c +++ b/lib/nghttp2_submit.c @@ -117,14 +117,6 @@ fail2: return rv; } -static void adjust_priority_spec_weight(nghttp2_priority_spec *pri_spec) { - if (pri_spec->weight < NGHTTP2_MIN_WEIGHT) { - pri_spec->weight = NGHTTP2_MIN_WEIGHT; - } else if (pri_spec->weight > NGHTTP2_MAX_WEIGHT) { - pri_spec->weight = NGHTTP2_MAX_WEIGHT; - } -} - static int32_t submit_headers_shared_nva(nghttp2_session *session, uint8_t flags, int32_t stream_id, const nghttp2_priority_spec *pri_spec, @@ -141,7 +133,7 @@ static int32_t submit_headers_shared_nva(nghttp2_session *session, if (pri_spec) { copy_pri_spec = *pri_spec; - adjust_priority_spec_weight(©_pri_spec); + nghttp2_priority_spec_normalize_weight(©_pri_spec); } else { nghttp2_priority_spec_default_init(©_pri_spec); } @@ -206,7 +198,7 @@ int nghttp2_submit_priority(nghttp2_session *session, uint8_t flags _U_, copy_pri_spec = *pri_spec; - adjust_priority_spec_weight(©_pri_spec); + nghttp2_priority_spec_normalize_weight(©_pri_spec); item = nghttp2_mem_malloc(mem, sizeof(nghttp2_outbound_item)); diff --git a/python/cnghttp2.pxd b/python/cnghttp2.pxd index 4a7e1100..3c829f82 100644 --- a/python/cnghttp2.pxd +++ b/python/cnghttp2.pxd @@ -288,6 +288,10 @@ cdef extern from 'nghttp2/nghttp2.h': const char* nghttp2_strerror(int lib_error_code) + int nghttp2_session_check_server_session(nghttp2_session *session) + + int nghttp2_session_get_stream_remote_close(nghttp2_session *session, int32_t stream_id) + int nghttp2_hd_deflate_new(nghttp2_hd_deflater **deflater_ptr, size_t deflate_hd_table_bufsize_max) diff --git a/python/nghttp2.pyx b/python/nghttp2.pyx index 7f348633..633cc337 100644 --- a/python/nghttp2.pyx +++ b/python/nghttp2.pyx @@ -473,6 +473,13 @@ cdef int server_on_frame_send(cnghttp2.nghttp2_session *session, if (frame.hd.flags & cnghttp2.NGHTTP2_FLAG_ACK) != 0: return 0 http2._start_settings_timer() + elif frame.hd.type == cnghttp2.NGHTTP2_HEADERS: + if (frame.hd.flags & cnghttp2.NGHTTP2_FLAG_END_STREAM) and \ + cnghttp2.nghttp2_session_check_server_session(session): + # Send RST_STREAM if remote is not closed yet + if cnghttp2.nghttp2_session_get_stream_remote_close( + session, frame.hd.stream_id) == 0: + http2._rst_stream(frame.hd.stream_id, cnghttp2.NGHTTP2_NO_ERROR) cdef int server_on_frame_not_send(cnghttp2.nghttp2_session *session, const cnghttp2.nghttp2_frame *frame, @@ -539,6 +546,11 @@ cdef ssize_t data_source_read(cnghttp2.nghttp2_session *session, if flag == DATA_EOF: data_flags[0] = cnghttp2.NGHTTP2_DATA_FLAG_EOF + if cnghttp2.nghttp2_session_check_server_session(session): + # Send RST_STREAM if remote is not closed yet + if cnghttp2.nghttp2_session_get_stream_remote_close( + session, stream_id) == 0: + http2._rst_stream(stream_id, cnghttp2.NGHTTP2_NO_ERROR) elif flag != DATA_OK: return cnghttp2.NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE diff --git a/src/HttpServer.cc b/src/HttpServer.cc index 2e313d85..14cb0395 100644 --- a/src/HttpServer.cc +++ b/src/HttpServer.cc @@ -175,6 +175,51 @@ namespace { void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config); } // namespace +namespace { +constexpr ev_tstamp RELEASE_FD_TIMEOUT = 2.; +} // namespace + +namespace { +void release_fd_cb(struct ev_loop *loop, ev_timer *w, int revents); +} // namespace + +namespace { +constexpr ev_tstamp FILE_ENTRY_MAX_AGE = 10.; +} // namespace + +namespace { +constexpr size_t FILE_ENTRY_EVICT_THRES = 2048; +} // namespace + +namespace { +bool need_validation_file_entry(const FileEntry *ent, ev_tstamp now) { + return ent->last_valid + FILE_ENTRY_MAX_AGE < now; +} +} // namespace + +namespace { +bool validate_file_entry(FileEntry *ent, ev_tstamp now) { + struct stat stbuf; + int rv; + + rv = fstat(ent->fd, &stbuf); + if (rv != 0) { + ent->stale = true; + return false; + } + + if (stbuf.st_nlink == 0 || ent->mtime != stbuf.st_mtime) { + ent->stale = true; + return false; + } + + ent->mtime = stbuf.st_mtime; + ent->last_valid = now; + + return true; +} +} // namespace + class Sessions { public: Sessions(HttpServer *sv, struct ev_loop *loop, const Config *config, @@ -185,15 +230,24 @@ public: nghttp2_session_callbacks_new(&callbacks_); fill_callback(callbacks_, config_); + + ev_timer_init(&release_fd_timer_, release_fd_cb, 0., RELEASE_FD_TIMEOUT); + release_fd_timer_.data = this; } ~Sessions() { + ev_timer_stop(loop_, &release_fd_timer_); for (auto handler : handlers_) { delete handler; } nghttp2_session_callbacks_del(callbacks_); } void add_handler(Http2Handler *handler) { handlers_.insert(handler); } - void remove_handler(Http2Handler *handler) { handlers_.erase(handler); } + void remove_handler(Http2Handler *handler) { + handlers_.erase(handler); + if (handlers_.empty() && !fd_cache_.empty()) { + ev_timer_again(loop_, &release_fd_timer_); + } + } SSL_CTX *get_ssl_ctx() const { return ssl_ctx_; } SSL *ssl_session_new(int fd) { SSL *ssl = SSL_new(ssl_ctx_); @@ -252,50 +306,122 @@ public: return cached_date_; } FileEntry *get_cached_fd(const std::string &path) { - auto i = fd_cache_.find(path); - if (i == std::end(fd_cache_)) { + auto range = fd_cache_.equal_range(path); + if (range.first == range.second) { return nullptr; } - auto &ent = (*i).second; - ++ent.usecount; - return &ent; + + auto now = ev_now(loop_); + + for (auto it = range.first; it != range.second;) { + auto &ent = (*it).second; + if (ent->stale) { + ++it; + continue; + } + if (need_validation_file_entry(ent.get(), now) && + !validate_file_entry(ent.get(), now)) { + if (ent->usecount == 0) { + fd_cache_lru_.remove(ent.get()); + close(ent->fd); + it = fd_cache_.erase(it); + continue; + } + ++it; + continue; + } + + fd_cache_lru_.remove(ent.get()); + fd_cache_lru_.append(ent.get()); + + ++ent->usecount; + return ent.get(); + } + return nullptr; } FileEntry *cache_fd(const std::string &path, const FileEntry &ent) { #ifdef HAVE_STD_MAP_EMPLACE - auto rv = fd_cache_.emplace(path, ent); + auto rv = fd_cache_.emplace(path, make_unique(ent)); #else // !HAVE_STD_MAP_EMPLACE // for gcc-4.7 - auto rv = fd_cache_.insert(std::make_pair(path, ent)); + auto rv = + fd_cache_.insert(std::make_pair(path, make_unique(ent))); #endif // !HAVE_STD_MAP_EMPLACE - return &(*rv.first).second; + auto &res = (*rv).second; + res->it = rv; + fd_cache_lru_.append(res.get()); + + while (fd_cache_.size() > FILE_ENTRY_EVICT_THRES) { + auto ent = fd_cache_lru_.head; + if (ent->usecount) { + break; + } + fd_cache_lru_.remove(ent); + close(ent->fd); + fd_cache_.erase(ent->it); + } + + return res.get(); } - void release_fd(const std::string &path) { - auto i = fd_cache_.find(path); - if (i == std::end(fd_cache_)) { + void release_fd(FileEntry *target) { + --target->usecount; + + if (target->usecount == 0 && target->stale) { + fd_cache_lru_.remove(target); + close(target->fd); + fd_cache_.erase(target->it); return; } - auto &ent = (*i).second; - if (--ent.usecount == 0) { - close(ent.fd); - fd_cache_.erase(i); + + // We use timer to close file descriptor and delete the entry from + // cache. The timer will be started when there is no handler. + } + void release_unused_fd() { + for (auto i = std::begin(fd_cache_); i != std::end(fd_cache_);) { + auto &ent = (*i).second; + if (ent->usecount != 0) { + ++i; + continue; + } + + fd_cache_lru_.remove(ent.get()); + close(ent->fd); + i = fd_cache_.erase(i); } } const HttpServer *get_server() const { return sv_; } + bool handlers_empty() const { return handlers_.empty(); } private: std::set handlers_; // cache for file descriptors to read file. - std::map fd_cache_; + std::multimap> fd_cache_; + DList fd_cache_lru_; HttpServer *sv_; struct ev_loop *loop_; const Config *config_; SSL_CTX *ssl_ctx_; nghttp2_session_callbacks *callbacks_; + ev_timer release_fd_timer_; int64_t next_session_id_; ev_tstamp tstamp_cached_; std::string cached_date_; }; +namespace { +void release_fd_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto sessions = static_cast(w->data); + + ev_timer_stop(loop, w); + + if (!sessions->handlers_empty()) { + return; + } + + sessions->release_unused_fd(); +} +} // namespace + Stream::Stream(Http2Handler *handler, int32_t stream_id) : handler(handler), file_ent(nullptr), body_length(0), body_offset(0), stream_id(stream_id), echo_upload(false) { @@ -313,7 +439,7 @@ Stream::Stream(Http2Handler *handler, int32_t stream_id) Stream::~Stream() { if (file_ent != nullptr) { auto sessions = handler->get_sessions(); - sessions->release_fd(file_ent->path); + sessions->release_fd(file_ent); } auto loop = handler->get_loop(); @@ -979,8 +1105,9 @@ bool prepare_upload_temp_store(Stream *stream, Http2Handler *hd) { unlink(tempfn); // Ordinary request never start with "echo:". The length is 0 for // now. We will update it when we get whole request body. - stream->file_ent = sessions->cache_fd(std::string("echo:") + tempfn, - FileEntry(tempfn, 0, 0, fd, nullptr)); + auto path = std::string("echo:") + tempfn; + stream->file_ent = + sessions->cache_fd(path, FileEntry(path, 0, 0, fd, nullptr, 0, true)); stream->echo_upload = true; return true; } @@ -1016,8 +1143,13 @@ namespace { void prepare_response(Stream *stream, Http2Handler *hd, bool allow_push = true) { int rv; - auto reqpath = - http2::get_header(stream->hdidx, http2::HD__PATH, stream->headers)->value; + auto pathhdr = + http2::get_header(stream->hdidx, http2::HD__PATH, stream->headers); + if (!pathhdr) { + prepare_status_response(stream, hd, 405); + return; + } + auto reqpath = pathhdr->value; auto ims = get_header(stream->hdidx, http2::HD_IF_MODIFIED_SINCE, stream->headers); @@ -1042,10 +1174,10 @@ void prepare_response(Stream *stream, Http2Handler *hd, auto sessions = hd->get_sessions(); - url = util::percentDecode(std::begin(url), std::end(url)); + url = util::percent_decode(std::begin(url), std::end(url)); if (!util::check_path(url)) { if (stream->file_ent) { - sessions->release_fd(stream->file_ent->path); + sessions->release_fd(stream->file_ent); stream->file_ent = nullptr; } prepare_status_response(stream, hd, 404); @@ -1096,7 +1228,7 @@ void prepare_response(Stream *stream, Http2Handler *hd, close(file); if (query_pos == std::string::npos) { - reqpath += "/"; + reqpath += '/'; } else { reqpath.insert(query_pos, "/"); } @@ -1127,7 +1259,8 @@ void prepare_response(Stream *stream, Http2Handler *hd, } file_ent = sessions->cache_fd( - path, FileEntry(path, buf.st_size, buf.st_mtime, file, content_type)); + path, FileEntry(path, buf.st_size, buf.st_mtime, file, content_type, + ev_now(sessions->get_loop()))); } stream->file_ent = file_ent; @@ -1651,7 +1784,7 @@ FileEntry make_status_body(int status, uint16_t port) { assert(0); } - return FileEntry(util::utos(status), nwrite, 0, fd, nullptr); + return FileEntry(util::utos(status), nwrite, 0, fd, nullptr, 0); } } // namespace @@ -1661,6 +1794,7 @@ enum { IDX_301, IDX_400, IDX_404, + IDX_405, }; HttpServer::HttpServer(const Config *config) : config_(config) { @@ -1669,6 +1803,7 @@ HttpServer::HttpServer(const Config *config) : config_(config) { {"301", make_status_body(301, config_->port)}, {"400", make_status_body(400, config_->port)}, {"404", make_status_body(404, config_->port)}, + {"405", make_status_body(405, config_->port)}, }; } @@ -1921,6 +2056,8 @@ const StatusPage *HttpServer::get_status_page(int status) const { return &status_pages_[IDX_400]; case 404: return &status_pages_[IDX_404]; + case 405: + return &status_pages_[IDX_405]; default: assert(0); } diff --git a/src/HttpServer.h b/src/HttpServer.h index de43eeec..3c1dc73e 100644 --- a/src/HttpServer.h +++ b/src/HttpServer.h @@ -84,16 +84,21 @@ class Http2Handler; struct FileEntry { FileEntry(std::string path, int64_t length, int64_t mtime, int fd, - const std::string *content_type) - : path(std::move(path)), length(length), mtime(mtime), dlprev(nullptr), - dlnext(nullptr), content_type(content_type), fd(fd), usecount(1) {} + const std::string *content_type, ev_tstamp last_valid, + bool stale = false) + : path(std::move(path)), length(length), mtime(mtime), + last_valid(last_valid), content_type(content_type), dlnext(nullptr), + dlprev(nullptr), fd(fd), usecount(1), stale(stale) {} std::string path; + std::multimap>::iterator it; int64_t length; int64_t mtime; - FileEntry *dlprev, *dlnext; + ev_tstamp last_valid; const std::string *content_type; + FileEntry *dlnext, *dlprev; int fd; int usecount; + bool stale; }; struct Stream { diff --git a/src/asio_client_session_impl.cc b/src/asio_client_session_impl.cc index d3a57a1e..3be7b20d 100644 --- a/src/asio_client_session_impl.cc +++ b/src/asio_client_session_impl.cc @@ -420,10 +420,10 @@ const request *session_impl::submit(boost::system::error_code &ec, if (util::ipv6_numeric_addr(uref.host.c_str())) { uref.host = "[" + uref.host; - uref.host += "]"; + uref.host += ']'; } if (u.field_set & (1 << UF_PORT)) { - uref.host += ":"; + uref.host += ':'; uref.host += util::utos(u.port); } @@ -435,7 +435,7 @@ const request *session_impl::submit(boost::system::error_code &ec, auto path = uref.raw_path; if (u.field_set & (1 << UF_QUERY)) { - path += "?"; + path += '?'; path += uref.raw_query; } @@ -525,7 +525,7 @@ void session_impl::do_read() { read_socket([this](const boost::system::error_code &ec, std::size_t bytes_transferred) { if (ec) { - if (ec.value() == boost::asio::error::operation_aborted) { + if (!should_stop()) { call_error_cb(ec); shutdown_socket(); } diff --git a/src/asio_common.cc b/src/asio_common.cc index 0319169e..c8d148fa 100644 --- a/src/asio_common.cc +++ b/src/asio_common.cc @@ -134,7 +134,7 @@ generator_cb file_generator_from_fd(int fd) { bool check_path(const std::string &path) { return util::check_path(path); } std::string percent_decode(const std::string &s) { - return util::percentDecode(std::begin(s), std::end(s)); + return util::percent_decode(std::begin(s), std::end(s)); } std::string http_date(int64_t t) { return util::http_date(t); } @@ -177,9 +177,11 @@ bool tls_h2_negotiated(ssl_socket &socket) { unsigned int next_proto_len = 0; SSL_get0_next_proto_negotiated(ssl, &next_proto, &next_proto_len); +#if OPENSSL_VERSION_NUMBER >= 0x10002000L if (next_proto == nullptr) { SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); } +#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L if (next_proto == nullptr) { return false; diff --git a/src/asio_common.h b/src/asio_common.h index b2384649..7eacfa51 100644 --- a/src/asio_common.h +++ b/src/asio_common.h @@ -55,7 +55,7 @@ void split_path(uri_ref &dst, InputIt first, InputIt last) { } else { query_first = path_last + 1; } - dst.path = util::percentDecode(first, path_last); + dst.path = util::percent_decode(first, path_last); dst.raw_path.assign(first, path_last); dst.raw_query.assign(query_first, last); } diff --git a/src/asio_server_serve_mux.cc b/src/asio_server_serve_mux.cc index 829568ba..b635ee4d 100644 --- a/src/asio_server_serve_mux.cc +++ b/src/asio_server_serve_mux.cc @@ -83,7 +83,7 @@ request_cb serve_mux::handler(request_impl &req) const { auto new_uri = util::percent_encode_path(clean_path); auto &uref = req.uri(); if (!uref.raw_query.empty()) { - new_uri += "?"; + new_uri += '?'; new_uri += uref.raw_query; } @@ -108,7 +108,7 @@ bool path_match(const std::string &pattern, const std::string &path) { if (pattern.back() != '/') { return pattern == path; } - return util::startsWith(path, pattern); + return util::starts_with(path, pattern); } } // namespace diff --git a/src/h2load.cc b/src/h2load.cc index 2ef99e78..3f642eb6 100644 --- a/src/h2load.cc +++ b/src/h2load.cc @@ -71,6 +71,12 @@ using namespace nghttp2; namespace h2load { +namespace { +bool recorded(const std::chrono::steady_clock::time_point &t) { + return std::chrono::steady_clock::duration::zero() != t.time_since_epoch(); +} +} // namespace + Config::Config() : data_length(-1), addrs(nullptr), nreqs(1), nclients(1), nthreads(1), max_concurrent_streams(-1), window_bits(30), connection_window_bits(30), @@ -92,11 +98,11 @@ Config config; RequestStat::RequestStat() : data_offset(0), completed(false) {} -Stats::Stats(size_t req_todo) +Stats::Stats(size_t req_todo, size_t nclients) : req_todo(0), 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) {} + status(), req_stats(req_todo), client_stats(nclients) {} Stream::Stream() : status_success(-1) {} @@ -149,7 +155,8 @@ 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, req_todo)); + worker->clients.push_back( + make_unique(worker->next_client_id++, worker, req_todo)); auto &client = worker->clients.back(); if (client->connect() != 0) { std::cerr << "client could not connect to host" << std::endl; @@ -232,11 +239,11 @@ void client_request_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { } } // namespace -Client::Client(Worker *worker, size_t req_todo) +Client::Client(uint32_t id, Worker *worker, size_t req_todo) : worker(worker), ssl(nullptr), next_addr(config.addrs), - current_addr(nullptr), reqidx(0), state(CLIENT_IDLE), - first_byte_received(false), req_todo(req_todo), req_started(0), - req_done(0), fd(-1), new_connection_requested(false) { + 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) { ev_io_init(&wev, writecb, 0, EV_WRITE); ev_io_init(&rev, readcb, 0, EV_READ); @@ -302,7 +309,9 @@ int Client::make_socket(addrinfo *addr) { int Client::connect() { int rv; - record_start_time(&worker->stats); + record_client_start_time(); + clear_connect_times(); + record_connect_start_time(); if (worker->config->conn_inactivity_timeout > 0.) { ev_timer_again(worker->loop, &conn_inactivity_watcher); @@ -360,23 +369,37 @@ void Client::fail() { if (new_connection_requested) { new_connection_requested = false; + if (req_started < req_todo) { + // At the moment, we don't have a facility to re-start request + // already in in-flight. Make them fail. + auto req_abandoned = req_started - req_done; - // Keep using current address - if (connect() == 0) { - return; + worker->stats.req_failed += req_abandoned; + worker->stats.req_error += req_abandoned; + worker->stats.req_done += req_abandoned; + + req_done = req_started; + + // Keep using current address + if (connect() == 0) { + return; + } + std::cerr << "client could not connect to host" << std::endl; } - std::cerr << "client could not connect to host" << std::endl; } process_abandoned_streams(); } void Client::disconnect() { + record_client_end_time(); + ev_timer_stop(worker->loop, &conn_inactivity_watcher); ev_timer_stop(worker->loop, &conn_active_watcher); ev_timer_stop(worker->loop, &request_timeout_watcher); streams.clear(); session.reset(); + wb.reset(); state = CLIENT_IDLE; ev_io_stop(worker->loop, &wev); ev_io_stop(worker->loop, &rev); @@ -590,6 +613,8 @@ void Client::on_stream_close(int32_t stream_id, bool success, 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; @@ -720,7 +745,7 @@ int Client::connection_made() { session->on_connect(); - record_connect_time(&worker->stats); + record_connect_time(); if (!config.timing_script) { auto nreq = @@ -742,10 +767,19 @@ int Client::connection_made() { break; } duration = config.timings[reqidx]; + if (reqidx == 0) { + // if reqidx wraps around back to 0, we uses up all lines and + // should break + break; + } } - request_timeout_watcher.repeat = duration; - ev_timer_again(worker->loop, &request_timeout_watcher); + if (duration >= 1e-9) { + // double check since we may have break due to reqidx wraps + // around back to 0 + request_timeout_watcher.repeat = duration; + ev_timer_again(worker->loop, &request_timeout_watcher); + } } signal_write(); @@ -950,21 +984,51 @@ void Client::record_request_time(RequestStat *req_stat) { req_stat->request_time = std::chrono::steady_clock::now(); } -void Client::record_start_time(Stats *stat) { - stat->start_times.push_back(std::chrono::steady_clock::now()); +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(Stats *stat) { - stat->connect_times.push_back(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() { - if (first_byte_received) { + auto &cstat = worker->stats.client_stats[id]; + if (recorded(cstat.ttfb)) { return; } - first_byte_received = true; - worker->stats.ttfbs.push_back(std::chrono::steady_clock::now()); + cstat.ttfb = std::chrono::steady_clock::now(); +} + +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)) { + return; + } + + cstat.client_start_time = std::chrono::steady_clock::now(); +} + +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(); } void Client::signal_write() { ev_io_start(worker->loop, &wev); } @@ -973,10 +1037,11 @@ 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) - : stats(req_todo), 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) { + : 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); @@ -992,7 +1057,7 @@ 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(this, req_todo)); + clients.push_back(make_unique(next_client_id++, this, req_todo)); } } } @@ -1025,9 +1090,7 @@ void Worker::run() { namespace { // Returns percentage of number of samples within mean +/- sd. -template -double within_sd(const std::vector &samples, const Duration &mean, - const Duration &sd) { +double within_sd(const std::vector &samples, double mean, double sd) { if (samples.size() == 0) { return 0.0; } @@ -1035,7 +1098,7 @@ double within_sd(const std::vector &samples, const Duration &mean, auto upper = mean + sd; auto m = std::count_if( std::begin(samples), std::end(samples), - [&lower, &upper](const Duration &t) { return lower <= t && t <= upper; }); + [&lower, &upper](double t) { return lower <= t && t <= upper; }); return (m / static_cast(samples.size())) * 100; } } // namespace @@ -1043,32 +1106,31 @@ double within_sd(const std::vector &samples, const Duration &mean, namespace { // Computes statistics using |samples|. The min, max, mean, sd, and // percentage of number of samples within mean +/- sd are computed. -template -TimeStat compute_time_stat(const std::vector &samples) { +SDStat compute_time_stat(const std::vector &samples) { if (samples.empty()) { - return {Duration::zero(), Duration::zero(), Duration::zero(), - Duration::zero(), 0.0}; + 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 double a = 0, q = 0; size_t n = 0; - int64_t sum = 0; - auto res = TimeStat{Duration::max(), Duration::min()}; + double sum = 0; + auto res = SDStat{std::numeric_limits::max(), + std::numeric_limits::min()}; for (const auto &t : samples) { ++n; res.min = std::min(res.min, t); res.max = std::max(res.max, t); - sum += t.count(); + sum += t; - auto na = a + (t.count() - a) / n; - q += (t.count() - a) * (t.count() - na); + auto na = a + (t - a) / n; + q += (t - a) * (t - na); a = na; } assert(n > 0); - res.mean = Duration(sum / n); - res.sd = Duration(static_cast(sqrt(q / n))); + res.mean = sum / n; + res.sd = sqrt(q / n); res.within_sd = within_sd(samples, res.mean, res.sd); return res; @@ -1076,19 +1138,20 @@ TimeStat compute_time_stat(const std::vector &samples) { } // namespace namespace { -TimeStats +SDStats process_time_stats(const std::vector> &workers) { - size_t nrequest_times = 0, nttfb_times = 0; + size_t nrequest_times = 0; for (const auto &w : workers) { nrequest_times += w->stats.req_stats.size(); - nttfb_times += w->stats.ttfbs.size(); } - std::vector request_times; + std::vector request_times; request_times.reserve(nrequest_times); - std::vector connect_times, ttfb_times; - connect_times.reserve(nttfb_times); - ttfb_times.reserve(nttfb_times); + + std::vector connect_times, ttfb_times, rps_values; + connect_times.reserve(config.nclients); + ttfb_times.reserve(config.nclients); + rps_values.reserve(config.nclients); for (const auto &w : workers) { for (const auto &req_stat : w->stats.req_stats) { @@ -1096,28 +1159,44 @@ process_time_stats(const std::vector> &workers) { continue; } request_times.push_back( - std::chrono::duration_cast( - req_stat.stream_close_time - req_stat.request_time)); + std::chrono::duration_cast>( + req_stat.stream_close_time - req_stat.request_time).count()); } const auto &stat = w->stats; - // rule out cases where we started but didn't connect or get the - // first byte (errors). We will get connect event before FFTB. - assert(stat.start_times.size() >= stat.ttfbs.size()); - assert(stat.connect_times.size() >= stat.ttfbs.size()); - for (size_t i = 0; i < stat.ttfbs.size(); ++i) { + + for (const auto &cstat : stat.client_stats) { + if (recorded(cstat.client_start_time) && + recorded(cstat.client_end_time)) { + auto t = std::chrono::duration_cast>( + cstat.client_end_time - cstat.client_start_time).count(); + if (t > 1e-9) { + rps_values.push_back(cstat.req_success / t); + } + } + + // We will get connect event before FFTB. + if (!recorded(cstat.connect_start_time) || + !recorded(cstat.connect_time)) { + continue; + } + connect_times.push_back( - std::chrono::duration_cast( - stat.connect_times[i] - stat.start_times[i])); + std::chrono::duration_cast>( + cstat.connect_time - cstat.connect_start_time).count()); + + if (!recorded(cstat.ttfb)) { + continue; + } ttfb_times.push_back( - std::chrono::duration_cast( - stat.ttfbs[i] - stat.start_times[i])); + std::chrono::duration_cast>( + cstat.ttfb - cstat.connect_start_time).count()); } } return {compute_time_stat(request_times), compute_time_stat(connect_times), - compute_time_stat(ttfb_times)}; + compute_time_stat(ttfb_times), compute_time_stat(rps_values)}; } } // namespace @@ -1156,7 +1235,7 @@ std::string get_reqline(const char *uri, const http_parser_url &u) { } if (util::has_uri_field(u, UF_QUERY)) { - reqline += "?"; + reqline += '?'; reqline += util::get_uri_field(uri, u, UF_QUERY); } @@ -1469,6 +1548,9 @@ Options: only and any white spaces are treated as a part of protocol string. Default: )" << DEFAULT_NPN_LIST << R"( + --h1 Short hand for --npn-list=http/1.1 + --no-tls-proto=http/1.1, which effectively force + http/1.1 for both http and https URI. -v, --verbose Output debug information. --version Display version information and exit. @@ -1516,6 +1598,7 @@ int main(int argc, char **argv) { {"base-uri", required_argument, nullptr, 'B'}, {"npn-list", required_argument, &flag, 4}, {"rate-period", required_argument, &flag, 5}, + {"h1", no_argument, &flag, 6}, {nullptr, 0, nullptr, 0}}; int option_index = 0; auto c = getopt_long(argc, argv, "hvW:c:d:m:n:p:t:w:H:i:r:T:N:B:", @@ -1682,6 +1765,11 @@ int main(int argc, char **argv) { exit(EXIT_FAILURE); } break; + case 6: + // --h1 + config.npn_list = util::parse_config_str_list("http/1.1"); + config.no_tls_proto = Config::PROTO_HTTP1_1; + break; } break; default: @@ -1893,8 +1981,8 @@ int main(int argc, char **argv) { shared_nva.emplace_back("user-agent", user_agent); // list overridalbe headers - auto override_hdrs = - make_array(":authority", ":host", ":method", ":scheme"); + auto override_hdrs = make_array(":authority", ":host", ":method", + ":scheme", "user-agent"); for (auto &kv : config.custom_headers) { if (std::find(std::begin(override_hdrs), std::end(override_hdrs), @@ -1912,20 +2000,44 @@ int main(int argc, char **argv) { } } + auto method_it = + std::find_if(std::begin(shared_nva), std::end(shared_nva), + [](const Header &nv) { return nv.name == ":method"; }); + assert(method_it != std::end(shared_nva)); + + config.h1reqs.reserve(reqlines.size()); + config.nva.reserve(reqlines.size()); + config.nv.reserve(reqlines.size()); + for (auto &req : reqlines) { // For HTTP/1.1 - std::string h1req; - h1req = config.data_fd == -1 ? "GET" : "POST"; - h1req += " " + req; + auto h1req = (*method_it).value; + h1req += ' '; + h1req += req; h1req += " HTTP/1.1\r\n"; - h1req += "Host: " + config.host + "\r\n"; - h1req += "User-Agent: " + user_agent + "\r\n"; - h1req += "Accept: */*\r\n"; + for (auto &nv : shared_nva) { + if (nv.name == ":authority") { + h1req += "Host: "; + h1req += nv.value; + h1req += "\r\n"; + continue; + } + if (nv.name[0] == ':') { + continue; + } + h1req += nv.name; + h1req += ": "; + h1req += nv.value; + h1req += "\r\n"; + } h1req += "\r\n"; - config.h1reqs.push_back(h1req); + + config.h1reqs.push_back(std::move(h1req)); // For nghttp2 std::vector nva; + // 1 for :path + nva.reserve(1 + shared_nva.size()); nva.push_back(http2::make_nv_ls(":path", req)); @@ -1937,6 +2049,8 @@ int main(int argc, char **argv) { // For spdylay std::vector cva; + // 2 for :path and :version, 1 for terminal nullptr + cva.reserve(2 * (2 + shared_nva.size()) + 1); cva.push_back(":path"); cva.push_back(req.c_str()); @@ -2058,7 +2172,7 @@ int main(int argc, char **argv) { auto duration = std::chrono::duration_cast(end - start); - Stats stats(0); + Stats stats(0, 0); for (const auto &w : workers) { const auto &s = w->stats; @@ -2138,7 +2252,11 @@ time for request: )" << std::setw(10) << util::format_duration(ts.request.min) << util::format_duration(ts.ttfb.max) << " " << std::setw(10) << util::format_duration(ts.ttfb.mean) << " " << std::setw(10) << util::format_duration(ts.ttfb.sd) << std::setw(9) - << util::dtos(ts.ttfb.within_sd) << "%" << std::endl; + << util::dtos(ts.ttfb.within_sd) << "%" + << "\nreq/s (client) : " << 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; SSL_CTX_free(ssl_ctx); diff --git a/src/h2load.h b/src/h2load.h index fe6c99ff..a7e07ea4 100644 --- a/src/h2load.h +++ b/src/h2load.h @@ -124,26 +124,47 @@ struct RequestStat { bool completed; }; -template struct TimeStat { +struct ClientStat { + // time client started (i.e., first connect starts) + std::chrono::steady_clock::time_point client_start_time; + // time client end (i.e., client somehow processed all requests it + // is responsible for, and disconnected) + std::chrono::steady_clock::time_point client_end_time; + // The number of requests completed successfull, but not necessarily + // means successful HTTP status code. + size_t req_success; + + // The following 3 numbers are overwritten each time when connection + // is made. + + // time connect starts + std::chrono::steady_clock::time_point connect_start_time; + // time to connect + std::chrono::steady_clock::time_point connect_time; + // time to first byte (TTFB) + std::chrono::steady_clock::time_point ttfb; +}; + +struct SDStat { // min, max, mean and sd (standard deviation) - Duration min, max, mean, sd; + double min, max, mean, sd; // percentage of samples inside mean -/+ sd double within_sd; }; -struct TimeStats { +struct SDStats { // time for request - TimeStat request; + SDStat request; // time for connect - TimeStat connect; + SDStat connect; // time to first byte (TTFB) - TimeStat ttfb; + SDStat ttfb; + // request per second for each client + SDStat rps; }; -enum TimeStatType { STAT_REQUEST, STAT_CONNECT, STAT_FIRST_BYTE }; - struct Stats { - Stats(size_t req_todo); + Stats(size_t req_todo, size_t nclients); // The total number of requests size_t req_todo; // The number of requests issued so far @@ -179,12 +200,8 @@ struct Stats { std::array status; // The statistics per request std::vector req_stats; - // time connect starts - std::vector start_times; - // time to connect - std::vector connect_times; - // time to first byte (TTFB) - std::vector ttfbs; + // THe statistics per client + std::vector client_stats; }; enum ClientState { CLIENT_IDLE, CLIENT_CONNECTED }; @@ -210,6 +227,8 @@ struct Worker { size_t nreqs_rem; size_t rate; 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); @@ -240,13 +259,14 @@ struct Client { addrinfo *current_addr; size_t reqidx; ClientState state; - bool first_byte_received; // The number of requests this client has to issue. size_t req_todo; // The number of requests this client has issued so far. size_t req_started; // The number of requests this client has done so far. size_t req_done; + // The client id per worker + uint32_t id; int fd; Buffer<64_k> wb; ev_timer conn_active_watcher; @@ -256,7 +276,7 @@ struct Client { enum { ERR_CONNECT_FAIL = -100 }; - Client(Worker *worker, size_t req_todo); + Client(uint32_t id, Worker *worker, size_t req_todo); ~Client(); int make_socket(addrinfo *addr); int connect(); @@ -302,9 +322,12 @@ struct Client { bool final = false); void record_request_time(RequestStat *req_stat); - void record_start_time(Stats *stat); - void record_connect_time(Stats *stat); + void record_connect_start_time(); + void record_connect_time(); void record_ttfb(); + void clear_connect_times(); + void record_client_start_time(); + void record_client_end_time(); void signal_write(); }; diff --git a/src/h2load_http1_session.cc b/src/h2load_http1_session.cc index 728233d3..aa43f90d 100644 --- a/src/h2load_http1_session.cc +++ b/src/h2load_http1_session.cc @@ -51,7 +51,15 @@ Http1Session::~Http1Session() {} namespace { // HTTP response message begin -int htp_msg_begincb(http_parser *htp) { return 0; } +int htp_msg_begincb(http_parser *htp) { + auto session = static_cast(htp->data); + + if (session->stream_resp_counter_ >= session->stream_req_counter_) { + return -1; + } + + return 0; +} } // namespace namespace { @@ -144,7 +152,7 @@ void Http1Session::on_connect() { client_->signal_write(); } int Http1Session::submit_request(RequestStat *req_stat) { auto config = client_->worker->config; - auto req = config->h1reqs[client_->reqidx]; + const auto &req = config->h1reqs[client_->reqidx]; client_->reqidx++; if (client_->reqidx == config->h1reqs.size()) { diff --git a/src/http2.cc b/src/http2.cc index acac147a..671b1a92 100644 --- a/src/http2.cc +++ b/src/http2.cc @@ -454,8 +454,8 @@ std::string rewrite_location_uri(const std::string &uri, return ""; } auto field = &u.field_data[UF_HOST]; - if (!util::startsWith(std::begin(match_host), std::end(match_host), - &uri[field->off], &uri[field->off] + field->len) || + if (!util::starts_with(std::begin(match_host), std::end(match_host), + &uri[field->off], &uri[field->off] + field->len) || (match_host.size() != field->len && match_host[field->len] != ':')) { return ""; } @@ -471,12 +471,12 @@ std::string rewrite_location_uri(const std::string &uri, } if (u.field_set & (1 << UF_QUERY)) { field = &u.field_data[UF_QUERY]; - res += "?"; + res += '?'; res.append(&uri[field->off], field->len); } if (u.field_set & (1 << UF_FRAGMENT)) { field = &u.field_data[UF_FRAGMENT]; - res += "#"; + res += '#'; res.append(&uri[field->off], field->len); } return res; @@ -1185,12 +1185,12 @@ std::string path_join(const char *base_path, size_t base_pathlen, } if (rel_querylen == 0) { if (base_querylen) { - res += "?"; + res += '?'; res.append(base_query, base_querylen); } return res; } - res += "?"; + res += '?'; res.append(rel_query, rel_querylen); return res; } @@ -1242,7 +1242,7 @@ std::string path_join(const char *base_path, size_t base_pathlen, ; } if (rel_querylen) { - res += "?"; + res += '?'; res.append(rel_query, rel_querylen); } return res; @@ -1500,7 +1500,7 @@ int construct_push_component(std::string &scheme, std::string &authority, if (u.field_set & (1 << UF_HOST)) { http2::copy_url_component(authority, &u, UF_HOST, uri); if (u.field_set & (1 << UF_PORT)) { - authority += ":"; + authority += ':'; authority += util::utos(u.port); } } diff --git a/src/http2.h b/src/http2.h index dedee285..aaf076d6 100644 --- a/src/http2.h +++ b/src/http2.h @@ -340,10 +340,11 @@ std::string normalize_path(InputIt first, InputIt last) { } else { for (; first < last - 2;) { if (*first == '%') { - if (util::isHexDigit(*(first + 1)) && util::isHexDigit(*(first + 2))) { + if (util::is_hex_digit(*(first + 1)) && + util::is_hex_digit(*(first + 2))) { auto c = (util::hex_to_uint(*(first + 1)) << 4) + util::hex_to_uint(*(first + 2)); - if (util::inRFC3986UnreservedChars(c)) { + if (util::in_rfc3986_unreserved_chars(c)) { result += c; first += 3; continue; diff --git a/src/nghttp.cc b/src/nghttp.cc index e48d6592..2788c104 100644 --- a/src/nghttp.cc +++ b/src/nghttp.cc @@ -95,8 +95,7 @@ constexpr auto anchors = std::array{{ Config::Config() : header_table_size(-1), min_header_table_size(std::numeric_limits::max()), padding(0), - max_concurrent_streams(100), - peer_max_concurrent_streams(NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS), + max_concurrent_streams(100), peer_max_concurrent_streams(100), weight(NGHTTP2_DEFAULT_WEIGHT), multiply(1), timeout(0.), window_bits(-1), connection_window_bits(-1), verbose(0), null_out(false), remote_name(false), get_assets(false), stat(false), upgrade(false), @@ -168,7 +167,7 @@ std::string Request::make_reqpath() const { ? util::get_uri_field(uri.c_str(), u, UF_PATH) : "/"; if (util::has_uri_field(u, UF_QUERY)) { - path += "?"; + path += '?'; path.append(uri.c_str() + u.field_data[UF_QUERY].off, u.field_data[UF_QUERY].len); } @@ -199,7 +198,7 @@ std::string decode_host(std::string host) { auto zone_id_src = (*(zone_start + 1) == '2' && *(zone_start + 2) == '5') ? zone_start + 3 : zone_start + 1; - auto zone_id = util::percentDecode(zone_id_src, std::end(host)); + auto zone_id = util::percent_decode(zone_id_src, std::end(host)); host.erase(zone_start + 1, std::end(host)); host += zone_id; return host; @@ -828,7 +827,7 @@ int HttpClient::on_upgrade_connect() { reqvec[0]->method = "GET"; } else { req = (*meth).value; - req += " "; + req += ' '; reqvec[0]->method = (*meth).value; } req += reqvec[0]->make_reqpath(); @@ -2449,7 +2448,7 @@ Options: -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. + Default: 100 -c, --header-table-size= Specify decoder header table size. If this option is used multiple times, and the minimum value among the diff --git a/src/shrpx.cc b/src/shrpx.cc index 855df5f3..a83062f3 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -325,11 +325,11 @@ void exec_binary(SignalServer *ssv) { } for (size_t i = 0; i < envlen; ++i) { - if (util::startsWith(environ[i], ENV_LISTENER4_FD) || - util::startsWith(environ[i], ENV_LISTENER6_FD) || - util::startsWith(environ[i], ENV_PORT) || - util::startsWith(environ[i], ENV_UNIX_FD) || - util::startsWith(environ[i], ENV_UNIX_PATH)) { + if (util::starts_with(environ[i], ENV_LISTENER4_FD) || + util::starts_with(environ[i], ENV_LISTENER6_FD) || + util::starts_with(environ[i], ENV_PORT) || + util::starts_with(environ[i], ENV_UNIX_FD) || + util::starts_with(environ[i], ENV_UNIX_PATH)) { continue; } @@ -1480,9 +1480,14 @@ HTTP/2 and SPDY: meant for debugging purpose and not intended to enhance protocol security. --no-server-push - Disable HTTP/2 server push. Server push is only - supported by default mode and HTTP/2 frontend. SPDY - frontend does not support server push. + Disable HTTP/2 server push. Server push is supported by + default mode and HTTP/2 frontend via Link header field. + It is also supported if both frontend and backend are + HTTP/2 (which implies --http2-bridge or --client mode). + In this case, server push from backend session is + relayed to frontend, and server push via Link header + field is also supported. HTTP SPDY frontend does not + support server push. Mode: (default mode) diff --git a/src/shrpx_client_handler.cc b/src/shrpx_client_handler.cc index 47818894..8ace9d4a 100644 --- a/src/shrpx_client_handler.cc +++ b/src/shrpx_client_handler.cc @@ -859,13 +859,13 @@ ssize_t parse_proxy_line_port(const uint8_t *first, const uint8_t *last) { } if (*p == '0') { - if (p + 1 != last && util::isDigit(*(p + 1))) { + if (p + 1 != last && util::is_digit(*(p + 1))) { return -1; } return 1; } - for (; p != last && util::isDigit(*p); ++p) { + for (; p != last && util::is_digit(*p); ++p) { port *= 10; port += *p - '0'; diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index a2e35abf..565d3118 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -471,7 +471,7 @@ LogFragmentType log_var_lookup_token(const char *name, size_t namelen) { namespace { bool var_token(char c) { - return util::isAlpha(c) || util::isDigit(c) || c == '_'; + return util::is_alpha(c) || util::is_digit(c) || c == '_'; } } // namespace @@ -519,7 +519,7 @@ std::vector parse_log_format(const char *optarg) { auto type = log_var_lookup_token(var_name, var_namelen); if (type == SHRPX_LOGF_NONE) { - if (util::istartsWith(var_name, var_namelen, "http_")) { + if (util::istarts_with(var_name, var_namelen, "http_")) { if (util::streq("host", var_name + str_size("http_"), var_namelen - str_size("http_"))) { // Special handling of host header field. We will use @@ -596,7 +596,7 @@ void parse_mapping(const DownstreamAddr &addr, const char *src) { // This effectively makes empty pattern to "/". pattern.assign(raw_pattern.first, raw_pattern.second); util::inp_strlower(pattern); - pattern += "/"; + pattern += '/'; } else { pattern.assign(raw_pattern.first, slash); util::inp_strlower(pattern); @@ -1342,7 +1342,7 @@ int parse_config(const char *opt, const char *optarg, pat_delim = optarg + optarglen; } DownstreamAddr addr; - if (util::istartsWith(optarg, SHRPX_UNIX_PATH_PREFIX)) { + if (util::istarts_with(optarg, SHRPX_UNIX_PATH_PREFIX)) { auto path = optarg + str_size(SHRPX_UNIX_PATH_PREFIX); addr.host = strcopy(path, pat_delim); addr.host_unix = true; @@ -1368,7 +1368,7 @@ int parse_config(const char *opt, const char *optarg, return 0; } case SHRPX_OPTID_FRONTEND: { - if (util::istartsWith(optarg, SHRPX_UNIX_PATH_PREFIX)) { + if (util::istarts_with(optarg, SHRPX_UNIX_PATH_PREFIX)) { auto path = optarg + str_size(SHRPX_UNIX_PATH_PREFIX); mod_config()->host = strcopy(path); mod_config()->port = 0; @@ -1664,7 +1664,7 @@ int parse_config(const char *opt, const char *optarg, // Surprisingly, u.field_set & UF_USERINFO is nonzero even if // userinfo component is empty string. if (!val.empty()) { - val = util::percentDecode(val.begin(), val.end()); + val = util::percent_decode(std::begin(val), std::end(val)); mod_config()->downstream_http_proxy_userinfo = strcopy(val); } } diff --git a/src/shrpx_downstream_queue.cc b/src/shrpx_downstream_queue.cc index d1430e61..02fd0356 100644 --- a/src/shrpx_downstream_queue.cc +++ b/src/shrpx_downstream_queue.cc @@ -120,7 +120,8 @@ bool remove_host_entry_if_empty(const DownstreamQueue::HostEntry &ent, } } // namespace -Downstream *DownstreamQueue::remove_and_get_blocked(Downstream *downstream) { +Downstream *DownstreamQueue::remove_and_get_blocked(Downstream *downstream, + bool next_blocked) { // Delete downstream when this function returns. auto delptr = std::unique_ptr(downstream); @@ -144,7 +145,7 @@ Downstream *DownstreamQueue::remove_and_get_blocked(Downstream *downstream) { return nullptr; } - if (ent.num_active >= conn_max_per_host_) { + if (!next_blocked || ent.num_active >= conn_max_per_host_) { return nullptr; } diff --git a/src/shrpx_downstream_queue.h b/src/shrpx_downstream_queue.h index de06d33e..755af10d 100644 --- a/src/shrpx_downstream_queue.h +++ b/src/shrpx_downstream_queue.h @@ -79,10 +79,12 @@ public: // |host|. bool can_activate(const std::string &host) const; // Removes and frees |downstream| object. If |downstream| is in - // Downstream::DISPATCH_ACTIVE, this function may return Downstream - // object with the same target host in Downstream::DISPATCH_BLOCKED - // if its connection is now not blocked by conn_max_per_host_ limit. - Downstream *remove_and_get_blocked(Downstream *downstream); + // Downstream::DISPATCH_ACTIVE, and |next_blocked| is true, this + // function may return Downstream object with the same target host + // in Downstream::DISPATCH_BLOCKED if its connection is now not + // blocked by conn_max_per_host_ limit. + Downstream *remove_and_get_blocked(Downstream *downstream, + bool next_blocked = true); Downstream *get_downstreams() const; HostEntry &find_host_entry(const std::string &host); const std::string &make_host_key(const std::string &host) const; diff --git a/src/shrpx_http.cc b/src/shrpx_http.cc index c3f1aa7b..5f2f4b9e 100644 --- a/src/shrpx_http.cc +++ b/src/shrpx_http.cc @@ -55,7 +55,7 @@ std::string create_via_header_value(int major, int minor) { std::string hdrs; hdrs += static_cast(major + '0'); if (major < 2) { - hdrs += "."; + hdrs += '.'; hdrs += static_cast(minor + '0'); } hdrs += " nghttpx"; diff --git a/src/shrpx_http2_downstream_connection.cc b/src/shrpx_http2_downstream_connection.cc index 8a9a216f..d5674f73 100644 --- a/src/shrpx_http2_downstream_connection.cc +++ b/src/shrpx_http2_downstream_connection.cc @@ -67,14 +67,8 @@ Http2DownstreamConnection::~Http2DownstreamConnection() { error_code = NGHTTP2_INTERNAL_ERROR; } - if (downstream_->get_downstream_stream_id() != -1) { - if (LOG_ENABLED(INFO)) { - DCLOG(INFO, this) << "Submit RST_STREAM for DOWNSTREAM:" << downstream_ - << ", stream_id=" - << downstream_->get_downstream_stream_id() - << ", error_code=" << error_code; - } - + if (http2session_->get_state() == Http2Session::CONNECTED && + downstream_->get_downstream_stream_id() != -1) { submit_rst_stream(downstream_, error_code); http2session_->consume(downstream_->get_downstream_stream_id(), @@ -143,7 +137,8 @@ int Http2DownstreamConnection::submit_rst_stream(Downstream *downstream, if (LOG_ENABLED(INFO)) { DCLOG(INFO, this) << "Submit RST_STREAM for DOWNSTREAM:" << downstream << ", stream_id=" - << downstream->get_downstream_stream_id(); + << downstream->get_downstream_stream_id() + << ", error_code=" << error_code; } rv = http2session_->submit_rst_stream( downstream->get_downstream_stream_id(), error_code); diff --git a/src/shrpx_http2_session.cc b/src/shrpx_http2_session.cc index 2258903c..dd6ed02d 100644 --- a/src/shrpx_http2_session.cc +++ b/src/shrpx_http2_session.cc @@ -520,7 +520,7 @@ int Http2Session::downstream_connect_proxy() { std::string req = "CONNECT "; req += downstream_addr.hostport.get(); if (downstream_addr.port == 80 || downstream_addr.port == 443) { - req += ":"; + req += ':'; req += util::utos(downstream_addr.port); } req += " HTTP/1.1\r\nHost: "; @@ -682,32 +682,43 @@ int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, if (dconn) { auto downstream = dconn->get_downstream(); if (downstream && downstream->get_downstream_stream_id() == stream_id) { + auto upstream = downstream->get_upstream(); - if (downstream->get_upgraded() && - downstream->get_response_state() == Downstream::HEADER_COMPLETE) { - // For tunneled connection, we have to submit RST_STREAM to - // upstream *after* whole response body is sent. We just set - // MSG_COMPLETE here. Upstream will take care of that. - downstream->get_upstream()->on_downstream_body_complete(downstream); - downstream->set_response_state(Downstream::MSG_COMPLETE); - } else if (error_code == NGHTTP2_NO_ERROR) { - switch (downstream->get_response_state()) { - case Downstream::MSG_COMPLETE: - case Downstream::MSG_BAD_HEADER: - break; - default: + if (downstream->get_downstream_stream_id() % 2 == 0 && + downstream->get_request_state() == Downstream::INITIAL) { + // Downstream is canceled in backend before it is submitted in + // frontend session. + + // This will avoid to send RST_STREAM to backend + downstream->set_response_state(Downstream::MSG_RESET); + upstream->cancel_premature_downstream(downstream); + } else { + if (downstream->get_upgraded() && + downstream->get_response_state() == Downstream::HEADER_COMPLETE) { + // For tunneled connection, we have to submit RST_STREAM to + // upstream *after* whole response body is sent. We just set + // MSG_COMPLETE here. Upstream will take care of that. + downstream->get_upstream()->on_downstream_body_complete(downstream); + downstream->set_response_state(Downstream::MSG_COMPLETE); + } else if (error_code == NGHTTP2_NO_ERROR) { + switch (downstream->get_response_state()) { + case Downstream::MSG_COMPLETE: + case Downstream::MSG_BAD_HEADER: + break; + default: + downstream->set_response_state(Downstream::MSG_RESET); + } + } else if (downstream->get_response_state() != + Downstream::MSG_BAD_HEADER) { downstream->set_response_state(Downstream::MSG_RESET); } - } else if (downstream->get_response_state() != - Downstream::MSG_BAD_HEADER) { - downstream->set_response_state(Downstream::MSG_RESET); + if (downstream->get_response_state() == Downstream::MSG_RESET && + downstream->get_response_rst_stream_error_code() == + NGHTTP2_NO_ERROR) { + downstream->set_response_rst_stream_error_code(error_code); + } + call_downstream_readcb(http2session, downstream); } - if (downstream->get_response_state() == Downstream::MSG_RESET && - downstream->get_response_rst_stream_error_code() == - NGHTTP2_NO_ERROR) { - downstream->set_response_rst_stream_error_code(error_code); - } - call_downstream_readcb(http2session, downstream); // dconn may be deleted } } @@ -730,6 +741,7 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, uint8_t flags, void *user_data) { + auto http2session = static_cast(user_data); auto sd = static_cast( nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); if (!sd || !sd->dconn) { @@ -740,44 +752,80 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, return 0; } - if (frame->hd.type != NGHTTP2_HEADERS) { - return 0; - } + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + auto trailer = frame->headers.cat == NGHTTP2_HCAT_HEADERS && + !downstream->get_expect_final_response(); - auto trailer = frame->headers.cat != NGHTTP2_HCAT_RESPONSE && - !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 (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) { - // we don't care trailer part exceeds header size limit; just - // discard it. + // just store header fields for trailer part + downstream->add_response_trailer(name, namelen, value, valuelen, + flags & NGHTTP2_NV_FLAG_NO_INDEX, -1); return 0; } - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - } + auto token = http2::lookup_token(name, namelen); - if (trailer) { - // just store header fields for trailer part - downstream->add_response_trailer(name, namelen, value, valuelen, - flags & NGHTTP2_NV_FLAG_NO_INDEX, -1); + downstream->add_response_header(name, namelen, value, valuelen, + flags & NGHTTP2_NV_FLAG_NO_INDEX, token); return 0; } + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + auto promised_sd = static_cast( + nghttp2_session_get_stream_user_data(session, promised_stream_id)); + if (!promised_sd || !promised_sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } - auto token = http2::lookup_token(name, namelen); + auto promised_downstream = promised_sd->dconn->get_downstream(); + + 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, + token); + return 0; + } + } - downstream->add_response_header(name, namelen, value, valuelen, - flags & NGHTTP2_NV_FLAG_NO_INDEX, token); return 0; } } // namespace @@ -786,24 +834,52 @@ namespace { int on_begin_headers_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { auto http2session = static_cast(user_data); - if (frame->hd.type != NGHTTP2_HEADERS || - frame->headers.cat != NGHTTP2_HCAT_RESPONSE) { + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + if (frame->headers.cat != NGHTTP2_HCAT_RESPONSE && + frame->headers.cat != NGHTTP2_HCAT_PUSH_RESPONSE) { + return 0; + } + auto sd = static_cast( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + return 0; + } + auto downstream = sd->dconn->get_downstream(); + if (!downstream || + downstream->get_downstream_stream_id() != frame->hd.stream_id) { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + return 0; + } return 0; } - auto sd = static_cast( - nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); - if (!sd || !sd->dconn) { - http2session->submit_rst_stream(frame->hd.stream_id, - NGHTTP2_INTERNAL_ERROR); + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + auto sd = static_cast( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + + auto downstream = sd->dconn->get_downstream(); + + assert(downstream); + assert(downstream->get_downstream_stream_id() == frame->hd.stream_id); + + if (http2session->handle_downstream_push_promise(downstream, + promised_stream_id) != 0) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + } + return 0; } - auto downstream = sd->dconn->get_downstream(); - if (!downstream || - downstream->get_downstream_stream_id() != frame->hd.stream_id) { - http2session->submit_rst_stream(frame->hd.stream_id, - NGHTTP2_INTERNAL_ERROR); - return 0; } + return 0; } } // namespace @@ -976,7 +1052,8 @@ int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, return 0; } - if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE) { + if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE || + frame->headers.cat == NGHTTP2_HCAT_PUSH_RESPONSE) { rv = on_response_headers(http2session, downstream, session, frame); if (rv != 0) { @@ -1044,17 +1121,47 @@ int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, http2session->connection_alive(); } return 0; - case NGHTTP2_PUSH_PROMISE: + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + if (LOG_ENABLED(INFO)) { SSLOG(INFO, http2session) << "Received downstream PUSH_PROMISE stream_id=" << frame->hd.stream_id - << ", promised_stream_id=" << frame->push_promise.promised_stream_id; + << ", promised_stream_id=" << promised_stream_id; } - // We just respond with RST_STREAM. - http2session->submit_rst_stream(frame->push_promise.promised_stream_id, - NGHTTP2_REFUSED_STREAM); + + auto sd = static_cast( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + + auto downstream = sd->dconn->get_downstream(); + + assert(downstream); + assert(downstream->get_downstream_stream_id() == frame->hd.stream_id); + + auto promised_sd = static_cast( + nghttp2_session_get_stream_user_data(session, promised_stream_id)); + if (!promised_sd || !promised_sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + + auto promised_downstream = promised_sd->dconn->get_downstream(); + + assert(promised_downstream); + + if (http2session->handle_downstream_push_promise_complete( + downstream, promised_downstream) != 0) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + return 0; + } default: return 0; } @@ -1279,16 +1386,22 @@ int Http2Session::connection_made() { flow_control_ = true; std::array entry; - entry[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; - entry[0].value = 0; - entry[1].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; - entry[1].value = get_config()->http2_max_concurrent_streams; + size_t nentry = 2; + entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + entry[0].value = get_config()->http2_max_concurrent_streams; - entry[2].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; - entry[2].value = (1 << get_config()->http2_downstream_window_bits) - 1; + entry[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + entry[1].value = (1 << get_config()->http2_downstream_window_bits) - 1; + + if (get_config()->no_server_push || get_config()->http2_proxy || + get_config()->client_proxy) { + entry[nentry].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; + entry[nentry].value = 0; + ++nentry; + } rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), - entry.size()); + nentry); if (rv != 0) { return -1; } @@ -1771,4 +1884,96 @@ size_t Http2Session::get_group() const { return group_; } size_t Http2Session::get_index() const { return index_; } +int Http2Session::handle_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + auto upstream = downstream->get_upstream(); + if (!upstream->push_enabled()) { + return -1; + } + + auto promised_downstream = + upstream->on_downstream_push_promise(downstream, promised_stream_id); + if (!promised_downstream) { + return -1; + } + + // Now we have Downstream object for pushed stream. + // promised_downstream->get_stream() still returns 0. + + auto handler = upstream->get_client_handler(); + auto worker = handler->get_worker(); + + auto promised_dconn = + make_unique(worker->get_dconn_pool(), this); + promised_dconn->set_client_handler(handler); + + auto ptr = promised_dconn.get(); + + if (promised_downstream->attach_downstream_connection( + std::move(promised_dconn)) != 0) { + return -1; + } + + auto promised_sd = make_unique(); + + nghttp2_session_set_stream_user_data(session_, promised_stream_id, + promised_sd.get()); + + ptr->attach_stream_data(promised_sd.get()); + streams_.append(promised_sd.release()); + + return 0; +} + +int Http2Session::handle_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + auto authority = + promised_downstream->get_request_header(http2::HD__AUTHORITY); + auto path = promised_downstream->get_request_header(http2::HD__PATH); + auto method = promised_downstream->get_request_header(http2::HD__METHOD); + auto scheme = promised_downstream->get_request_header(http2::HD__SCHEME); + + if (!authority) { + authority = promised_downstream->get_request_header(http2::HD_HOST); + } + + auto method_token = http2::lookup_method_token(method->value); + if (method_token == -1) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Unrecognized method: " << method->value; + } + + return -1; + } + + // TODO Rewrite authority if we enabled rewrite host. But we + // really don't know how to rewrite host. Should we use the same + // host in associated stream? + promised_downstream->set_request_http2_authority( + http2::value_to_str(authority)); + promised_downstream->set_request_method(method_token); + // libnghttp2 ensures that we don't have CONNECT method in + // PUSH_PROMISE, and guarantees that :scheme exists. + promised_downstream->set_request_http2_scheme(http2::value_to_str(scheme)); + + // For server-wide OPTIONS request, path is empty. + if (method_token != HTTP_OPTIONS || path->value != "*") { + promised_downstream->set_request_path(http2::rewrite_clean_path( + std::begin(path->value), std::end(path->value))); + } + + promised_downstream->inspect_http2_request(); + + auto upstream = promised_downstream->get_upstream(); + + promised_downstream->set_request_state(Downstream::MSG_COMPLETE); + + if (upstream->on_downstream_push_promise_complete(downstream, + promised_downstream) != 0) { + return -1; + } + + return 0; +} + } // namespace shrpx diff --git a/src/shrpx_http2_session.h b/src/shrpx_http2_session.h index 3ae3e784..e321d59e 100644 --- a/src/shrpx_http2_session.h +++ b/src/shrpx_http2_session.h @@ -156,6 +156,11 @@ public: size_t get_index() const; + int handle_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + int handle_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + enum { // Disconnected DISCONNECTED, diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index 42a347fb..bc0de69c 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -543,6 +543,13 @@ int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, return 0; case NGHTTP2_PUSH_PROMISE: { auto promised_stream_id = frame->push_promise.promised_stream_id; + + if (nghttp2_session_get_stream_user_data(session, promised_stream_id)) { + // In case of push from backend, downstream object was already + // created. + return 0; + } + auto downstream = make_unique(upstream, handler->get_mcpool(), promised_stream_id, 0); @@ -1712,6 +1719,7 @@ int Http2Upstream::on_downstream_reset(bool no_retry) { } if (!downstream->request_submission_ready()) { + // pushed stream is handled here rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); downstream->pop_downstream_connection(); continue; @@ -1798,7 +1806,6 @@ int Http2Upstream::submit_push_promise(const std::string &scheme, const std::string &authority, const std::string &path, Downstream *downstream) { - int rv; std::vector nva; // 4 for :method, :scheme, :path and :authority nva.reserve(4 + downstream->get_request_headers().size()); @@ -1827,16 +1834,16 @@ int Http2Upstream::submit_push_promise(const std::string &scheme, } } - rv = nghttp2_submit_push_promise(session_, NGHTTP2_FLAG_NONE, - downstream->get_stream_id(), nva.data(), - nva.size(), nullptr); + auto promised_stream_id = nghttp2_submit_push_promise( + session_, NGHTTP2_FLAG_NONE, downstream->get_stream_id(), nva.data(), + nva.size(), nullptr); - if (rv < 0) { + if (promised_stream_id < 0) { if (LOG_ENABLED(INFO)) { ULOG(INFO, this) << "nghttp2_submit_push_promise() failed: " - << nghttp2_strerror(rv); + << nghttp2_strerror(promised_stream_id); } - if (nghttp2_is_fatal(rv)) { + if (nghttp2_is_fatal(promised_stream_id)) { return -1; } return 0; @@ -1847,22 +1854,25 @@ int Http2Upstream::submit_push_promise(const std::string &scheme, for (auto &nv : nva) { ss << TTY_HTTP_HD << nv.name << TTY_RST << ": " << nv.value << "\n"; } - ULOG(INFO, this) << "HTTP push request headers. promised_stream_id=" << rv - << "\n" << ss.str(); + ULOG(INFO, this) << "HTTP push request headers. promised_stream_id=" + << promised_stream_id << "\n" << ss.str(); } return 0; } +bool Http2Upstream::push_enabled() const { + return !(get_config()->no_server_push || + nghttp2_session_get_remote_settings( + session_, NGHTTP2_SETTINGS_ENABLE_PUSH) == 0 || + get_config()->http2_proxy || get_config()->client_proxy); +} + int Http2Upstream::initiate_push(Downstream *downstream, const char *uri, size_t len) { int rv; - if (len == 0 || get_config()->no_server_push || - nghttp2_session_get_remote_settings(session_, - NGHTTP2_SETTINGS_ENABLE_PUSH) == 0 || - get_config()->http2_proxy || get_config()->client_proxy || - (downstream->get_stream_id() % 2) == 0) { + if (len == 0 || !push_enabled() || (downstream->get_stream_id() % 2)) { return 0; } @@ -1917,4 +1927,58 @@ bool Http2Upstream::response_empty() const { return wb_.rleft() == 0; } Http2Upstream::WriteBuffer *Http2Upstream::get_response_buf() { return &wb_; } +Downstream * +Http2Upstream::on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + // promised_stream_id is for backend HTTP/2 session, not for + // frontend. + auto promised_downstream = + make_unique(this, handler_->get_mcpool(), 0, 0); + promised_downstream->set_downstream_stream_id(promised_stream_id); + + promised_downstream->disable_upstream_rtimer(); + + promised_downstream->set_request_major(2); + promised_downstream->set_request_minor(0); + + auto ptr = promised_downstream.get(); + add_pending_downstream(std::move(promised_downstream)); + downstream_queue_.mark_active(ptr); + + return ptr; +} + +int Http2Upstream::on_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + std::vector nva; + + auto &headers = promised_downstream->get_request_headers(); + + nva.reserve(headers.size()); + + for (auto &kv : headers) { + nva.push_back(http2::make_nv_nocopy(kv.name, kv.value, kv.no_index)); + } + + auto promised_stream_id = nghttp2_submit_push_promise( + session_, NGHTTP2_FLAG_NONE, downstream->get_stream_id(), nva.data(), + nva.size(), promised_downstream); + if (promised_stream_id < 0) { + return -1; + } + + promised_downstream->set_stream_id(promised_stream_id); + + return 0; +} + +void Http2Upstream::cancel_premature_downstream( + Downstream *promised_downstream) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Remove premature promised stream " + << promised_downstream; + } + downstream_queue_.remove_and_get_blocked(promised_downstream, false); +} + } // namespace shrpx diff --git a/src/shrpx_http2_upstream.h b/src/shrpx_http2_upstream.h index d49455cc..e2527626 100644 --- a/src/shrpx_http2_upstream.h +++ b/src/shrpx_http2_upstream.h @@ -87,6 +87,14 @@ public: virtual void response_drain(size_t n); virtual bool response_empty() const; + virtual Downstream *on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + virtual bool push_enabled() const; + virtual void cancel_premature_downstream(Downstream *promised_downstream); + bool get_flow_control() const; // Perform HTTP/2 upgrade from |upstream|. On success, this object // takes ownership of the |upstream|. This function returns 0 if it diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc index 25184427..2f9cd831 100644 --- a/src/shrpx_https_upstream.cc +++ b/src/shrpx_https_upstream.cc @@ -1191,4 +1191,20 @@ bool HttpsUpstream::response_empty() const { return buf->rleft() == 0; } +Downstream * +HttpsUpstream::on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + return nullptr; +} + +int HttpsUpstream::on_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + return -1; +} + +bool HttpsUpstream::push_enabled() const { return false; } + +void HttpsUpstream::cancel_premature_downstream( + Downstream *promised_downstream) {} + } // namespace shrpx diff --git a/src/shrpx_https_upstream.h b/src/shrpx_https_upstream.h index 6f3b5742..3ab698bc 100644 --- a/src/shrpx_https_upstream.h +++ b/src/shrpx_https_upstream.h @@ -82,6 +82,14 @@ public: virtual void response_drain(size_t n); virtual bool response_empty() const; + virtual Downstream *on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + virtual bool push_enabled() const; + virtual void cancel_premature_downstream(Downstream *promised_downstream); + void reset_current_header_length(); void log_response_headers(DefaultMemchunks *buf) const; diff --git a/src/shrpx_log.cc b/src/shrpx_log.cc index 59095b51..6055f3e9 100644 --- a/src/shrpx_log.cc +++ b/src/shrpx_log.cc @@ -297,7 +297,7 @@ void upstream_accesslog(const std::vector &lfv, if (frac.size() < 3) { frac = std::string(3 - frac.size(), '0') + frac; } - sec += "."; + sec += '.'; sec += frac; std::tie(p, avail) = copy(sec, avail, p); @@ -415,12 +415,12 @@ void log_chld(pid_t pid, int rstatus, const char *msg) { auto s = strsignal(sig); if (s) { signalstr += s; - signalstr += "("; + signalstr += '('; } else { signalstr += "UNKNOWN("; } signalstr += util::utos(sig); - signalstr += ")"; + signalstr += ')'; } LOG(NOTICE) << msg << ": [" << pid << "] exited " diff --git a/src/shrpx_spdy_upstream.cc b/src/shrpx_spdy_upstream.cc index 2ce0491c..d7d2269a 100644 --- a/src/shrpx_spdy_upstream.cc +++ b/src/shrpx_spdy_upstream.cc @@ -1230,4 +1230,20 @@ bool SpdyUpstream::response_empty() const { return wb_.rleft() == 0; } SpdyUpstream::WriteBuffer *SpdyUpstream::get_response_buf() { return &wb_; } +Downstream * +SpdyUpstream::on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + return nullptr; +} + +int SpdyUpstream::on_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + return -1; +} + +bool SpdyUpstream::push_enabled() const { return false; } + +void SpdyUpstream::cancel_premature_downstream( + Downstream *promised_downstream) {} + } // namespace shrpx diff --git a/src/shrpx_spdy_upstream.h b/src/shrpx_spdy_upstream.h index 7d509340..9dfcc1b2 100644 --- a/src/shrpx_spdy_upstream.h +++ b/src/shrpx_spdy_upstream.h @@ -82,6 +82,14 @@ public: virtual void response_drain(size_t n); virtual bool response_empty() const; + virtual Downstream *on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + virtual bool push_enabled() const; + virtual void cancel_premature_downstream(Downstream *promised_downstream); + bool get_flow_control() const; int consume(int32_t stream_id, size_t len); diff --git a/src/shrpx_ssl.cc b/src/shrpx_ssl.cc index 8c4b4166..0a7ca6ac 100644 --- a/src/shrpx_ssl.cc +++ b/src/shrpx_ssl.cc @@ -792,7 +792,7 @@ bool tls_hostname_match(const char *pattern, const char *hostname) { // Don't attempt to match a presented identifier where the wildcard // character is embedded within an A-label. if (ptLeftLabelEnd == 0 || strchr(ptLeftLabelEnd + 1, '.') == 0 || - ptLeftLabelEnd < ptWildcard || util::istartsWith(pattern, "xn--")) { + ptLeftLabelEnd < ptWildcard || util::istarts_with(pattern, "xn--")) { wildcardEnabled = false; } if (!wildcardEnabled) { @@ -807,9 +807,9 @@ bool tls_hostname_match(const char *pattern, const char *hostname) { if (hnLeftLabelEnd - hostname < ptLeftLabelEnd - pattern) { return false; } - return util::istartsWith(hostname, hnLeftLabelEnd, pattern, ptWildcard) && - util::iendsWith(hostname, hnLeftLabelEnd, ptWildcard + 1, - ptLeftLabelEnd); + return util::istarts_with(hostname, hnLeftLabelEnd, pattern, ptWildcard) && + util::iends_with(hostname, hnLeftLabelEnd, ptWildcard + 1, + ptLeftLabelEnd); } } // namespace diff --git a/src/shrpx_upstream.h b/src/shrpx_upstream.h index 92fe5190..b9639e72 100644 --- a/src/shrpx_upstream.h +++ b/src/shrpx_upstream.h @@ -76,6 +76,28 @@ public: virtual int response_riovec(struct iovec *iov, int iovcnt) const = 0; virtual void response_drain(size_t n) = 0; virtual bool response_empty() const = 0; + + // Called when PUSH_PROMISE was started in downstream. The + // associated downstream is given as |downstream|. The promised + // stream ID is given as |promised_stream_id|. If upstream supports + // server push for the corresponding upstream connection, it should + // return Downstream object for pushed stream. Otherwise, returns + // nullptr. + virtual Downstream * + on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) = 0; + // Called when PUSH_PROMISE frame was completely received in + // downstream. The associated downstream is given as |downstream|. + // This function returns 0 if it succeeds, or -1. + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream) = 0; + // Returns true if server push is enabled in upstream connection. + virtual bool push_enabled() const = 0; + // Cancels promised downstream. This function is called when + // PUSH_PROMISE for |promised_downstream| is not submitted to + // upstream session. + virtual void cancel_premature_downstream(Downstream *promised_downstream) = 0; }; } // namespace shrpx diff --git a/src/util.cc b/src/util.cc index 1a040cf5..a2d649ef 100644 --- a/src/util.cc +++ b/src/util.cc @@ -63,32 +63,31 @@ namespace nghttp2 { namespace util { -const char DEFAULT_STRIP_CHARSET[] = "\r\n\t "; - const char UPPER_XDIGITS[] = "0123456789ABCDEF"; -bool inRFC3986UnreservedChars(const char c) { - static const char unreserved[] = {'-', '.', '_', '~'}; - return isAlpha(c) || isDigit(c) || - std::find(&unreserved[0], &unreserved[4], c) != &unreserved[4]; +bool in_rfc3986_unreserved_chars(const char c) { + static constexpr const char unreserved[] = {'-', '.', '_', '~'}; + return is_alpha(c) || is_digit(c) || + std::find(std::begin(unreserved), std::end(unreserved), c) != + std::end(unreserved); } bool in_rfc3986_sub_delims(const char c) { - static const char sub_delims[] = {'!', '$', '&', '\'', '(', ')', - '*', '+', ',', ';', '='}; + static constexpr const char sub_delims[] = {'!', '$', '&', '\'', '(', ')', + '*', '+', ',', ';', '='}; return std::find(std::begin(sub_delims), std::end(sub_delims), c) != std::end(sub_delims); } -std::string percentEncode(const unsigned char *target, size_t len) { +std::string percent_encode(const unsigned char *target, size_t len) { std::string dest; for (size_t i = 0; i < len; ++i) { unsigned char c = target[i]; - if (inRFC3986UnreservedChars(c)) { + if (in_rfc3986_unreserved_chars(c)) { dest += c; } else { - dest += "%"; + dest += '%'; dest += UPPER_XDIGITS[c >> 4]; dest += UPPER_XDIGITS[(c & 0x0f)]; } @@ -96,20 +95,21 @@ std::string percentEncode(const unsigned char *target, size_t len) { return dest; } -std::string percentEncode(const std::string &target) { - return percentEncode(reinterpret_cast(target.c_str()), - target.size()); +std::string percent_encode(const std::string &target) { + return percent_encode(reinterpret_cast(target.c_str()), + target.size()); } std::string percent_encode_path(const std::string &s) { std::string dest; for (auto c : s) { - if (inRFC3986UnreservedChars(c) || in_rfc3986_sub_delims(c) || c == '/') { + if (in_rfc3986_unreserved_chars(c) || in_rfc3986_sub_delims(c) || + c == '/') { dest += c; continue; } - dest += "%"; + dest += '%'; dest += UPPER_XDIGITS[(c >> 4) & 0x0f]; dest += UPPER_XDIGITS[(c & 0x0f)]; } @@ -117,18 +117,17 @@ std::string percent_encode_path(const std::string &s) { } bool in_token(char c) { - static const char extra[] = {'!', '#', '$', '%', '&', '\'', '*', '+', - '-', '.', '^', '_', '`', '|', '~'}; - - return isAlpha(c) || isDigit(c) || - std::find(&extra[0], &extra[sizeof(extra)], c) != - &extra[sizeof(extra)]; + static constexpr const char extra[] = {'!', '#', '$', '%', '&', + '\'', '*', '+', '-', '.', + '^', '_', '`', '|', '~'}; + return is_alpha(c) || is_digit(c) || + std::find(std::begin(extra), std::end(extra), c) != std::end(extra); } bool in_attr_char(char c) { - static const char bad[] = {'*', '\'', '%'}; + static constexpr const char bad[] = {'*', '\'', '%'}; return util::in_token(c) && - std::find(std::begin(bad), std::end(bad) - 1, c) == std::end(bad) - 1; + std::find(std::begin(bad), std::end(bad), c) == std::end(bad); } std::string percent_encode_token(const std::string &target) { @@ -141,7 +140,7 @@ std::string percent_encode_token(const std::string &target) { if (c != '%' && in_token(c)) { dest += c; } else { - dest += "%"; + dest += '%'; dest += UPPER_XDIGITS[c >> 4]; dest += UPPER_XDIGITS[(c & 0x0f)]; } @@ -354,7 +353,7 @@ void streq_advance(const char **ap, const char **bp) { } } // namespace -bool istartsWith(const char *a, const char *b) { +bool istarts_with(const char *a, const char *b) { if (!a || !b) { return false; } @@ -510,8 +509,8 @@ void show_candidates(const char *unkopt, option *options) { for (size_t i = 0; options[i].name != nullptr; ++i) { auto optnamelen = strlen(options[i].name); // Use cost 0 for prefix match - if (istartsWith(options[i].name, options[i].name + optnamelen, unkopt, - unkopt + unkoptlen)) { + if (istarts_with(options[i].name, options[i].name + optnamelen, unkopt, + unkopt + unkoptlen)) { if (optnamelen == static_cast(unkoptlen)) { // Exact match, then we don't show any condidates. return; @@ -522,8 +521,8 @@ void show_candidates(const char *unkopt, option *options) { } // Use cost 0 for suffix match, but match at least 3 characters if (unkoptlen >= 3 && - iendsWith(options[i].name, options[i].name + optnamelen, unkopt, - unkopt + unkoptlen)) { + iends_with(options[i].name, options[i].name + optnamelen, unkopt, + unkopt + unkoptlen)) { cands.emplace_back(0, options[i].name); continue; } @@ -712,7 +711,7 @@ std::string ascii_dump(const uint8_t *data, size_t len) { if (c >= 0x20 && c < 0x7f) { res += c; } else { - res += "."; + res += '.'; } } @@ -755,7 +754,7 @@ bool check_path(const std::string &path) { path.find('\\') == std::string::npos && path.find("/../") == std::string::npos && path.find("/./") == std::string::npos && - !util::endsWith(path, "/..") && !util::endsWith(path, "/."); + !util::ends_with(path, "/..") && !util::ends_with(path, "/."); } int64_t to_time64(const timeval &tv) { @@ -1093,6 +1092,20 @@ std::string format_duration(const std::chrono::microseconds &u) { return dtos(static_cast(t) / d) + unit; } +std::string format_duration(double t) { + const char *unit = "us"; + if (t >= 1.) { + unit = "s"; + } else if (t >= 0.001) { + t *= 1000.; + unit = "ms"; + } else { + t *= 1000000.; + return utos(static_cast(t)) + unit; + } + return dtos(t) + unit; +} + std::string dtos(double n) { auto f = utos(static_cast(round(100. * n)) % 100); return utos(static_cast(n)) + "." + (f.size() == 1 ? "0" : "") + f; @@ -1103,17 +1116,17 @@ std::string make_hostport(const char *host, uint16_t port) { std::string hostport; if (ipv6) { - hostport += "["; + hostport += '['; } hostport += host; if (ipv6) { - hostport += "]"; + hostport += ']'; } if (port != 80 && port != 443) { - hostport += ":"; + hostport += ':'; hostport += utos(port); } @@ -1245,8 +1258,13 @@ int read_mime_types(std::map &res, break; } ext_end = std::find_if(ext_start, std::end(line), delim_pred); +#ifdef HAVE_STD_MAP_EMPLACE res.emplace(std::string(ext_start, ext_end), std::string(std::begin(line), type_end)); +#else // !HAVE_STD_MAP_EMPLACE + res.insert(std::make_pair(std::string(ext_start, ext_end), + std::string(std::begin(line), type_end))); +#endif // !HAVE_STD_MAP_EMPLACE } } diff --git a/src/util.h b/src/util.h index 8eb88cdd..8efd7407 100644 --- a/src/util.h +++ b/src/util.h @@ -64,115 +64,17 @@ constexpr const char NGHTTP2_H1_1[] = "http/1.1"; namespace util { -extern const char DEFAULT_STRIP_CHARSET[]; - -template -std::pair -stripIter(InputIterator first, InputIterator last, - const char *chars = DEFAULT_STRIP_CHARSET) { - for (; first != last && strchr(chars, *first) != 0; ++first) - ; - if (first == last) { - return std::make_pair(first, last); - } - InputIterator left = last - 1; - for (; left != first && strchr(chars, *left) != 0; --left) - ; - return std::make_pair(first, left + 1); -} - -template -OutputIterator splitIter(InputIterator first, InputIterator last, - OutputIterator out, char delim, bool doStrip = false, - bool allowEmpty = false) { - for (InputIterator i = first; i != last;) { - InputIterator j = std::find(i, last, delim); - std::pair p(i, j); - if (doStrip) { - p = stripIter(i, j); - } - if (allowEmpty || p.first != p.second) { - *out++ = p; - } - i = j; - if (j != last) { - ++i; - } - } - if (allowEmpty && (first == last || *(last - 1) == delim)) { - *out++ = std::make_pair(last, last); - } - return out; -} - -template -OutputIterator split(InputIterator first, InputIterator last, - OutputIterator out, char delim, bool doStrip = false, - bool allowEmpty = false) { - for (InputIterator i = first; i != last;) { - InputIterator j = std::find(i, last, delim); - std::pair p(i, j); - if (doStrip) { - p = stripIter(i, j); - } - if (allowEmpty || p.first != p.second) { - *out++ = std::string(p.first, p.second); - } - i = j; - if (j != last) { - ++i; - } - } - if (allowEmpty && (first == last || *(last - 1) == delim)) { - *out++ = std::string(last, last); - } - return out; -} - -template -std::string strjoin(InputIterator first, InputIterator last, - const DelimiterType &delim) { - std::string result; - if (first == last) { - return result; - } - InputIterator beforeLast = last - 1; - for (; first != beforeLast; ++first) { - result += *first; - result += delim; - } - result += *beforeLast; - return result; -} - -template -std::string joinPath(InputIterator first, InputIterator last) { - std::vector elements; - for (; first != last; ++first) { - if (*first == "..") { - if (!elements.empty()) { - elements.pop_back(); - } - } else if (*first == ".") { - // do nothing - } else { - elements.push_back(*first); - } - } - return strjoin(elements.begin(), elements.end(), "/"); -} - -inline bool isAlpha(const char c) { +inline bool is_alpha(const char c) { return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'); } -inline bool isDigit(const char c) { return '0' <= c && c <= '9'; } +inline bool is_digit(const char c) { return '0' <= c && c <= '9'; } -inline bool isHexDigit(const char c) { - return isDigit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); +inline bool is_hex_digit(const char c) { + return is_digit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); } -bool inRFC3986UnreservedChars(const char c); +bool in_rfc3986_unreserved_chars(const char c); bool in_rfc3986_sub_delims(const char c); @@ -182,23 +84,23 @@ bool in_token(char c); bool in_attr_char(char c); // Returns integer corresponding to hex notation |c|. It is undefined -// if isHexDigit(c) is false. +// if is_hex_digit(c) is false. uint32_t hex_to_uint(char c); -std::string percentEncode(const unsigned char *target, size_t len); +std::string percent_encode(const unsigned char *target, size_t len); -std::string percentEncode(const std::string &target); +std::string percent_encode(const std::string &target); // percent-encode path component of URI |s|. std::string percent_encode_path(const std::string &s); template -std::string percentDecode(InputIt first, InputIt last) { +std::string percent_decode(InputIt first, InputIt last) { std::string result; for (; first != last; ++first) { if (*first == '%') { - if (first + 1 != last && first + 2 != last && isHexDigit(*(first + 1)) && - isHexDigit(*(first + 2))) { + if (first + 1 != last && first + 2 != last && + is_hex_digit(*(first + 1)) && is_hex_digit(*(first + 2))) { result += (hex_to_uint(*(first + 1)) << 4) + hex_to_uint(*(first + 2)); first += 2; continue; @@ -267,20 +169,20 @@ inline char lowcase(char c) { } template -bool startsWith(InputIterator1 first1, InputIterator1 last1, - InputIterator2 first2, InputIterator2 last2) { +bool starts_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { if (last1 - first1 < last2 - first2) { return false; } return std::equal(first2, last2, first1); } -inline bool startsWith(const std::string &a, const std::string &b) { - return startsWith(std::begin(a), std::end(a), std::begin(b), std::end(b)); +inline bool starts_with(const std::string &a, const std::string &b) { + return starts_with(std::begin(a), std::end(a), std::begin(b), std::end(b)); } -inline bool startsWith(const char *a, const char *b) { - return startsWith(a, a + strlen(a), b, b + strlen(b)); +inline bool starts_with(const char *a, const char *b) { + return starts_with(a, a + strlen(a), b, b + strlen(b)); } struct CaseCmp { @@ -290,49 +192,49 @@ struct CaseCmp { }; template -bool istartsWith(InputIterator1 first1, InputIterator1 last1, - InputIterator2 first2, InputIterator2 last2) { +bool istarts_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { if (last1 - first1 < last2 - first2) { return false; } return std::equal(first2, last2, first1, CaseCmp()); } -inline bool istartsWith(const std::string &a, const std::string &b) { - return istartsWith(std::begin(a), std::end(a), std::begin(b), std::end(b)); +inline bool istarts_with(const std::string &a, const std::string &b) { + return istarts_with(std::begin(a), std::end(a), std::begin(b), std::end(b)); } template -bool istartsWith(InputIt a, size_t an, const char *b) { - return istartsWith(a, a + an, b, b + strlen(b)); +bool istarts_with(InputIt a, size_t an, const char *b) { + return istarts_with(a, a + an, b, b + strlen(b)); } -bool istartsWith(const char *a, const char *b); +bool istarts_with(const char *a, const char *b); template -bool endsWith(InputIterator1 first1, InputIterator1 last1, - InputIterator2 first2, InputIterator2 last2) { +bool ends_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { if (last1 - first1 < last2 - first2) { return false; } return std::equal(first2, last2, last1 - (last2 - first2)); } -inline bool endsWith(const std::string &a, const std::string &b) { - return endsWith(std::begin(a), std::end(a), std::begin(b), std::end(b)); +inline bool ends_with(const std::string &a, const std::string &b) { + return ends_with(std::begin(a), std::end(a), std::begin(b), std::end(b)); } template -bool iendsWith(InputIterator1 first1, InputIterator1 last1, - InputIterator2 first2, InputIterator2 last2) { +bool iends_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { if (last1 - first1 < last2 - first2) { return false; } return std::equal(first2, last2, last1 - (last2 - first2), CaseCmp()); } -inline bool iendsWith(const std::string &a, const std::string &b) { - return iendsWith(std::begin(a), std::end(a), std::begin(b), std::end(b)); +inline bool iends_with(const std::string &a, const std::string &b) { + return iends_with(std::begin(a), std::end(a), std::begin(b), std::end(b)); } int strcompare(const char *a, const uint8_t *b, size_t n); @@ -683,6 +585,9 @@ std::string duration_str(double t); // fractional digits follow. std::string format_duration(const std::chrono::microseconds &u); +// Just like above, but this takes |t| as seconds. +std::string format_duration(double t); + // Creates "host:port" string using given |host| and |port|. If // |host| is numeric IPv6 address (e.g., ::1), it is enclosed by "[" // and "]". If |port| is 80 or 443, port part is omitted. diff --git a/src/util_test.cc b/src/util_test.cc index da2485a9..9732ff15 100644 --- a/src/util_test.cc +++ b/src/util_test.cc @@ -147,15 +147,15 @@ void test_util_percent_encode_path(void) { void test_util_percent_decode(void) { { std::string s = "%66%6F%6f%62%61%72"; - CU_ASSERT("foobar" == util::percentDecode(std::begin(s), std::end(s))); + CU_ASSERT("foobar" == util::percent_decode(std::begin(s), std::end(s))); } { std::string s = "%66%6"; - CU_ASSERT("f%6" == util::percentDecode(std::begin(s), std::end(s))); + CU_ASSERT("f%6" == util::percent_decode(std::begin(s), std::end(s))); } { std::string s = "%66%"; - CU_ASSERT("f%" == util::percentDecode(std::begin(s), std::end(s))); + CU_ASSERT("f%" == util::percent_decode(std::begin(s), std::end(s))); } } @@ -341,30 +341,39 @@ void test_util_format_duration(void) { util::format_duration(std::chrono::microseconds(1000000))); CU_ASSERT("1.05s" == util::format_duration(std::chrono::microseconds(1050000))); + + CU_ASSERT("0us" == util::format_duration(0.)); + CU_ASSERT("999us" == util::format_duration(0.000999)); + CU_ASSERT("1.00ms" == util::format_duration(0.001)); + CU_ASSERT("1.09ms" == util::format_duration(0.00109)); + CU_ASSERT("1.01ms" == util::format_duration(0.001009)); + CU_ASSERT("999.99ms" == util::format_duration(0.99999)); + CU_ASSERT("1.00s" == util::format_duration(1.)); + CU_ASSERT("1.05s" == util::format_duration(1.05)); } void test_util_starts_with(void) { - CU_ASSERT(util::startsWith("foo", "foo")); - CU_ASSERT(util::startsWith("fooo", "foo")); - CU_ASSERT(util::startsWith("ofoo", "")); - CU_ASSERT(!util::startsWith("ofoo", "foo")); + CU_ASSERT(util::starts_with("foo", "foo")); + CU_ASSERT(util::starts_with("fooo", "foo")); + CU_ASSERT(util::starts_with("ofoo", "")); + CU_ASSERT(!util::starts_with("ofoo", "foo")); - CU_ASSERT(util::istartsWith("FOO", "fOO")); - CU_ASSERT(util::startsWith("ofoo", "")); - CU_ASSERT(util::istartsWith("fOOo", "Foo")); - CU_ASSERT(!util::istartsWith("ofoo", "foo")); + CU_ASSERT(util::istarts_with("FOO", "fOO")); + CU_ASSERT(util::starts_with("ofoo", "")); + CU_ASSERT(util::istarts_with("fOOo", "Foo")); + CU_ASSERT(!util::istarts_with("ofoo", "foo")); } void test_util_ends_with(void) { - CU_ASSERT(util::endsWith("foo", "foo")); - CU_ASSERT(util::endsWith("foo", "")); - CU_ASSERT(util::endsWith("ofoo", "foo")); - CU_ASSERT(!util::endsWith("ofoo", "fo")); + CU_ASSERT(util::ends_with("foo", "foo")); + CU_ASSERT(util::ends_with("foo", "")); + CU_ASSERT(util::ends_with("ofoo", "foo")); + CU_ASSERT(!util::ends_with("ofoo", "fo")); - CU_ASSERT(util::iendsWith("fOo", "Foo")); - CU_ASSERT(util::iendsWith("foo", "")); - CU_ASSERT(util::iendsWith("oFoo", "fOO")); - CU_ASSERT(!util::iendsWith("ofoo", "fo")); + CU_ASSERT(util::iends_with("fOo", "Foo")); + CU_ASSERT(util::iends_with("foo", "")); + CU_ASSERT(util::iends_with("oFoo", "fOO")); + CU_ASSERT(!util::iends_with("ofoo", "fo")); } void test_util_parse_http_date(void) { diff --git a/tests/main.c b/tests/main.c index b290dbf1..db7ef68c 100644 --- a/tests/main.c +++ b/tests/main.c @@ -87,6 +87,8 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_session_recv_continuation) || !CU_add_test(pSuite, "session_recv_headers_with_priority", 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_recv_premature_headers", test_nghttp2_session_recv_premature_headers) || !CU_add_test(pSuite, "session_recv_unknown_frame", @@ -161,6 +163,8 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_submit_response_with_data) || !CU_add_test(pSuite, "submit_response_without_data", test_nghttp2_submit_response_without_data) || + !CU_add_test(pSuite, "Submit_response_push_response", + test_nghttp2_submit_response_push_response) || !CU_add_test(pSuite, "submit_trailer", test_nghttp2_submit_trailer) || !CU_add_test(pSuite, "submit_headers_start_stream", test_nghttp2_submit_headers_start_stream) || @@ -252,6 +256,8 @@ int main(int argc _U_, char *argv[] _U_) { test_nghttp2_session_stream_get_state) || !CU_add_test(pSuite, "session_stream_get_something", test_nghttp2_session_stream_get_something) || + !CU_add_test(pSuite, "session_find_stream", + test_nghttp2_session_find_stream) || !CU_add_test(pSuite, "session_keep_closed_stream", test_nghttp2_session_keep_closed_stream) || !CU_add_test(pSuite, "session_keep_idle_stream", @@ -283,6 +289,8 @@ int main(int argc _U_, char *argv[] _U_) { !CU_add_test(pSuite, "session_detach_item_from_closed_stream", test_nghttp2_session_detach_item_from_closed_stream) || !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, "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 86c9e711..1033ea4d 100644 --- a/tests/nghttp2_session_test.c +++ b/tests/nghttp2_session_test.c @@ -1408,6 +1408,85 @@ void test_nghttp2_session_recv_headers_with_priority(void) { nghttp2_session_del(session); } +static int response_on_begin_frame_callback(nghttp2_session *session, + const nghttp2_frame_hd *hd, + void *user_data _U_) { + int rv; + + if (hd->type != NGHTTP2_HEADERS) { + return 0; + } + + rv = nghttp2_submit_response(session, hd->stream_id, resnv, ARRLEN(resnv), + NULL); + + CU_ASSERT(0 == rv); + + return 0; +} + +void test_nghttp2_session_recv_headers_early_response(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_bufs bufs; + nghttp2_buf *buf; + nghttp2_hd_deflater deflater; + nghttp2_mem *mem; + nghttp2_nv *nva; + size_t nvlen; + nghttp2_frame frame; + ssize_t rv; + nghttp2_stream *stream; + + mem = nghttp2_mem_default(); + frame_pack_bufs_init(&bufs); + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_begin_frame_callback = response_on_begin_frame_callback; + + nghttp2_session_server_new(&session, &callbacks, NULL); + + nghttp2_hd_deflate_init(&deflater, mem); + + nvlen = ARRLEN(reqnv); + nghttp2_nv_array_copy(&nva, reqnv, nvlen, mem); + nghttp2_frame_headers_init(&frame.headers, + NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_END_STREAM, + 1, NGHTTP2_HCAT_REQUEST, NULL, nva, nvlen); + + rv = nghttp2_frame_pack_headers(&bufs, &frame.headers, &deflater); + + CU_ASSERT(0 == rv); + + nghttp2_frame_headers_free(&frame.headers, mem); + + buf = &bufs.head->buf; + + /* Only receive 9 bytes headers, and invoke + on_begin_frame_callback */ + rv = nghttp2_session_mem_recv(session, buf->pos, 9); + + CU_ASSERT(9 == rv); + + rv = nghttp2_session_send(session); + + CU_ASSERT(0 == rv); + + rv = + nghttp2_session_mem_recv(session, buf->pos + 9, nghttp2_buf_len(buf) - 9); + + CU_ASSERT((ssize_t)nghttp2_buf_len(buf) - 9 == rv); + + stream = nghttp2_session_get_stream_raw(session, 1); + + CU_ASSERT(stream->flags & NGHTTP2_STREAM_FLAG_CLOSED); + + nghttp2_hd_deflate_free(&deflater); + nghttp2_session_del(session); + nghttp2_bufs_free(&bufs); +} + void test_nghttp2_session_recv_premature_headers(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; @@ -2765,7 +2844,7 @@ void test_nghttp2_session_on_push_promise_received(void) { item = nghttp2_session_get_next_ob_item(session); CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); CU_ASSERT(6 == item->frame.hd.stream_id); - CU_ASSERT(NGHTTP2_REFUSED_STREAM == item->frame.rst_stream.error_code); + 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 */ @@ -2939,7 +3018,7 @@ void test_nghttp2_session_on_push_promise_received(void) { item = nghttp2_session_get_next_ob_item(session); CU_ASSERT(NGHTTP2_RST_STREAM == item->frame.hd.type); - CU_ASSERT(NGHTTP2_REFUSED_STREAM == item->frame.rst_stream.error_code); + CU_ASSERT(NGHTTP2_CANCEL == item->frame.rst_stream.error_code); nghttp2_frame_push_promise_free(&frame.push_promise, mem); nghttp2_session_del(session); @@ -3580,6 +3659,15 @@ void test_nghttp2_session_reprioritize_stream(void) { CU_ASSERT(128 == stream->weight); CU_ASSERT(dep_stream == stream->dep_prev); + /* Change weight again to test short-path case */ + pri_spec.weight = 100; + + nghttp2_session_reprioritize_stream(session, stream, &pri_spec); + + CU_ASSERT(100 == stream->weight); + CU_ASSERT(dep_stream == stream->dep_prev); + CU_ASSERT(100 == dep_stream->sum_dep_weight); + /* Test circular dependency; stream 1 is first removed and becomes root. Then stream 3 depends on it. */ nghttp2_priority_spec_init(&pri_spec, 1, 1, 0); @@ -4012,6 +4100,33 @@ void test_nghttp2_submit_response_without_data(void) { nghttp2_session_del(session); } +void test_nghttp2_submit_response_push_response(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + my_user_data ud; + + memset(&callbacks, 0, sizeof(nghttp2_session_callbacks)); + callbacks.send_callback = null_send_callback; + callbacks.on_frame_not_send_callback = on_frame_not_send_callback; + + nghttp2_session_server_new(&session, &callbacks, &ud); + + nghttp2_session_open_stream(session, 2, NGHTTP2_FLAG_NONE, &pri_spec_default, + NGHTTP2_STREAM_RESERVED, NULL); + + session->goaway_flags |= NGHTTP2_GOAWAY_RECV; + + CU_ASSERT(0 == + nghttp2_submit_response(session, 2, resnv, ARRLEN(resnv), NULL)); + + ud.frame_not_send_cb_called = 0; + + CU_ASSERT(0 == nghttp2_session_send(session)); + CU_ASSERT(1 == ud.frame_not_send_cb_called); + + nghttp2_session_del(session); +} + void test_nghttp2_submit_trailer(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; @@ -4572,28 +4687,28 @@ void test_nghttp2_submit_push_promise(void) { CU_ASSERT(2 == nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 1, reqnv, ARRLEN(reqnv), &ud)); + stream = nghttp2_session_get_stream(session, 2); + + CU_ASSERT(NULL != stream); + CU_ASSERT(NGHTTP2_STREAM_RESERVED == stream->state); + CU_ASSERT(&ud == nghttp2_session_get_stream_user_data(session, 2)); + ud.frame_send_cb_called = 0; ud.sent_frame_type = 0; + CU_ASSERT(0 == nghttp2_session_send(session)); CU_ASSERT(1 == ud.frame_send_cb_called); CU_ASSERT(NGHTTP2_PUSH_PROMISE == ud.sent_frame_type); + stream = nghttp2_session_get_stream(session, 2); + CU_ASSERT(NGHTTP2_STREAM_RESERVED == stream->state); CU_ASSERT(&ud == nghttp2_session_get_stream_user_data(session, 2)); /* submit PUSH_PROMISE while associated stream is not opened */ - CU_ASSERT(4 == nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 3, - reqnv, ARRLEN(reqnv), &ud)); - - ud.frame_not_send_cb_called = 0; - - CU_ASSERT(0 == nghttp2_session_send(session)); - CU_ASSERT(1 == ud.frame_not_send_cb_called); - CU_ASSERT(NGHTTP2_PUSH_PROMISE == ud.not_sent_frame_type); - - stream = nghttp2_session_get_stream(session, 4); - - CU_ASSERT(NULL == stream); + CU_ASSERT(NGHTTP2_ERR_STREAM_CLOSED == + nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 3, reqnv, + ARRLEN(reqnv), &ud)); nghttp2_session_del(session); } @@ -4965,7 +5080,7 @@ void test_nghttp2_session_open_stream(void) { NGHTTP2_STREAM_RESERVED, NULL); CU_ASSERT(0 == session->num_incoming_streams); CU_ASSERT(0 == session->num_outgoing_streams); - CU_ASSERT(NULL == stream->dep_prev); + CU_ASSERT(&session->root == stream->dep_prev); CU_ASSERT(NGHTTP2_DEFAULT_WEIGHT == stream->weight); CU_ASSERT(NGHTTP2_SHUT_WR == stream->shut_flags); @@ -7457,7 +7572,8 @@ void test_nghttp2_session_stream_get_state(void) { stream = nghttp2_session_find_stream(session, 2); - CU_ASSERT(NGHTTP2_STREAM_STATE_CLOSED == nghttp2_stream_get_state(stream)); + /* At server, pushed stream object is not retained after closed */ + CU_ASSERT(NULL == stream); /* Push stream 4 associated to stream 5 */ rv = nghttp2_submit_push_promise(session, NGHTTP2_FLAG_NONE, 5, reqnv, @@ -7597,6 +7713,34 @@ void test_nghttp2_session_stream_get_something(void) { nghttp2_session_del(session); } +void test_nghttp2_session_find_stream(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_stream *stream; + + memset(&callbacks, 0, sizeof(callbacks)); + + nghttp2_session_server_new(&session, &callbacks, NULL); + + open_stream(session, 1); + + stream = nghttp2_session_find_stream(session, 1); + + CU_ASSERT(NULL != stream); + CU_ASSERT(1 == stream->stream_id); + + stream = nghttp2_session_find_stream(session, 0); + + CU_ASSERT(&session->root == stream); + CU_ASSERT(0 == stream->stream_id); + + stream = nghttp2_session_find_stream(session, 2); + + CU_ASSERT(NULL == stream); + + nghttp2_session_del(session); +} + void test_nghttp2_session_keep_closed_stream(void) { nghttp2_session *session; nghttp2_session_callbacks callbacks; @@ -7650,6 +7794,22 @@ void test_nghttp2_session_keep_closed_stream(void) { CU_ASSERT(NULL == session->closed_stream_tail); CU_ASSERT(NULL == session->closed_stream_head); + nghttp2_session_close_stream(session, 3, NGHTTP2_NO_ERROR); + + CU_ASSERT(1 == session->num_closed_streams); + CU_ASSERT(3 == session->closed_stream_head->stream_id); + + /* server initiated stream is not counted to max concurrent limit */ + open_stream(session, 2); + + CU_ASSERT(1 == session->num_closed_streams); + CU_ASSERT(3 == session->closed_stream_head->stream_id); + + nghttp2_session_close_stream(session, 2, NGHTTP2_NO_ERROR); + + CU_ASSERT(1 == session->num_closed_streams); + CU_ASSERT(3 == session->closed_stream_head->stream_id); + nghttp2_session_del(session); } @@ -7787,13 +7947,12 @@ void test_nghttp2_session_large_dep_tree(void) { nghttp2_session_server_new(&session, &callbacks, NULL); stream_id = 1; - for (i = 0; i < 250; ++i) { + for (i = 0; i < 250; ++i, stream_id += 2) { dep_stream = open_stream_with_dep(session, stream_id, dep_stream); - stream_id += 2; } stream_id = 1; - for (i = 0; i < 250; ++i) { + for (i = 0; i < 250; ++i, stream_id += 2) { stream = nghttp2_session_get_stream(session, stream_id); CU_ASSERT(nghttp2_stream_dep_find_ancestor(stream, &session->root)); CU_ASSERT(nghttp2_stream_in_dep_tree(stream)); @@ -8477,6 +8636,116 @@ void test_nghttp2_session_flooding(void) { nghttp2_bufs_free(&bufs); } +void test_nghttp2_session_change_stream_priority(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_stream *stream1, *stream2, *stream3; + nghttp2_priority_spec pri_spec; + int rv; + + memset(&callbacks, 0, sizeof(callbacks)); + + nghttp2_session_server_new(&session, &callbacks, NULL); + + stream1 = open_stream(session, 1); + stream3 = open_stream_with_dep_weight(session, 3, 199, stream1); + stream2 = open_stream_with_dep_weight(session, 2, 101, stream3); + + nghttp2_priority_spec_init(&pri_spec, 1, 256, 0); + + rv = nghttp2_session_change_stream_priority(session, 2, &pri_spec); + + CU_ASSERT(0 == rv); + + CU_ASSERT(stream1 == stream2->dep_prev); + CU_ASSERT(256 == stream2->weight); + + /* Cannot change stream which does not exist */ + rv = nghttp2_session_change_stream_priority(session, 5, &pri_spec); + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + /* It is an error to depend on itself */ + rv = nghttp2_session_change_stream_priority(session, 1, &pri_spec); + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + /* It is an error to change priority of root stream (0) */ + rv = nghttp2_session_change_stream_priority(session, 0, &pri_spec); + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + nghttp2_session_del(session); +} + +void test_nghttp2_session_create_idle_stream(void) { + nghttp2_session *session; + nghttp2_session_callbacks callbacks; + nghttp2_stream *stream2, *stream4, *stream8, *stream10; + nghttp2_priority_spec pri_spec; + int rv; + + memset(&callbacks, 0, sizeof(callbacks)); + + nghttp2_session_server_new(&session, &callbacks, NULL); + + stream2 = open_stream(session, 2); + + nghttp2_priority_spec_init(&pri_spec, 2, 111, 1); + + rv = nghttp2_session_create_idle_stream(session, 4, &pri_spec); + + CU_ASSERT(0 == rv); + + stream4 = nghttp2_session_get_stream_raw(session, 4); + + CU_ASSERT(4 == stream4->stream_id); + CU_ASSERT(111 == stream4->weight); + CU_ASSERT(stream2 == stream4->dep_prev); + CU_ASSERT(stream4 == stream2->dep_next); + + /* If pri_spec->stream_id does not exist, and it is idle stream, it + is created too */ + nghttp2_priority_spec_init(&pri_spec, 8, 109, 0); + + rv = nghttp2_session_create_idle_stream(session, 8, &pri_spec); + + CU_ASSERT(0 == rv); + + stream8 = nghttp2_session_get_stream_raw(session, 8); + stream10 = nghttp2_session_get_stream_raw(session, 10); + + CU_ASSERT(8 == stream8->stream_id); + CU_ASSERT(109 == stream8->weight); + CU_ASSERT(10 == stream10->stream_id); + CU_ASSERT(16 == stream10->weight); + CU_ASSERT(stream10 == stream8->dep_prev); + CU_ASSERT(&session->root == stream10->dep_prev); + + /* It is an error to attempt to create already existing idle + stream */ + rv = nghttp2_session_create_idle_stream(session, 4, &pri_spec); + + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + /* It is an error to depend on itself */ + pri_spec.stream_id = 6; + + rv = nghttp2_session_create_idle_stream(session, 6, &pri_spec); + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + /* It is an error to create root stream (0) as idle stream */ + rv = nghttp2_session_create_idle_stream(session, 0, &pri_spec); + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + /* It is an error to create non-idle stream */ + session->next_stream_id = 20; + pri_spec.stream_id = 2; + + rv = nghttp2_session_create_idle_stream(session, 18, &pri_spec); + + CU_ASSERT(NGHTTP2_ERR_INVALID_ARGUMENT == rv); + + nghttp2_session_del(session); +} + static void check_nghttp2_http_recv_headers_fail( nghttp2_session *session, nghttp2_hd_deflater *deflater, int32_t stream_id, int stream_state, const nghttp2_nv *nva, size_t nvlen) { diff --git a/tests/nghttp2_session_test.h b/tests/nghttp2_session_test.h index 8215dae8..9b6b2228 100644 --- a/tests/nghttp2_session_test.h +++ b/tests/nghttp2_session_test.h @@ -33,6 +33,7 @@ void test_nghttp2_session_recv_data(void); 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_recv_premature_headers(void); void test_nghttp2_session_recv_unknown_frame(void); void test_nghttp2_session_recv_unexpected_continuation(void); @@ -71,6 +72,7 @@ void test_nghttp2_submit_request_with_data(void); void test_nghttp2_submit_request_without_data(void); void test_nghttp2_submit_response_with_data(void); void test_nghttp2_submit_response_without_data(void); +void test_nghttp2_submit_response_push_response(void); void test_nghttp2_submit_trailer(void); void test_nghttp2_submit_headers_start_stream(void); void test_nghttp2_submit_headers_reply(void); @@ -118,6 +120,7 @@ void test_nghttp2_session_stream_attach_item(void); void test_nghttp2_session_stream_attach_item_subtree(void); void test_nghttp2_session_stream_get_state(void); void test_nghttp2_session_stream_get_something(void); +void test_nghttp2_session_find_stream(void); void test_nghttp2_session_keep_closed_stream(void); void test_nghttp2_session_keep_idle_stream(void); void test_nghttp2_session_detach_idle_stream(void); @@ -134,6 +137,8 @@ void test_nghttp2_session_on_begin_headers_temporal_failure(void); void test_nghttp2_session_defer_then_close(void); 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_http_mandatory_headers(void); void test_nghttp2_http_content_length(void); void test_nghttp2_http_content_length_mismatch(void); diff --git a/third-party/mruby b/third-party/mruby index 83922d0b..22464fe5 160000 --- a/third-party/mruby +++ b/third-party/mruby @@ -1 +1 @@ -Subproject commit 83922d0bbb7de894380fa6b33e7d75061114aa59 +Subproject commit 22464fe5a0a10f2b077eaba109ce1e912e4a77de