From: Stefan Eissing Date: Mon, 7 Sep 2015 17:37:19 +0000 (+0000) Subject: merge of protocols + http2 relevant changes X-Git-Tag: 2.4.17~79^2~15 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=0e132fa7f349cfa6bd745ae85b428e9f514464a1;p=thirdparty%2Fapache%2Fhttpd.git merge of protocols + http2 relevant changes git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.17-protocols-http2@1701655 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/Apache-apr2.dsw b/Apache-apr2.dsw index 44b193e1d7b..db8f9c455fc 100644 --- a/Apache-apr2.dsw +++ b/Apache-apr2.dsw @@ -1723,6 +1723,24 @@ Package=<4> ############################################################################### +Project: "mod_h2"=.\modules\http2\mod_h2.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name libapr + End Project Dependency + Begin Project Dependency + Project_Dep_Name libhttpd + End Project Dependency +}}} + +############################################################################### + Project: "mod_headers"=.\modules\metadata\mod_headers.dsp - Package Owner=<4> Package=<5> diff --git a/Apache.dsw b/Apache.dsw index 6ac0075f81e..bfbab3f584a 100644 --- a/Apache.dsw +++ b/Apache.dsw @@ -2041,6 +2041,27 @@ Package=<4> ############################################################################### +Project: "mod_h2"=.\modules\http2\mod_h2.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name libapr + End Project Dependency + Begin Project Dependency + Project_Dep_Name libaprutil + End Project Dependency + Begin Project Dependency + Project_Dep_Name libhttpd + End Project Dependency +}}} + +############################################################################### + Project: "mod_headers"=.\modules\metadata\mod_headers.dsp - Package Owner=<4> Package=<5> diff --git a/BuildBin.dsp b/BuildBin.dsp index 08d4762b659..84143ca124d 100644 --- a/BuildBin.dsp +++ b/BuildBin.dsp @@ -39,7 +39,7 @@ CFG=BuildBin - Win32 Debug # PROP Use_Debug_Libraries 0 # PROP Output_Dir "" # PROP Intermediate_Dir "" -# PROP Cmd_Line "NMAKE /f makefile.win INSTDIR="\Apache2" LONG=Release _trydb _trylua _tryxml _tryssl _tryzlib _dummy" +# PROP Cmd_Line "NMAKE /f makefile.win INSTDIR="\Apache2" LONG=Release _trydb _trylua _tryxml _tryssl _tryzlib _trynghttp2 _tryserf _dummy" # PROP Rebuild_Opt "" # PROP Target_File "\Apache2\bin\httpd.exe" # PROP Bsc_Name ".\Browse\httpd.bsc" @@ -58,7 +58,7 @@ CFG=BuildBin - Win32 Debug # PROP Use_Debug_Libraries 1 # PROP Output_Dir "" # PROP Intermediate_Dir "" -# PROP Cmd_Line "NMAKE /f makefile.win INSTDIR="\Apache2" LONG=Debug _trydb _trylua _tryxml _tryssl _tryzlib _dummy" +# PROP Cmd_Line "NMAKE /f makefile.win INSTDIR="\Apache2" LONG=Debug _trydb _trylua _tryxml _tryssl _tryzlib _trynghttp2 _tryserf _dummy" # PROP Rebuild_Opt "" # PROP Target_File "\Apache2\bin\httpd.exe" # PROP Bsc_Name ".\Browse\httpd.bsc" diff --git a/CHANGES b/CHANGES index bbe52e8419e..709d46e8e5c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,8 @@ -*- coding: utf-8 -*- Changes with Apache 2.4.17 + *) mod_h2: added donated http/2 implementation to build system. Similar + configuration options to mod_ssl. [Stefan Eissing] *) mod_rewrite: Allow cookies set by mod_rewrite to contain ':' by accepting ';' as an alternate separator. PR47241. diff --git a/Makefile.win b/Makefile.win index 35d9b8eab05..2dade641e9c 100644 --- a/Makefile.win +++ b/Makefile.win @@ -215,6 +215,30 @@ _trylua: !ENDIF +!IF EXIST("srclib\nghttp2") + +_trynghttp2: +!IF $(USEMAK) == 1 + cd modules\http2 + $(MAKE) $(MAKEOPT) -f mod_h2.mak CFG="mod_h2 - Win32 $(LONG)" RECURSE=0 $(CTARGET) + cd ..\.. +!ELSEIF $(USESLN) == 1 + devenv $(TLP).sln /useenv $(CTARGET) $(LONG) /project mod_h2 +!ELSE + @msdev $(TLP).dsw /USEENV /MAKE \ + "mod_h2 - Win32 $(LONG)" /NORECURSE $(CTARGET) +!ENDIF + +!ELSE +# NOT EXIST("srclib\lua") + +_trynghttp2: + @echo ----- + @echo mod_h2 will not build unless nghttp2 is installed in srclib\nghttp2. + @echo Version 1.0 includes an lib\makefile.msvc that will satisfy this + @echo requirement. + +!ENDIF _trydb: !IF $(USEMAK) == 1 @@ -754,6 +778,14 @@ _copybin: copy modules\generators\$(LONG)\mod_info.$(src_so) "$(inst_so)" <.y copy modules\generators\$(LONG)\mod_status.$(src_so) "$(inst_so)" <.y copy modules\http\$(LONG)\mod_mime.$(src_so) "$(inst_so)" <.y +!IF EXIST("srclib\nghttp2") + copy modules\http2\$(LONG)\mod_h2.$(src_so) "$(inst_so)" <.y +!IF "$(SHORT)" == "D" + copy srclib\nghttp2\lib\MSVC_obj\nghttp2d.$(src_dll) "$(inst_dll)" <.y +!ELSE + copy srclib\nghttp2\lib\MSVC_obj\nghttp2.$(src_dll) "$(inst_dll)" <.y +!ENDIF +!ENDIF copy modules\ldap\$(LONG)\mod_ldap.$(src_so) "$(inst_so)" <.y copy modules\loggers\$(LONG)\mod_log_config.$(src_so) "$(inst_so)" <.y copy modules\loggers\$(LONG)\mod_log_debug.$(src_so) "$(inst_so)" <.y diff --git a/build/NWGNUenvironment.inc b/build/NWGNUenvironment.inc index 52295608fda..0e510c0b859 100644 --- a/build/NWGNUenvironment.inc +++ b/build/NWGNUenvironment.inc @@ -49,6 +49,12 @@ ifneq "$(wildcard $(LDAPSDK)/inc/ldap.h)" "$(LDAPSDK)/inc/ldap.h" $(error LDAPSDK does not point to a valid Novell CLDAP SDK) endif +ifdef WITH_HTTP2 +ifneq "$(wildcard $(NGH2SRC)/lib/nghttp2_hd.h)" "$(NGH2SRC)/lib/nghttp2_hd.h" +$(error NGH2SRC does not point to a valid NGHTTP2 source tree) +endif +endif + ifndef PCRESRC PCRESRC = $(AP_WORK)/srclib/pcre endif diff --git a/build/installwinconf.awk b/build/installwinconf.awk index 33627341292..8e66679bec3 100644 --- a/build/installwinconf.awk +++ b/build/installwinconf.awk @@ -59,6 +59,7 @@ BEGIN { filelist["httpd-dav.conf"] = "httpd-dav.conf.in"; filelist["httpd-default.conf"] = "httpd-default.conf.in"; filelist["httpd-info.conf"] = "httpd-info.conf.in"; + filelist["httpd-h2.conf"] = "httpd-h2.conf.in"; filelist["httpd-languages.conf"] = "httpd-languages.conf.in"; filelist["httpd-manual.conf"] = "httpd-manual.conf.in"; filelist["httpd-mpm.conf"] = "httpd-mpm.conf.in"; @@ -136,6 +137,7 @@ BEGIN { print "#LoadModule ext_filter_module modules/mod_ext_filter.so" > dstfl; print "#LoadModule file_cache_module modules/mod_file_cache.so" > dstfl; print "#LoadModule filter_module modules/mod_filter.so" > dstfl; + print "#LoadModule h2_module modules/mod_h2.so" > dstfl; print "#LoadModule headers_module modules/mod_headers.so" > dstfl; print "#LoadModule heartbeat_module modules/mod_heartbeat.so" > dstfl; print "#LoadModule heartmonitor_module modules/mod_heartmonitor.so" > dstfl; diff --git a/docs/conf/extra/httpd-h2.conf.in b/docs/conf/extra/httpd-h2.conf.in new file mode 100644 index 00000000000..6d624f66ef6 --- /dev/null +++ b/docs/conf/extra/httpd-h2.conf.in @@ -0,0 +1,12 @@ +# +# This is the Apache server configuration file providing HTTP/2 support. +# It contains the configuration directives to instruct the server how to +# serve pages via the http/2 protocol. For detailed information about these +# directives see +# +# Required modules: mod_h2 + + + # This can also set to "off" and turned on only for specific virtual hosts + H2Engine on + diff --git a/docs/conf/httpd.conf.in b/docs/conf/httpd.conf.in index 6e418913774..9fb2e9d1ab5 100644 --- a/docs/conf/httpd.conf.in +++ b/docs/conf/httpd.conf.in @@ -418,3 +418,6 @@ SSLRandomSeed connect builtin #RequestHeader unset DNT env=bad_DNT # +# h2/h2c (HTTP/2) connections +#Include @rel_sysconfdir@/extra/httpd-h2.conf + diff --git a/docs/manual/mod/allmodules.xml b/docs/manual/mod/allmodules.xml index 48e61762b67..fbc20266d19 100644 --- a/docs/manual/mod/allmodules.xml +++ b/docs/manual/mod/allmodules.xml @@ -51,6 +51,7 @@ mod_ext_filter.xml mod_file_cache.xml mod_filter.xml + mod_h2.xml mod_headers.xml mod_heartbeat.xml mod_heartmonitor.xml diff --git a/docs/manual/mod/core.xml b/docs/manual/mod/core.xml index 2e7812cc8e1..1620ccbbc1d 100644 --- a/docs/manual/mod/core.xml +++ b/docs/manual/mod/core.xml @@ -3589,6 +3589,74 @@ On Windows, from Apache 2.3.3 and later. + + Protocols + Protocols available for a server/virtual host + Protocols protocol ... + Protocols http/1.1 + server configvirtual host + Only available from Apache 2.4.17 and later. + + +

This directive specifies the list of protocols supported for a + server/virtual host. The list determines the allowed protocols + a client may negotiate for this server/host.

+ +

You need to set protocols if you want to extend the available + protocols for a server/host. By default, only the http/1.1 protocol + (which includes the compatibility with 1.0 and 0.9 clients) is + allowed.

+ +

For example, if you want to support HTTP/2 for a server with TLS, + specify:

+ + + Protocols h2 http/1.1 + + +

Valid protocols are http/1.1 for http and https connections, + h2 on https connections and h2c for http + connections. Modules may enable more protocols.

+ +

It is safe to specify protocols that are unavailable/disabled. Such + protocol names will simply be ignored.

+ +

Protocols specified in base servers are inherited for virtual hosts + only if the virtual host has no own Protocols directive. Or, the other + way around, Protocols directives in virtual hosts replace any + such directive in the base server. +

+ +
+ ProtocolsHonorOrder +
+ + + + ProtocolsHonorOrder + Protocols available for a server/virtual host + ProtocolsHonorOrder On|Off + ProtocolsHonorOrder On + server configvirtual host + Only available from Apache 2.4.17 and later. + + +

This directive specifies if the server should honor the order in which + the Protocols directive lists protocols.

+ +

If configured Off, the client supplied list order of protocols has + precedence over the order in the server configuration.

+ +

With ProtocolsHonorOrder set to on + (default), the client ordering does not matter and only the ordering + in the server settings influences the outcome of the protocol + negotiation.

+ +
+ Protocols +
+ + RLimitCPU Limits the CPU consumption of processes launched diff --git a/docs/manual/mod/mod_h2.html b/docs/manual/mod/mod_h2.html new file mode 100644 index 00000000000..9346e292d03 --- /dev/null +++ b/docs/manual/mod/mod_h2.html @@ -0,0 +1,5 @@ +# GENERATED FROM XML -- DO NOT EDIT + +URI: mod_h2.html.en +Content-Language: en +Content-type: text/html; charset=ISO-8859-1 diff --git a/docs/manual/mod/mod_h2.html.en b/docs/manual/mod/mod_h2.html.en new file mode 100644 index 00000000000..90a8e0ee007 --- /dev/null +++ b/docs/manual/mod/mod_h2.html.en @@ -0,0 +1,409 @@ + + + + + +mod_h2 - Apache HTTP Server Version 2.5 + + + + + + + + +
<-
+ +
+

Apache Module mod_h2

+
+

Available Languages:  en 

+
+ + + +
Description:Support for the HTTP/2 transport layer
Status:Extension
Module Identifier:h2_module
Source File:mod_h2.c
+

Summary

+ +

This module provides HTTP/2 (RFC 7540) support for the Apache + HTTP Server.

+ +

This module relies on libnghttp2 + to provide the core http/2 engine.

+ +
+ + +
top
+

H2BufferOutput Directive

+ + + + + + +
Description:Output Buffering Switch
Syntax:H2BufferOutput on|off
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive toggles if buffering of HTTP/2 output shall be used + or if data is written immediately when it arrives. Unless specified + otherwise, this directive is on for TLS connections and + off for plain connections. +

+

Example

H2BufferOutput on
+
+ +
+
top
+

H2BufferSize Directive

+ + + + + + + +
Description:Buffer size for outgoing data per HTTP/2 connection.
Syntax:H2BufferSize bytes
Default:H2BufferSize 65536
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets the size of the buffer used to hold outgoing + HTTP/2 raw data, should H2BufferOutput be switched on. + This data is allocated per HTTP/2 connection, not stream and is + counted against the raw protocol data. +

+

Example

H2BufferSize 128000
+
+ +
+
top
+

H2BufferWriteMax Directive

+ + + + + + + +
Description:Maximum size of write on a HTTP/2 connection.
Syntax:H2BufferWriteMax bytes
Default:H2BufferWriteMax 16384
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets maximum amount of data sent out in a single + write on a http/2 connection. It only takes effect when + H2BufferOutput is switched on. +

+ This directive affects performance of underlying TLS transports. TLS + transforms each write into an encrypted record. Clients need + to receive all of the record in order to decrypt it. Larger sizes + result in better server performance, shorter sizes can affect web + page paint timings. +

+ BufferSize should be a multiple of H2BufferWriteMax. + H2BufferWriteMax, if larger than 16k, should be a multiple of 16k, + since this is the TLS max record size. Be aware that there are TLS + extensions to limit the record size to powers of 2 less than 16k. +

+

Example

H2BufferWriteMax 8000
+
+ +
+
top
+

H2Direct Directive

+ + + + + + + +
Description:H2 Direct Protocol Switch
Syntax:H2Direct on|off
Default:H2Direct on
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive toggles the usage of the HTTP/2 Direct Mode. This + should be used inside a + <VirtualHost> + section to enable direct HTTP/2 communication for that virtual host. + Direct communication means that if the first bytes received by the + server on a connection match the HTTP/2 preamble, the HTTP/2 + protocol is switched to immediately without further negotiation. + This mode falls outside the RFC 7540 but has become widely implemented + as it is very convenient for development and testing. + By default the direct HTTP/2 mode is enabled. +

+

Example

H2Direct on
+
+ +
+
top
+

H2Engine Directive

+ + + + + + + +
Description:H2 Engine Operation Switch
Syntax:H2Engine on|off
Default:H2Engine off
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive toggles the usage of the HTTP/2 Protocol Engine. This + should be used inside a + <VirtualHost> + section to enable HTTP/2 for that virtual host. By default the + HTTP/2 Protocol Engine is disabled for both the main server and all + configured virtual hosts. +

+

Example

<VirtualHost _default_:443>
+    H2Engine on
+    #...
+</VirtualHost>
+
+

+ The HTTP/2 engine is usable in TLS and plain scenarios, supporting + the 'h2' and 'h2c' variants of the protocol. +

+ +
+
top
+

H2MaxHeaderListSize Directive

+ + + + + + + +
Description:Maximum size of acceptable stream headers.
Syntax:H2MaxHeaderListSize bytes
Default:H2MaxHeaderListSize 16384
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets the maximum amount of stream header bytes that + the server is willing to accept. It is announced to the client during + the initial HTTP/2 handshake. +

+

Example

H2MaxHeaderListSize 10000
+
+ +
+
top
+

H2MaxSessionStreams Directive

+ + + + + + + +
Description:Maximum number of active streams per HTTP/2 session.
Syntax:H2MaxSessionStreams n
Default:H2MaxSessionStreams 100
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets the maximum number of active streams per HTTP/2 session (e.g. connection) + that the server allows. A stream is active if it is not idle or + closed according to RFC 7540. +

+

Example

H2MaxSessionStreams 20
+
+ +
+
top
+

H2MaxWorkerIdleSeconds Directive

+ + + + + + + +
Description:Maximum number of seconds h2 workers remain idle until shut down.
Syntax:H2MaxWorkerIdleSeconds n
Default:H2MaxWorkerIdleSeconds 600
Context:server config
Status:Extension
Module:mod_h2
+

+ This directive sets the maximum number of seconds a h2 worker may + idle until it shuts itself down. This only happens while the number of + h2 workers exceeds H2MinWorkers. +

+

Example

H2MaxWorkerIdleSeconds 20
+
+ +
+
top
+

H2MaxWorkers Directive

+ + + + + + +
Description:Maximum number of worker threads to use per child process.
Syntax:H2MaxWorkers n
Context:server config
Status:Extension
Module:mod_h2
+

+ This directive sets the maximum number of worker threads to spawn + per child process for HTTP/2 processing. If this directive is not used, + mod_h2 will chose a value suitable for the mpm + module loaded. +

+

Example

H2MaxWorkers 20
+
+ +
+
top
+

H2MinWorkers Directive

+ + + + + + +
Description:Minimal number of worker threads to use per child process.
Syntax:H2MinWorkers n
Context:server config
Status:Extension
Module:mod_h2
+

+ This directive sets the minimum number of worker threads to spawn + per child process for HTTP/2 processing. If this directive is not used, + mod_h2 will chose a value suitable for the mpm + module loaded. +

+

Example

H2MinWorkers 10
+
+ +
+
top
+

H2SerializeHeaders Directive

+ + + + + + + +
Description:Serialize Request/Resoonse Processing Switch
Syntax:H2SerializeHeaders on|off
Default:H2SerializeHeaders off
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive toggles if HTTP/2 requests shall be serialized in + HTTP/1.1 format for processing by httpd core or if + received binary data shall be passed into the request_recs + directly. +

+

+ Serialization will lower performance, but gives more backward + compatibility in case custom filters/hooks need it. +

+

Example

H2SerializeHeaders on
+
+ +
+
top
+

H2SessionExtraFiles Directive

+ + + + + + + +
Description:Number of Extra File Handles
Syntax:H2SessionExtraFiles n
Default:H2SessionExtraFiles 5
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets maximum number of extra file handles + a HTTP/2 session is allowed to use. A file handle is counted as + extra when it is transfered from a h2 worker thread to + the main HTTP/2 connection handling. This commonly happens when + serving static files. +

+ Depending on the processing model configured on the server, the + number of connections times number of active streams may exceed + the number of file handles for the process. On the other hand, + converting every file into memory bytes early results in too + many buffer writes. This option helps to mitigate that. +

+ The number of file handles used by a server process is then in + the order of: +

+
(h2_connections * extra_files) + (h2_max_worker)
+

Example

H2SessionExtraFiles 10
+
+ +
+
top
+

H2StreamMaxMemSize Directive

+ + + + + + + +
Description:Maximum amount of output data buffered per stream.
Syntax:H2StreamMaxMemSize bytes
Default:H2StreamMaxMemSize 65536
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets the maximum number of outgoing data bytes buffered in memory + for an active streams. This memory is not allocated per stream as such. Allocations + are counted against this limit when they are about to be done. Stream processing + freezes when the limit has been reached and will only continue when buffered data + has been sent out to the client. +

+

Example

H2StreamMaxMemSize 128000
+
+ +
+
top
+

H2WindowSize Directive

+ + + + + + + +
Description:Size of Stream Window for upstream data.
Syntax:H2WindowSize bytes
Default:H2WindowSize 65536
Context:server config, virtual host
Status:Extension
Module:mod_h2
+

+ This directive sets the size of the window that is used for flow control + from client to server and limits the amount of data the server has to buffer. + The client will stop sending on a stream once the limit has been reached until + the server announces more available space (as it has processed some of the data). +

+ This limit affects only request bodies, not its meta data such as headers. Also, + it has no effect on response bodies as the window size for those are managed + by the clients. +

+

Example

H2WindowSize 128000
+
+ +
+
+
+

Available Languages:  en 

+
top

Comments

Notice:
This is not a Q&A section. Comments placed here should be pointed towards suggestions on improving the documentation or server, and may be removed again by our moderators if they are either implemented or considered invalid/off-topic. Questions on how to manage the Apache HTTP Server should be directed at either our IRC channel, #httpd, on Freenode, or sent to our mailing lists.
+
+ \ No newline at end of file diff --git a/docs/manual/mod/mod_h2.xml b/docs/manual/mod/mod_h2.xml new file mode 100644 index 00000000000..c5c77aa373d --- /dev/null +++ b/docs/manual/mod/mod_h2.xml @@ -0,0 +1,278 @@ + + + + + + + + + + mod_h2 + Support for the HTTP/2 transport layer + Extension + mod_h2.c + h2_module + + +

This module provides HTTP/2 (RFC 7540) support for the Apache + HTTP Server.

+ +

This module relies on libnghttp2 + to provide the core http/2 engine.

+ +
+ + + H2Direct + H2 Direct Protocol Switch + H2Direct on|off + H2Direct on (for non TLS) + + server config + virtual host + + + +

+ This directive toggles the usage of the HTTP/2 Direct Mode. This + should be used inside a + VirtualHost + section to enable direct HTTP/2 communication for that virtual host. + Direct communication means that if the first bytes received by the + server on a connection match the HTTP/2 preamble, the HTTP/2 + protocol is switched to immediately without further negotiation. + This mode falls outside the RFC 7540 but has become widely implemented + as it is very convenient for development and testing. + By default the direct HTTP/2 mode is enabled. +

+ Example + + H2Direct on + + +
+
+ + + H2MaxSessionStreams + Maximum number of active streams per HTTP/2 session. + H2MaxSessionStreams n + H2MaxSessionStreams 100 + + server config + virtual host + + +

+ This directive sets the maximum number of active streams per HTTP/2 session (e.g. connection) + that the server allows. A stream is active if it is not idle or + closed according to RFC 7540. +

+ Example + + H2MaxSessionStreams 20 + + +
+
+ + + H2StreamMaxMemSize + Maximum amount of output data buffered per stream. + H2StreamMaxMemSize bytes + H2StreamMaxMemSize 65536 + + server config + virtual host + + +

+ This directive sets the maximum number of outgoing data bytes buffered in memory + for an active streams. This memory is not allocated per stream as such. Allocations + are counted against this limit when they are about to be done. Stream processing + freezes when the limit has been reached and will only continue when buffered data + has been sent out to the client. +

+ Example + + H2StreamMaxMemSize 128000 + + +
+
+ + + H2WindowSize + Size of Stream Window for upstream data. + H2WindowSize bytes + H2WindowSize 65536 + + server config + virtual host + + +

+ This directive sets the size of the window that is used for flow control + from client to server and limits the amount of data the server has to buffer. + The client will stop sending on a stream once the limit has been reached until + the server announces more available space (as it has processed some of the data). +

+ This limit affects only request bodies, not its meta data such as headers. Also, + it has no effect on response bodies as the window size for those are managed + by the clients. +

+ Example + + H2WindowSize 128000 + + +
+
+ + + H2MinWorkers + Minimal number of worker threads to use per child process. + H2MinWorkers n + + server config + + +

+ This directive sets the minimum number of worker threads to spawn + per child process for HTTP/2 processing. If this directive is not used, + mod_h2 will chose a value suitable for the mpm + module loaded. +

+ Example + + H2MinWorkers 10 + + +
+
+ + + H2MaxWorkers + Maximum number of worker threads to use per child process. + H2MaxWorkers n + + server config + + +

+ This directive sets the maximum number of worker threads to spawn + per child process for HTTP/2 processing. If this directive is not used, + mod_h2 will chose a value suitable for the mpm + module loaded. +

+ Example + + H2MaxWorkers 20 + + +
+
+ + + H2MaxWorkerIdleSeconds + Maximum number of seconds h2 workers remain idle until shut down. + H2MaxWorkerIdleSeconds n + H2MaxWorkerIdleSeconds 600 + + server config + + +

+ This directive sets the maximum number of seconds a h2 worker may + idle until it shuts itself down. This only happens while the number of + h2 workers exceeds H2MinWorkers. +

+ Example + + H2MaxWorkerIdleSeconds 20 + + +
+
+ + + H2SessionExtraFiles + Number of Extra File Handles + H2SessionExtraFiles n + H2SessionExtraFiles 5 + + server config + virtual host + + +

+ This directive sets maximum number of extra file handles + a HTTP/2 session is allowed to use. A file handle is counted as + extra when it is transfered from a h2 worker thread to + the main HTTP/2 connection handling. This commonly happens when + serving static files. +

+ Depending on the processing model configured on the server, the + number of connections times number of active streams may exceed + the number of file handles for the process. On the other hand, + converting every file into memory bytes early results in too + many buffer writes. This option helps to mitigate that. +

+ The number of file handles used by a server process is then in + the order of: +

+
+                (h2_connections * extra_files) + (h2_max_worker)
+            
+ Example + + H2SessionExtraFiles 10 + + +
+
+ + + H2SerializeHeaders + Serialize Request/Resoonse Processing Switch + H2SerializeHeaders on|off + H2SerializeHeaders off + + server config + virtual host + + +

+ This directive toggles if HTTP/2 requests shall be serialized in + HTTP/1.1 format for processing by httpd core or if + received binary data shall be passed into the request_recs + directly. +

+

+ Serialization will lower performance, but gives more backward + compatibility in case custom filters/hooks need it. +

+ Example + + H2SerializeHeaders on + + +
+
+ +
diff --git a/docs/manual/mod/mod_h2.xml.meta b/docs/manual/mod/mod_h2.xml.meta new file mode 100644 index 00000000000..ff80d56469d --- /dev/null +++ b/docs/manual/mod/mod_h2.xml.meta @@ -0,0 +1,12 @@ + + + + + mod_h2 + /mod/ + .. + + + en + + diff --git a/docs/manual/mod/mod_proxy.xml b/docs/manual/mod/mod_proxy.xml index a2ba3395919..e4cdd50c1d7 100644 --- a/docs/manual/mod/mod_proxy.xml +++ b/docs/manual/mod/mod_proxy.xml @@ -1202,9 +1202,9 @@ ProxyPass "/mirror/foo" "http://backend.example.com" Balancer sticky session name. The value is usually set to something like JSESSIONID or PHPSESSIONID, and it depends on the backend application server that support sessions. - If the backend application server uses different names for cookies - and url encoded id (like servlet containers), use | to separate them. - The first part is for the cookie; the second for the path.
+ If the backend application server uses different name for cookies + and url encoded id (like servlet containers) use | to separate them. + The first part is for the cookie the second for the path.
Available in Apache HTTP Server 2.4.4 and later. stickysessionsep diff --git a/docs/manual/mod/quickreference.html.en b/docs/manual/mod/quickreference.html.en index ca697e8702c..1a5acbeb0f0 100644 --- a/docs/manual/mod/quickreference.html.en +++ b/docs/manual/mod/quickreference.html.en @@ -467,7 +467,21 @@ media type in the HTTP Content-Type header field will exit. Group unix-group #-1 sBGroup under which the server will answer requests -Header [condition] add|append|echo|edit|edit*|merge|set|setifempty|unset|note +H2BufferOutput on|offsvEOutput Buffering Switch +H2BufferSize bytes 65536 svEBuffer size for outgoing data per HTTP/2 connection. +H2BufferWriteMax bytes 16384 svEMaximum size of write on a HTTP/2 connection. +H2Direct on|off on svEH2 Direct Protocol Switch +H2Engine on|off off svEH2 Engine Operation Switch +H2MaxHeaderListSize bytes 16384 svEMaximum size of acceptable stream headers. +H2MaxSessionStreams n 100 svEMaximum number of active streams per HTTP/2 session. +H2MaxWorkerIdleSeconds n 600 sEMaximum number of seconds h2 workers remain idle until shut down. +H2MaxWorkers nsEMaximum number of worker threads to use per child process. +H2MinWorkers nsEMinimal number of worker threads to use per child process. +H2SerializeHeaders on|off off svESerialize Request/Resoonse Processing Switch +H2SessionExtraFiles n 5 svENumber of Extra File Handles +H2StreamMaxMemSize bytes 65536 svEMaximum amount of output data buffered per stream. +H2WindowSize bytes 65536 svESize of Stream Window for upstream data. +Header [condition] add|append|echo|edit|edit*|merge|set|setifempty|unset|note header [[expr=]value [replacement] [early|env=[!]varname|expr=expression]] svdhEConfigure HTTP response headers diff --git a/include/ap_mmn.h b/include/ap_mmn.h index 7e609e3ea9d..72fe541e14f 100644 --- a/include/ap_mmn.h +++ b/include/ap_mmn.h @@ -445,6 +445,7 @@ * 20120211.46 (2.4.13-dev) Add ap_map_http_request_error() * 20120211.47 (2.4.13-dev) Add ap_some_authn_required, ap_force_authn hook. * Deprecate broken ap_some_auth_required. + * 20140627.4 (2.5.0-dev) Added ap_parse_token_list_strict() to httpd.h. */ #define MODULE_MAGIC_COOKIE 0x41503234UL /* "AP24" */ diff --git a/include/http_core.h b/include/http_core.h index 8171823a083..6ca53f76edb 100644 --- a/include/http_core.h +++ b/include/http_core.h @@ -681,6 +681,10 @@ typedef struct { #define AP_MERGE_TRAILERS_DISABLE 2 int merge_trailers; + + + apr_array_header_t *protocols; + int protocols_honor_order; } core_server_config; /* for AddOutputFiltersByType in core.c */ diff --git a/include/http_protocol.h b/include/http_protocol.h index ee61b687695..64ed01362cc 100644 --- a/include/http_protocol.h +++ b/include/http_protocol.h @@ -700,6 +700,139 @@ AP_DECLARE_HOOK(const char *,http_scheme,(const request_rec *r)) */ AP_DECLARE_HOOK(apr_port_t,default_port,(const request_rec *r)) + +#define AP_PROTOCOL_HTTP1 "http/1.1" + +/** + * Determine the list of protocols available for a connection/request. This may + * be collected with or without any request sent, in which case the request is + * NULL. Or it may be triggered by the request received, e.g. through the + * "Upgrade" header. + * + * This hook will be run whenever protocols are being negotiated (ALPN as + * one example). It may also be invoked at other times, e.g. when the server + * wants to advertise protocols it is capable of switching to. + * + * The identifiers for protocols are taken from the TLS extension type ALPN: + * https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xml + * + * If no protocols are added to the proposals, the server not perform any + * switch. If the protocol selected from the proposals is the protocol + * already in place, also no protocol switch will be invoked. + * + * The client may already have announced the protocols it is willing to + * accept. These will then be listed as offers. This parameter may also + * be NULL, indicating that offers from the client are not known and + * the hooks should propose all protocols that are valid for the + * current connection/request. + * + * All hooks are run, unless one returns an error. Proposals may contain + * duplicates. The order in which proposals are added is usually ignored. + * + * @param c The current connection + * @param r The current request or NULL + * @param s The server/virtual host selected + * @param offers A list of protocol identifiers offered by the client or + * NULL to indicated that the hooks are free to propose + * @param proposals The list of protocol identifiers proposed by the hooks + * @return OK or DECLINED + */ +AP_DECLARE_HOOK(int,protocol_propose,(conn_rec *c, request_rec *r, + server_rec *s, + const apr_array_header_t *offers, + apr_array_header_t *proposals)) + +/** + * Perform a protocol switch on the connection. The exact requirements for + * that depend on the protocol in place and the one switched to. The first + * protocol module to handle the switch is the last module run. + * + * For a connection level switch (r == NULL), the handler must on return + * leave the conn_rec in a state suitable for processing the switched + * protocol, e.g. correct filters in place. + * + * For a request triggered switch (r != NULL), the protocol switch is done + * before the response is sent out. When switching from "http/1.1" via Upgrade + * header, the 101 intermediate response will have been sent. The + * hook needs then to process the connection until it can be closed. Which + * the server will enforce on hook return. + * Any error the hook might encounter must already be sent by the hook itself + * to the client in whatever form the new protocol requires. + * + * @param c The current connection + * @param r The current request or NULL + * @param s The server/virtual host selected + * @param choices A list of protocol identifiers, normally the clients whishes + * @param proposals the list of protocol identifiers proposed by the hooks + * @return OK or DECLINED + */ +AP_DECLARE_HOOK(int,protocol_switch,(conn_rec *c, request_rec *r, + server_rec *s, + const char *protocol)) + +/** + * Return the protocol used on the connection. Modules implementing + * protocol switching must register here and return the correct protocol + * identifier for connections they switched. + * + * To find out the protocol for the current connection, better call + * @see ap_get_protocol which internally uses this hook. + * + * @param c The current connection + * @return The identifier of the protocol in place or NULL + */ +AP_DECLARE_HOOK(const char *,protocol_get,(const conn_rec *c)) + +/** + * Select a protocol for the given connection and optional request. Will return + * the protocol identifier selected which may be the protocol already in place + * on the connection. The selected protocol will be NULL if non of the given + * choices could be agreed upon (e.g. no proposal as made). + * + * A special case is where the choices itself is NULL (instead of empty). In + * this case there are no restrictions imposed on protocol selection. + * + * @param c The current connection + * @param r The current request or NULL + * @param s The server/virtual host selected + * @param choices A list of protocol identifiers, normally the clients whishes + * @return The selected protocol or NULL if no protocol could be agreed upon + */ +AP_DECLARE(const char *) ap_select_protocol(conn_rec *c, request_rec *r, + server_rec *s, + const apr_array_header_t *choices); + +/** + * Perform the actual protocol switch. The protocol given must have been + * selected before on the very same connection and request pair. + * + * @param c The current connection + * @param r The current request or NULL + * @param s The server/virtual host selected + * @param protocol the protocol to switch to + * @return APR_SUCCESS, if caller may continue processing as usual + * APR_EOF, if caller needs to stop processing the connection + * APR_EINVAL, if the protocol is already in place + * APR_NOTIMPL, if no module performed the switch + * Other errors where appropriate + */ +AP_DECLARE(apr_status_t) ap_switch_protocol(conn_rec *c, request_rec *r, + server_rec *s, + const char *protocol); + +/** + * Call the protocol_get hook to determine the protocol currently in use + * for the given connection. + * + * Unless another protocol has been switch to, will default to + * @see AP_PROTOCOL_HTTP1 and modules implementing a new protocol must + * report a switched connection via the protocol_get hook. + * + * @param c The connection to determine the protocol for + * @return the protocol in use, never NULL + */ +AP_DECLARE(const char *) ap_get_protocol(conn_rec *c); + /** @see ap_bucket_type_error */ typedef struct ap_bucket_error ap_bucket_error; diff --git a/include/http_request.h b/include/http_request.h index fabb4c843b7..3d0b143e786 100644 --- a/include/http_request.h +++ b/include/http_request.h @@ -315,7 +315,7 @@ AP_DECLARE(void) ap_allow_standard_methods(request_rec *r, int reset, ...); * the response to the client * @param r The current request */ -void ap_process_request(request_rec *r); +AP_DECLARE(void) ap_process_request(request_rec *r); /* For post-processing after a handler has finished with a request. * (Commonly used after it was suspended) diff --git a/include/httpd.h b/include/httpd.h index a9163c31a33..331a4d0c3ba 100644 --- a/include/httpd.h +++ b/include/httpd.h @@ -518,6 +518,7 @@ AP_DECLARE(const char *) ap_get_server_built(void); #define HTTP_UNSUPPORTED_MEDIA_TYPE 415 #define HTTP_RANGE_NOT_SATISFIABLE 416 #define HTTP_EXPECTATION_FAILED 417 +#define HTTP_MISDIRECTED_REQUEST 421 #define HTTP_UNPROCESSABLE_ENTITY 422 #define HTTP_LOCKED 423 #define HTTP_FAILED_DEPENDENCY 424 @@ -1548,6 +1549,23 @@ AP_DECLARE(int) ap_find_etag_weak(apr_pool_t *p, const char *line, const char *t */ AP_DECLARE(int) ap_find_etag_strong(apr_pool_t *p, const char *line, const char *tok); +/** + * Retrieve an array of tokens in the format "1#token" defined in RFC2616. Only + * accepts ',' as a delimiter, does not accept quoted strings, and errors on + * any separator. + * @param p The pool to allocate from + * @param tok The line to read tokens from + * @param tokens Pointer to an array of tokens. If not NULL, must be an array + * of char*, otherwise it will be allocated on @a p when a token is found + * @param skip_invalid If true, when an invalid separator is encountered, it + * will be ignored. + * @return NULL on success, an error string otherwise. + * @remark *tokens may be NULL on output if NULL in input and no token is found + */ +AP_DECLARE(const char *) ap_parse_token_list_strict(apr_pool_t *p, const char *tok, + apr_array_header_t **tokens, + int skip_invalid); + /** * Retrieve a token, spacing over it and adjusting the pointer to * the first non-white byte afterwards. Note that these tokens @@ -2265,6 +2283,28 @@ AP_DECLARE(char *) ap_get_exec_line(apr_pool_t *p, #define AP_NORESTART APR_OS_START_USEERR + 1 +/** + * Get the first index of the string in the array or -1 if not found. Start + * searching a start. + * @param array The array the check + * @param s The string to find + * @param start Start index for search. If start is out of bounds (negative or + equal to array length or greater), -1 will be returned. + * @return index of string in array or -1 + */ +AP_DECLARE(int) ap_array_str_index(const apr_array_header_t *array, + const char *s, + int start); + +/** + * Check if the string is member of the given array by strcmp. + * @param array The array the check + * @param s The string to find + * @return !=0 iff string is member of array (via strcmp) + */ +AP_DECLARE(int) ap_array_str_contains(const apr_array_header_t *array, + const char *s); + #ifdef __cplusplus } #endif diff --git a/modules/NWGNUmakefile b/modules/NWGNUmakefile index 6e8113d4b93..f625e7c2422 100644 --- a/modules/NWGNUmakefile +++ b/modules/NWGNUmakefile @@ -5,7 +5,8 @@ # variable WITH_SSL=1 # To build with the mod_lua module set the environment # variable WITH_LUA=1 - +# To build with the mod_h2 module set the environment +# variable WITH_HTTP2=1 # # Check if LDAP is enabled in APR-UTIL # @@ -72,6 +73,13 @@ SUBDIRS += lua endif endif +# Allow the mod_h2 module to be built if WITH_HTTP2 is defined +ifeq "$(WITH_HTTP2)" "1" +ifneq "$(NGH2SRC)" "" +SUBDIRS += http2 +endif +endif + # Allow the experimental modules to be built if WITH_EXPERIMENTAL is defined ifeq "$(WITH_EXPERIMENTAL)" "1" SUBDIRS += experimental diff --git a/modules/README b/modules/README index f11ad42f216..2dee0795664 100644 --- a/modules/README +++ b/modules/README @@ -41,6 +41,9 @@ generators/ http/ This directory houses modules that basic HTTP protocol implementation. +http2/ + This directory houses modules that provide HTTP/2 protocol implementation. + loggers/ This directory houses modules that handle logging functions. diff --git a/modules/http/http_protocol.c b/modules/http/http_protocol.c index fc7ec6ccbe7..589611b593b 100644 --- a/modules/http/http_protocol.c +++ b/modules/http/http_protocol.c @@ -135,7 +135,7 @@ static const char * const status_lines[RESPONSE_CODES] = NULL, /* 418 */ NULL, /* 419 */ NULL, /* 420 */ - NULL, /* 421 */ + "421 Misdirected Request", "422 Unprocessable Entity", "423 Locked", "424 Failed Dependency", @@ -1293,6 +1293,11 @@ static const char *get_canned_error_string(int status, case HTTP_NETWORK_AUTHENTICATION_REQUIRED: return("

The client needs to authenticate to gain\n" "network access.

\n"); + case HTTP_MISDIRECTED_REQUEST: + return("

The client needs a new connection for this\n" + "request as the requested host name does not match\n" + "the Server Name Indication (SNI) in use for this\n" + "connection.

\n"); default: /* HTTP_INTERNAL_SERVER_ERROR */ /* * This comparison to expose error-notes could be modified to diff --git a/modules/http/http_request.c b/modules/http/http_request.c index 7b06def9405..70bf2937c08 100644 --- a/modules/http/http_request.c +++ b/modules/http/http_request.c @@ -363,7 +363,7 @@ void ap_process_async_request(request_rec *r) ap_process_request_after_handler(r); } -void ap_process_request(request_rec *r) +AP_DECLARE(void) ap_process_request(request_rec *r) { apr_bucket_brigade *bb; apr_bucket *b; diff --git a/modules/http2/.gitignore b/modules/http2/.gitignore new file mode 100644 index 00000000000..ca49620fafe --- /dev/null +++ b/modules/http2/.gitignore @@ -0,0 +1,35 @@ +*.xcuserstate +sandbox/httpd/packages/httpd-2.4.x.tar.gz +sandbox/test/conf/sites/mod-h2.greenbytes.de.conf +*.o +*.slo +*.lo +*.la +*.pcap +.libs +.configured +.deps +compile +aclocal.m4 +autom4te.cache +autoscan.log +config.guess +config.log +config.status +config.sub +config.h +config.h.in +config.h.in~ +configure +configure.scan +depcomp +install-sh +libtool +ltmain.sh +missing +stamp-h1 +Makefile.in +Makefile +mod_h2-*.tar.gz +mod_h2/h2_version.h +m4 diff --git a/modules/http2/Makefile.in b/modules/http2/Makefile.in new file mode 100644 index 00000000000..4395bc3ac76 --- /dev/null +++ b/modules/http2/Makefile.in @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# standard stuff +# + +include $(top_srcdir)/build/special.mk diff --git a/modules/http2/NWGNUmakefile b/modules/http2/NWGNUmakefile new file mode 100644 index 00000000000..cc08702b310 --- /dev/null +++ b/modules/http2/NWGNUmakefile @@ -0,0 +1,331 @@ +# +# This Makefile requires the environment var NGH2SRC +# pointing to the base directory of nghttp2 source tree. +# + +# +# Declare the sub-directories to be built here +# + +SUBDIRS = \ + $(EOLIST) + +# +# Get the 'head' of the build environment. This includes default targets and +# paths to tools +# + +include $(AP_WORK)/build/NWGNUhead.inc + +# +# build this level's files +# +# Make sure all needed macro's are defined +# + +# +# These directories will be at the beginning of the include list, followed by +# INCDIRS +# +XINCDIRS += \ + $(APR)/include \ + $(APRUTIL)/include \ + $(SRC)/include \ + $(NGH2SRC)/lib/ \ + $(NGH2SRC)/lib/includes \ + $(SERVER)/mpm/NetWare \ + $(NWOS) \ + $(EOLIST) + +# +# These flags will come after CFLAGS +# +XCFLAGS += \ + $(EOLIST) + +# +# These defines will come after DEFINES +# +XDEFINES += \ + -DHAVE_CONFIG_H \ + $(EOLIST) + +# +# These flags will be added to the link.opt file +# +XLFLAGS += \ + -L$(OBJDIR) \ + $(EOLIST) + +# +# These values will be appended to the correct variables based on the value of +# RELEASE +# +ifeq "$(RELEASE)" "debug" +XINCDIRS += \ + $(EOLIST) + +XCFLAGS += \ + $(EOLIST) + +XDEFINES += \ + $(EOLIST) + +XLFLAGS += \ + $(EOLIST) +endif + +ifeq "$(RELEASE)" "noopt" +XINCDIRS += \ + $(EOLIST) + +XCFLAGS += \ + $(EOLIST) + +XDEFINES += \ + $(EOLIST) + +XLFLAGS += \ + $(EOLIST) +endif + +ifeq "$(RELEASE)" "release" +XINCDIRS += \ + $(EOLIST) + +XCFLAGS += \ + $(EOLIST) + +XDEFINES += \ + $(EOLIST) + +XLFLAGS += \ + $(EOLIST) +endif + +# +# These are used by the link target if an NLM is being generated +# This is used by the link 'name' directive to name the nlm. If left blank +# TARGET_nlm (see below) will be used. +# +NLM_NAME = mod_h2 + +# +# This is used by the link '-desc ' directive. +# If left blank, NLM_NAME will be used. +# +NLM_DESCRIPTION = Apache $(VERSION_STR) HTTP2 Support module (w/ NGHTTP2 Lib) + +# +# This is used by the '-threadname' directive. If left blank, +# NLM_NAME Thread will be used. +# +NLM_THREAD_NAME = $(NLM_NAME) + +# +# If this is specified, it will override VERSION value in +# $(AP_WORK)/build/NWGNUenvironment.inc +# +NLM_VERSION = + +# +# If this is specified, it will override the default of 64K +# +NLM_STACK_SIZE = 65536 + +# +# If this is specified it will be used by the link '-entry' directive +# +NLM_ENTRY_SYM = + +# +# If this is specified it will be used by the link '-exit' directive +# +NLM_EXIT_SYM = + +# +# If this is specified it will be used by the link '-check' directive +# +NLM_CHECK_SYM = + +# +# If this is specified it will be used by the link '-flags' directive +# +NLM_FLAGS = + +# +# If this is specified it will be linked in with the XDCData option in the def +# file instead of the default of $(NWOS)/apache.xdc. XDCData can be disabled +# by setting APACHE_UNIPROC in the environment +# +XDCDATA = + +# +# Declare all target files (you must add your files here) +# + +# +# If there is an NLM target, put it here +# +TARGET_nlm = \ + $(OBJDIR)/$(NLM_NAME).nlm \ + $(EOLIST) + +# +# If there is an LIB target, put it here +# +TARGET_lib = \ + $(OBJDIR)/nghttp2.lib \ + $(EOLIST) + +# +# These are the OBJ files needed to create the NLM target above. +# Paths must all use the '/' character +# +FILES_nlm_objs := $(sort $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard *.c))) + +# +# These are the LIB files needed to create the NLM target above. +# These will be added as a library command in the link.opt file. +# +FILES_nlm_libs = \ + $(PRELUDE) \ + $(OBJDIR)/nghttp2.lib \ + $(EOLIST) + +# +# These are the modules that the above NLM target depends on to load. +# These will be added as a module command in the link.opt file. +# +FILES_nlm_modules = \ + Libc \ + Apache2 \ + $(EOLIST) + +# +# If the nlm has a msg file, put it's path here +# +FILE_nlm_msg = + +# +# If the nlm has a hlp file put it's path here +# +FILE_nlm_hlp = + +# +# If this is specified, it will override $(NWOS)\copyright.txt. +# +FILE_nlm_copyright = + +# +# Any additional imports go here +# +FILES_nlm_Ximports = \ + @libc.imp \ + @aprlib.imp \ + @httpd.imp \ + $(EOLIST) + +# +# Any symbols exported to here +# +FILES_nlm_exports = \ + h2_module \ + $(EOLIST) + +# +# These are the OBJ files needed to create the LIB target above. +# Paths must all use the '/' character +# +FILES_lib_objs := $(sort $(patsubst $(NGH2SRC)/lib/%.c,$(OBJDIR)/%.o,$(wildcard $(NGH2SRC)/lib/*.c))) +# +# implement targets and dependancies (leave this section alone) +# + +libs :: $(OBJDIR) $(NGH2SRC)/lib/config.h $(TARGET_lib) + +nlms :: libs $(TARGET_nlm) + +# +# Updated this target to create necessary directories and copy files to the +# correct place. (See $(AP_WORK)/build/NWGNUhead.inc for examples) +# +install :: nlms FORCE + $(call COPY,$(OBJDIR)/*.nlm, $(INSTALLBASE)/modules/) + +clean :: + $(call DEL,$(NGH2SRC)/lib/config.h) +# +# Any specialized rules here +# +vpath %.c $(NGH2SRC)/lib + +$(NGH2SRC)/lib/config.h : NWGNUmakefile + @echo $(DL)GEN $@$(DL) + @echo $(DL)/* For NetWare target.$(DL) > $@ + @echo $(DL)** Do not edit - created by Make!$(DL) >> $@ + @echo $(DL)*/$(DL) >> $@ + @echo $(DL)#ifndef NGH2_CONFIG_H$(DL) >> $@ + @echo $(DL)#define NGH2_CONFIG_H$(DL) >> $@ + @echo #define HAVE_ARPA_INET_H 1 >> $@ + @echo #define HAVE_CHOWN 1 >> $@ + @echo #define HAVE_DECL_STRERROR_R 1 >> $@ + @echo #define HAVE_DLFCN_H 1 >> $@ + @echo #define HAVE_DUP2 1 >> $@ + @echo #define HAVE_FCNTL_H 1 >> $@ + @echo #define HAVE_GETCWD 1 >> $@ + @echo #define HAVE_INTTYPES_H 1 >> $@ + @echo #define HAVE_LIMITS_H 1 >> $@ + @echo #define HAVE_LOCALTIME_R 1 >> $@ + @echo #define HAVE_MALLOC 1 >> $@ + @echo #define HAVE_MEMCHR 1 >> $@ + @echo #define HAVE_MEMMOVE 1 >> $@ + @echo #define HAVE_MEMORY_H 1 >> $@ + @echo #define HAVE_MEMSET 1 >> $@ + @echo #define HAVE_NETDB_H 1 >> $@ + @echo #define HAVE_NETINET_IN_H 1 >> $@ + @echo #define HAVE_PTRDIFF_T 1 >> $@ + @echo #define HAVE_PWD_H 1 >> $@ + @echo #define HAVE_SOCKET 1 >> $@ + @echo #define HAVE_SQRT 1 >> $@ + @echo #define HAVE_STDDEF_H 1 >> $@ + @echo #define HAVE_STDINT_H 1 >> $@ + @echo #define HAVE_STDLIB_H 1 >> $@ + @echo #define HAVE_STRCHR 1 >> $@ + @echo #define HAVE_STRDUP 1 >> $@ + @echo #define HAVE_STRERROR 1 >> $@ + @echo #define HAVE_STRERROR_R 1 >> $@ + @echo #define HAVE_STRINGS_H 1 >> $@ + @echo #define HAVE_STRING_H 1 >> $@ + @echo #define HAVE_STRSTR 1 >> $@ + @echo #define HAVE_STRTOL 1 >> $@ + @echo #define HAVE_STRTOUL 1 >> $@ + @echo #define HAVE_SYSLOG_H 1 >> $@ + @echo #define HAVE_SYS_SOCKET_H 1 >> $@ + @echo #define HAVE_SYS_STAT_H 1 >> $@ + @echo #define HAVE_SYS_TIME_H 1 >> $@ + @echo #define HAVE_SYS_TYPES_H 1 >> $@ + @echo #define HAVE_TIME_H 1 >> $@ + @echo #define HAVE_UNISTD_H 1 >> $@ + + @echo #define SIZEOF_INT_P 4 >> $@ + @echo #define STDC_HEADERS 1 >> $@ + @echo #define STRERROR_R_CHAR_P 4 >> $@ + +# Hint to compiler a function parameter is not used + @echo #define _U_ >> $@ + + @echo #ifndef __cplusplus >> $@ + @echo #define inline __inline >> $@ + @echo #endif >> $@ + + @echo $(DL)#endif /* NGH2_CONFIG_H */$(DL) >> $@ + +# +# Include the 'tail' makefile that has targets that depend on variables defined +# in this makefile +# + +include $(APBUILD)/NWGNUtail.inc + + diff --git a/modules/http2/README.h2 b/modules/http2/README.h2 new file mode 100644 index 00000000000..803f787f9a1 --- /dev/null +++ b/modules/http2/README.h2 @@ -0,0 +1,70 @@ +The h2 module adds support for the HTTP/2 protocol to the server. + +Specifically, it supports the protocols "h2" (HTTP2 over TLS) and "h2c" +(HTTP2 over plain HTTP connections via Upgrade). Additionally it offers +the "direct" mode for both encrypted and unencrypted connections. + +You may enable it for the whole server or specific virtual hosts only. + + +BUILD + +If you have libnghttp2 (https://nghttp2.org) installed on your system, simply +add + + --enable-h2 + +to your httpd ./configure invocation. Should libnghttp2 reside in a unusual +location, add + + --with-nghttp2= + +to ./configure. is expected to be the installation prefix, so there +should be a /lib/libnghttp2.*. If your system support pkg-config, +/lib/pkgconfig/libnghttp2.pc will be inspected. + +If you want to link nghttp2 statically into the mod_h2 module, you may +similarly to mod_ssl add + + --enable-nghttp2-staticlib-deps + +For this, the lib directory should only contain the libnghttp2.a, not its +shared cousins. + + +CONFIGURATION + +If mod_h2 is enabled for a site or not depends on the new "Protocols" +directive. This directive list all protocols enabled for a server or +virtual host. + +If you do not specify "Protocols" all available protocols are enabled. For +sites using TLS, the protocol supported by mod_h2 is "h2". For cleartext +http:, the offered protocol is "h2c". + +The following is an example of a server that only supports http/1.1 in +general and offers h2 for a specific virtual host. + + ... + Protocols http/1.1 + + Protocols h2 http/1.1 + ... + + +Please see the documentation of mod_h2 for a complete list and explanation +of all options. + + +TLS CONFIGURATION + +If you want to use HTTP/2 with a browser, most modern browsers will support +it without further configuration. However, browsers so far only support +HTTP/2 over TLS and are expecially picky about the certificate and +encryption ciphers used. + +Server admins may look for up-to-date information about "modern" TLS +compatibility under: + + https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility + diff --git a/modules/http2/config.m4 b/modules/http2/config.m4 new file mode 100644 index 00000000000..6bb231fbb87 --- /dev/null +++ b/modules/http2/config.m4 @@ -0,0 +1,186 @@ +dnl Licensed to the Apache Software Foundation (ASF) under one or more +dnl contributor license agreements. See the NOTICE file distributed with +dnl this work for additional information regarding copyright ownership. +dnl The ASF licenses this file to You under the Apache License, Version 2.0 +dnl (the "License"); you may not use this file except in compliance with +dnl the License. You may obtain a copy of the License at +dnl +dnl http://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, +dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +dnl See the License for the specific language governing permissions and +dnl limitations under the License. + +dnl # start of module specific part +APACHE_MODPATH_INIT(http2) + +dnl # list of module object files +h2_objs="dnl +mod_h2.lo dnl +h2_alt_svc.lo dnl +h2_config.lo dnl +h2_conn.lo dnl +h2_conn_io.lo dnl +h2_ctx.lo dnl +h2_from_h1.lo dnl +h2_h2.lo dnl +h2_io.lo dnl +h2_io_set.lo dnl +h2_mplx.lo dnl +h2_request.lo dnl +h2_response.lo dnl +h2_session.lo dnl +h2_stream.lo dnl +h2_stream_set.lo dnl +h2_switch.lo dnl +h2_task.lo dnl +h2_task_input.lo dnl +h2_task_output.lo dnl +h2_task_queue.lo dnl +h2_to_h1.lo dnl +h2_util.lo dnl +h2_worker.lo dnl +h2_workers.lo dnl +" + +dnl +dnl APACHE_CHECK_NGHTTP2 +dnl +dnl Configure for nghttp2, giving preference to +dnl "--with-nghttp2=" if it was specified. +dnl +AC_DEFUN(APACHE_CHECK_NGHTTP2,[ + AC_CACHE_CHECK([for nghttp2], [ac_cv_nghttp2], [ + dnl initialise the variables we use + ac_cv_nghttp2=no + ap_nghttp2_found="" + ap_nghttp2_base="" + ap_nghttp2_libs="" + + dnl Determine the nghttp2 base directory, if any + AC_MSG_CHECKING([for user-provided nghttp2 base directory]) + AC_ARG_WITH(nghttp2, APACHE_HELP_STRING(--with-nghttp2=PATH, nghttp2 installation directory), [ + dnl If --with-nghttp2 specifies a directory, we use that directory + if test "x$withval" != "xyes" -a "x$withval" != "x"; then + dnl This ensures $withval is actually a directory and that it is absolute + ap_nghttp2_base="`cd $withval ; pwd`" + fi + ]) + if test "x$ap_nghttp2_base" = "x"; then + AC_MSG_RESULT(none) + else + AC_MSG_RESULT($ap_nghttp2_base) + fi + + dnl Run header and version checks + saved_CPPFLAGS="$CPPFLAGS" + saved_LIBS="$LIBS" + saved_LDFLAGS="$LDFLAGS" + + dnl Before doing anything else, load in pkg-config variables + if test -n "$PKGCONFIG"; then + saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" + AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH]) + if test "x$ap_nghttp2_base" != "x" -a \ + -f "${ap_nghttp2_base}/lib/pkgconfig/libnghttp2.pc"; then + dnl Ensure that the given path is used by pkg-config too, otherwise + dnl the system libnghttp2.pc might be picked up instead. + PKG_CONFIG_PATH="${ap_nghttp2_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + fi + AC_ARG_ENABLE(nghttp2-staticlib-deps,APACHE_HELP_STRING(--enable-nghttp2-staticlib-deps,[link mod_h2 with dependencies of libnghttp2's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-h2.]), [ + if test "$enableval" = "yes"; then + PKGCONFIG_LIBOPTS="--static" + fi + ]) + ap_nghttp2_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libnghttp2`" + if test $? -eq 0; then + ap_nghttp2_found="yes" + pkglookup="`$PKGCONFIG --cflags-only-I libnghttp2`" + APR_ADDTO(CPPFLAGS, [$pkglookup]) + APR_ADDTO(MOD_CFLAGS, [$pkglookup]) + APR_ADDTO(ab_CFLAGS, [$pkglookup]) + pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libnghttp2`" + APR_ADDTO(LDFLAGS, [$pkglookup]) + APR_ADDTO(MOD_LDFLAGS, [$pkglookup]) + pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libnghttp2`" + APR_ADDTO(LDFLAGS, [$pkglookup]) + APR_ADDTO(MOD_LDFLAGS, [$pkglookup]) + fi + PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH" + fi + + dnl fall back to the user-supplied directory if not found via pkg-config + if test "x$ap_nghttp2_base" != "x" -a "x$ap_nghttp2_found" = "x"; then + APR_ADDTO(CPPFLAGS, [-I$ap_nghttp2_base/include]) + APR_ADDTO(MOD_CFLAGS, [-I$ap_nghttp2_base/include]) + APR_ADDTO(ab_CFLAGS, [-I$ap_nghttp2_base/include]) + APR_ADDTO(LDFLAGS, [-L$ap_nghttp2_base/lib]) + APR_ADDTO(MOD_LDFLAGS, [-L$ap_nghttp2_base/lib]) + if test "x$ap_platform_runtime_link_flag" != "x"; then + APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_nghttp2_base/lib]) + APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_nghttp2_base/lib]) + fi + fi + + AC_MSG_CHECKING([for nghttp2 version >= 1.0.0]) + AC_TRY_COMPILE([#include ],[ +#if !defined(NGHTTP2_VERSION_NUM) +#error "Missing nghttp2 version" +#endif +#if NGHTTP2_VERSION_NUM < 0x010000 +#error "Unsupported nghttp2 version " NGHTTP2_VERSION_TEXT +#endif], + [AC_MSG_RESULT(OK) + ac_cv_nghttp2=yes], + [AC_MSG_RESULT(FAILED)]) + + if test "x$ac_cv_nghttp2" = "xyes"; then + ap_nghttp2_libs="${ap_nghttp2_libs:--lnghttp2} `$apr_config --libs`" + APR_ADDTO(MOD_LDFLAGS, [$ap_nghttp2_libs]) + APR_ADDTO(LIBS, [$ap_nghttp2_libs]) + APR_SETVAR(ab_LDFLAGS, [$MOD_LDFLAGS]) + APACHE_SUBST(ab_CFLAGS) + APACHE_SUBST(ab_LDFLAGS) + + dnl Run library and function checks + liberrors="" + AC_CHECK_HEADERS([nghttp2/nghttp2.h]) + AC_CHECK_FUNCS([nghttp2_session_server_new2], [], [liberrors="yes"]) + if test "x$liberrors" != "x"; then + AC_MSG_WARN([nghttp2 library is unusable]) + fi + else + AC_MSG_WARN([nghttp2 version is too old]) + fi + + dnl restore + CPPFLAGS="$saved_CPPFLAGS" + LIBS="$saved_LIBS" + LDFLAGS="$saved_LDFLAGS" + ]) + if test "x$ac_cv_nghttp2" = "xyes"; then + AC_DEFINE(HAVE_NGHTTP2, 1, [Define if nghttp2 is available]) + fi +]) + + +dnl # hook module into the Autoconf mechanism (--enable-h2 option) +APACHE_MODULE(h2, [HTTP/2 support (mod_h2)], $h2_objs, , most, [ + APACHE_CHECK_NGHTTP2 + if test "$ac_cv_nghttp2" = "yes" ; then + if test "x$enable_ssl" = "xshared"; then + # The only symbol which needs to be exported is the module + # structure, so ask libtool to hide everything else: + APR_ADDTO(MOD_H2_LDADD, [-export-symbols-regex h2_module]) + fi + else + enable_h2=no + fi +]) + +dnl # end of module specific part +APACHE_MODPATH_FINISH + diff --git a/modules/http2/h2_alt_svc.c b/modules/http2/h2_alt_svc.c new file mode 100644 index 00000000000..d18ae5f20de --- /dev/null +++ b/modules/http2/h2_alt_svc.c @@ -0,0 +1,132 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_alt_svc.h" +#include "h2_ctx.h" +#include "h2_config.h" +#include "h2_h2.h" +#include "h2_util.h" + +static int h2_alt_svc_handler(request_rec *r); + +void h2_alt_svc_register_hooks(void) +{ + ap_hook_post_read_request(h2_alt_svc_handler, NULL, NULL, APR_HOOK_MIDDLE); +} + +/** + * Parse an Alt-Svc specifier as described in "HTTP Alternative Services" + * (https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-04) + * with the following changes: + * - do not percent encode token values + * - do not use quotation marks + */ +h2_alt_svc *h2_alt_svc_parse(const char *s, apr_pool_t *pool) { + const char *sep = ap_strchr_c(s, '='); + if (sep) { + const char *alpn = apr_pstrndup(pool, s, sep - s); + const char *host = NULL; + int port = 0; + s = sep + 1; + sep = ap_strchr_c(s, ':'); /* mandatory : */ + if (sep) { + if (sep != s) { /* optional host */ + host = apr_pstrndup(pool, s, sep - s); + } + s = sep + 1; + if (*s) { /* must be a port number */ + port = (int)apr_atoi64(s); + if (port > 0 && port < (0x1 << 16)) { + h2_alt_svc *as = apr_pcalloc(pool, sizeof(*as)); + as->alpn = alpn; + as->host = host; + as->port = port; + return as; + } + } + } + } + return NULL; +} + +#define h2_alt_svc_IDX(list, i) ((h2_alt_svc**)(list)->elts)[i] + +static int h2_alt_svc_handler(request_rec *r) +{ + h2_ctx *ctx; + h2_config *cfg; + int i; + + if (r->connection->keepalives > 0) { + /* Only announce Alt-Svc on the first response */ + return DECLINED; + } + + ctx = h2_ctx_rget(r); + if (h2_ctx_is_active(ctx) || h2_ctx_is_task(ctx)) { + return DECLINED; + } + + cfg = h2_config_rget(r); + if (r->hostname && cfg && cfg->alt_svcs && cfg->alt_svcs->nelts > 0) { + const char *alt_svc_used = apr_table_get(r->headers_in, "Alt-Svc-Used"); + if (!alt_svc_used) { + /* We have alt-svcs defined and client is not already using + * one, announce the services that were configured and match. + * The security of this connection determines if we allow + * other host names or ports only. + */ + const char *alt_svc = ""; + const char *svc_ma = ""; + int secure = h2_h2_is_tls(r->connection); + int ma = h2_config_geti(cfg, H2_CONF_ALT_SVC_MAX_AGE); + if (ma >= 0) { + svc_ma = apr_psprintf(r->pool, "; ma=%d", ma); + } + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "h2_alt_svc: announce %s for %s:%d", + (secure? "secure" : "insecure"), + r->hostname, (int)r->server->port); + for (i = 0; i < cfg->alt_svcs->nelts; ++i) { + h2_alt_svc *as = h2_alt_svc_IDX(cfg->alt_svcs, i); + const char *ahost = as->host; + if (ahost && !apr_strnatcasecmp(ahost, r->hostname)) { + ahost = NULL; + } + if (secure || !ahost) { + alt_svc = apr_psprintf(r->pool, "%s%s%s=\"%s:%d\"%s", + alt_svc, + (*alt_svc? ", " : ""), as->alpn, + ahost? ahost : "", as->port, + svc_ma); + } + } + if (*alt_svc) { + apr_table_set(r->headers_out, "Alt-Svc", alt_svc); + } + } + } + + return DECLINED; +} + diff --git a/modules/http2/h2_alt_svc.h b/modules/http2/h2_alt_svc.h new file mode 100644 index 00000000000..51f89d00b83 --- /dev/null +++ b/modules/http2/h2_alt_svc.h @@ -0,0 +1,39 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_alt_svc__ +#define __mod_h2__h2_alt_svc__ + +typedef struct h2_alt_svc h2_alt_svc; + +struct h2_alt_svc { + const char *alpn; + const char *host; + int port; +}; + +void h2_alt_svc_register_hooks(void); + +/** + * Parse an Alt-Svc specifier as described in "HTTP Alternative Services" + * (https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-04) + * with the following changes: + * - do not percent encode token values + * - do not use quotation marks + */ +h2_alt_svc *h2_alt_svc_parse(const char *s, apr_pool_t *pool); + + +#endif /* defined(__mod_h2__h2_alt_svc__) */ diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c new file mode 100644 index 00000000000..883f273a38f --- /dev/null +++ b/modules/http2/h2_config.c @@ -0,0 +1,416 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include "h2_alt_svc.h" +#include "h2_ctx.h" +#include "h2_conn.h" +#include "h2_config.h" +#include "h2_private.h" + +#define DEF_VAL (-1) + +#define H2_CONFIG_GET(a, b, n) \ + (((a)->n == DEF_VAL)? (b) : (a))->n + +static h2_config defconf = { + "default", + 100, /* max_streams */ + 64 * 1024, /* window_size */ + -1, /* min workers */ + -1, /* max workers */ + 10 * 60, /* max workers idle secs */ + 64 * 1024, /* stream max mem size */ + NULL, /* no alt-svcs */ + -1, /* alt-svc max age */ + 0, /* serialize headers */ + -1, /* h2 direct mode */ + -1, /* # session extra files */ +}; + +static int files_per_session = 0; + +void h2_config_init(apr_pool_t *pool) { + /* Determine a good default for this platform and mpm? + * TODO: not sure how APR wants to hand out this piece of + * information. + */ + int max_files = 256; + int conn_threads = 1; + int tx_files = max_files / 4; + + (void)pool; + ap_mpm_query(AP_MPMQ_MAX_THREADS, &conn_threads); + switch (h2_conn_mpm_type()) { + case H2_MPM_PREFORK: + case H2_MPM_WORKER: + case H2_MPM_EVENT: + /* allow that many transfer open files per mplx */ + files_per_session = (tx_files / conn_threads); + break; + default: + /* don't know anything about it, stay safe */ + break; + } +} + +static void *h2_config_create(apr_pool_t *pool, + const char *prefix, const char *x) +{ + h2_config *conf = (h2_config *)apr_pcalloc(pool, sizeof(h2_config)); + + const char *s = x? x : "unknown"; + char *name = apr_pcalloc(pool, strlen(prefix) + strlen(s) + 20); + strcpy(name, prefix); + strcat(name, "["); + strcat(name, s); + strcat(name, "]"); + + conf->name = name; + conf->h2_max_streams = DEF_VAL; + conf->h2_window_size = DEF_VAL; + conf->min_workers = DEF_VAL; + conf->max_workers = DEF_VAL; + conf->max_worker_idle_secs = DEF_VAL; + conf->stream_max_mem_size = DEF_VAL; + conf->alt_svc_max_age = DEF_VAL; + conf->serialize_headers = DEF_VAL; + conf->h2_direct = DEF_VAL; + conf->session_extra_files = DEF_VAL; + return conf; +} + +void *h2_config_create_svr(apr_pool_t *pool, server_rec *s) +{ + return h2_config_create(pool, "srv", s->defn_name); +} + +void *h2_config_create_dir(apr_pool_t *pool, char *x) +{ + return h2_config_create(pool, "dir", x); +} + +void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv) +{ + h2_config *base = (h2_config *)basev; + h2_config *add = (h2_config *)addv; + h2_config *n = (h2_config *)apr_pcalloc(pool, sizeof(h2_config)); + + char *name = apr_pcalloc(pool, 20 + strlen(add->name) + strlen(base->name)); + strcpy(name, "merged["); + strcat(name, add->name); + strcat(name, ", "); + strcat(name, base->name); + strcat(name, "]"); + n->name = name; + + n->h2_max_streams = H2_CONFIG_GET(add, base, h2_max_streams); + n->h2_window_size = H2_CONFIG_GET(add, base, h2_window_size); + n->min_workers = H2_CONFIG_GET(add, base, min_workers); + n->max_workers = H2_CONFIG_GET(add, base, max_workers); + n->max_worker_idle_secs = H2_CONFIG_GET(add, base, max_worker_idle_secs); + n->stream_max_mem_size = H2_CONFIG_GET(add, base, stream_max_mem_size); + n->alt_svcs = add->alt_svcs? add->alt_svcs : base->alt_svcs; + n->alt_svc_max_age = H2_CONFIG_GET(add, base, alt_svc_max_age); + n->serialize_headers = H2_CONFIG_GET(add, base, serialize_headers); + n->h2_direct = H2_CONFIG_GET(add, base, h2_direct); + n->session_extra_files = H2_CONFIG_GET(add, base, session_extra_files); + + return n; +} + +int h2_config_geti(h2_config *conf, h2_config_var_t var) +{ + int n; + switch(var) { + case H2_CONF_MAX_STREAMS: + return H2_CONFIG_GET(conf, &defconf, h2_max_streams); + case H2_CONF_WIN_SIZE: + return H2_CONFIG_GET(conf, &defconf, h2_window_size); + case H2_CONF_MIN_WORKERS: + return H2_CONFIG_GET(conf, &defconf, min_workers); + case H2_CONF_MAX_WORKERS: + return H2_CONFIG_GET(conf, &defconf, max_workers); + case H2_CONF_MAX_WORKER_IDLE_SECS: + return H2_CONFIG_GET(conf, &defconf, max_worker_idle_secs); + case H2_CONF_STREAM_MAX_MEM: + return H2_CONFIG_GET(conf, &defconf, stream_max_mem_size); + case H2_CONF_ALT_SVC_MAX_AGE: + return H2_CONFIG_GET(conf, &defconf, alt_svc_max_age); + case H2_CONF_SER_HEADERS: + return H2_CONFIG_GET(conf, &defconf, serialize_headers); + case H2_CONF_DIRECT: + return H2_CONFIG_GET(conf, &defconf, h2_direct); + case H2_CONF_SESSION_FILES: + n = H2_CONFIG_GET(conf, &defconf, session_extra_files); + if (n < 0) { + n = files_per_session; + } + return n; + default: + return DEF_VAL; + } +} + +h2_config *h2_config_sget(server_rec *s) +{ + h2_config *cfg = (h2_config *)ap_get_module_config(s->module_config, + &h2_module); + AP_DEBUG_ASSERT(cfg); + return cfg; +} + + +static const char *h2_conf_set_max_streams(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + cfg->h2_max_streams = (int)apr_atoi64(value); + (void)arg; + if (cfg->h2_max_streams < 1) { + return "value must be > 0"; + } + return NULL; +} + +static const char *h2_conf_set_window_size(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + cfg->h2_window_size = (int)apr_atoi64(value); + (void)arg; + if (cfg->h2_window_size < 1024) { + return "value must be > 1k"; + } + return NULL; +} + +static const char *h2_conf_set_min_workers(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + cfg->min_workers = (int)apr_atoi64(value); + (void)arg; + if (cfg->min_workers < 1) { + return "value must be > 1"; + } + return NULL; +} + +static const char *h2_conf_set_max_workers(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + cfg->max_workers = (int)apr_atoi64(value); + (void)arg; + if (cfg->max_workers < 1) { + return "value must be > 1"; + } + return NULL; +} + +static const char *h2_conf_set_max_worker_idle_secs(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + cfg->max_worker_idle_secs = (int)apr_atoi64(value); + (void)arg; + if (cfg->max_worker_idle_secs < 1) { + return "value must be > 1"; + } + return NULL; +} + +static const char *h2_conf_set_stream_max_mem_size(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + + + cfg->stream_max_mem_size = (int)apr_atoi64(value); + (void)arg; + if (cfg->stream_max_mem_size < 1024) { + return "value must be > 1k"; + } + return NULL; +} + +static const char *h2_add_alt_svc(cmd_parms *parms, + void *arg, const char *value) +{ + if (value && strlen(value)) { + h2_config *cfg = h2_config_sget(parms->server); + h2_alt_svc *as = h2_alt_svc_parse(value, parms->pool); + if (!as) { + return "unable to parse alt-svc specifier"; + } + if (!cfg->alt_svcs) { + cfg->alt_svcs = apr_array_make(parms->pool, 5, sizeof(h2_alt_svc*)); + } + APR_ARRAY_PUSH(cfg->alt_svcs, h2_alt_svc*) = as; + } + (void)arg; + return NULL; +} + +static const char *h2_conf_set_alt_svc_max_age(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + cfg->alt_svc_max_age = (int)apr_atoi64(value); + (void)arg; + return NULL; +} + +static const char *h2_conf_set_session_extra_files(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + apr_int64_t max = (int)apr_atoi64(value); + if (max <= 0) { + return "value must be a positive number"; + } + cfg->session_extra_files = (int)max; + (void)arg; + return NULL; +} + +static const char *h2_conf_set_serialize_headers(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + if (!strcasecmp(value, "On")) { + cfg->serialize_headers = 1; + return NULL; + } + else if (!strcasecmp(value, "Off")) { + cfg->serialize_headers = 0; + return NULL; + } + + (void)arg; + return "value must be On or Off"; +} + +static const char *h2_conf_set_direct(cmd_parms *parms, + void *arg, const char *value) +{ + h2_config *cfg = h2_config_sget(parms->server); + if (!strcasecmp(value, "On")) { + cfg->h2_direct = 1; + return NULL; + } + else if (!strcasecmp(value, "Off")) { + cfg->h2_direct = 0; + return NULL; + } + + (void)arg; + return "value must be On or Off"; +} + +#define AP_END_CMD AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) + + +const command_rec h2_cmds[] = { + AP_INIT_TAKE1("H2MaxSessionStreams", h2_conf_set_max_streams, NULL, + RSRC_CONF, "maximum number of open streams per session"), + AP_INIT_TAKE1("H2WindowSize", h2_conf_set_window_size, NULL, + RSRC_CONF, "window size on client DATA"), + AP_INIT_TAKE1("H2MinWorkers", h2_conf_set_min_workers, NULL, + RSRC_CONF, "minimum number of worker threads per child"), + AP_INIT_TAKE1("H2MaxWorkers", h2_conf_set_max_workers, NULL, + RSRC_CONF, "maximum number of worker threads per child"), + AP_INIT_TAKE1("H2MaxWorkerIdleSeconds", h2_conf_set_max_worker_idle_secs, NULL, + RSRC_CONF, "maximum number of idle seconds before a worker shuts down"), + AP_INIT_TAKE1("H2StreamMaxMemSize", h2_conf_set_stream_max_mem_size, NULL, + RSRC_CONF, "maximum number of bytes buffered in memory for a stream"), + AP_INIT_TAKE1("H2AltSvc", h2_add_alt_svc, NULL, + RSRC_CONF, "adds an Alt-Svc for this server"), + AP_INIT_TAKE1("H2AltSvcMaxAge", h2_conf_set_alt_svc_max_age, NULL, + RSRC_CONF, "set the maximum age (in seconds) that client can rely on alt-svc information"), + AP_INIT_TAKE1("H2SerializeHeaders", h2_conf_set_serialize_headers, NULL, + RSRC_CONF, "on to enable header serialization for compatibility"), + AP_INIT_TAKE1("H2Direct", h2_conf_set_direct, NULL, + RSRC_CONF, "on to enable direct HTTP/2 mode"), + AP_INIT_TAKE1("H2SessionExtraFiles", h2_conf_set_session_extra_files, NULL, + RSRC_CONF, "number of extra file a session might keep open"), + AP_END_CMD +}; + + +h2_config *h2_config_rget(request_rec *r) +{ + h2_config *cfg = (h2_config *)ap_get_module_config(r->per_dir_config, + &h2_module); + return cfg? cfg : h2_config_sget(r->server); +} + +h2_config *h2_config_get(conn_rec *c) +{ + h2_ctx *ctx = h2_ctx_get(c); + if (ctx->config) { + return ctx->config; + } + if (!ctx->server && ctx->hostname) { + /* We have a host agreed upon via TLS SNI, but no request yet. + * The sni host was accepted and therefore does match a server record + * (vhost) for it. But we need to know which one. + * Normally, it is enough to be set on the initial request on a + * connection, but we need it earlier. Simulate a request and call + * the vhost matching stuff. + */ + apr_uri_t uri; + request_rec r; + memset(&uri, 0, sizeof(uri)); + uri.scheme = (char*)"https"; + uri.hostinfo = (char*)ctx->hostname; + uri.hostname = (char*)ctx->hostname; + uri.port_str = (char*)""; + uri.port = c->local_addr->port; + uri.path = (char*)"/"; + + memset(&r, 0, sizeof(r)); + r.uri = (char*)"/"; + r.connection = c; + r.pool = c->pool; + r.hostname = ctx->hostname; + r.headers_in = apr_table_make(c->pool, 1); + r.parsed_uri = uri; + r.status = HTTP_OK; + r.server = r.connection->base_server; + ap_update_vhost_from_headers(&r); + ctx->server = r.server; + } + + if (ctx->server) { + ctx->config = h2_config_sget(ctx->server); + return ctx->config; + } + + return h2_config_sget(c->base_server); +} + diff --git a/modules/http2/h2_config.h b/modules/http2/h2_config.h new file mode 100644 index 00000000000..931af59d302 --- /dev/null +++ b/modules/http2/h2_config.h @@ -0,0 +1,74 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_config_h__ +#define __mod_h2__h2_config_h__ + +#undef PACKAGE_VERSION +#undef PACKAGE_TARNAME +#undef PACKAGE_STRING +#undef PACKAGE_NAME +#undef PACKAGE_BUGREPORT + +typedef enum { + H2_CONF_MAX_STREAMS, + H2_CONF_WIN_SIZE, + H2_CONF_MIN_WORKERS, + H2_CONF_MAX_WORKERS, + H2_CONF_MAX_WORKER_IDLE_SECS, + H2_CONF_STREAM_MAX_MEM, + H2_CONF_ALT_SVCS, + H2_CONF_ALT_SVC_MAX_AGE, + H2_CONF_SER_HEADERS, + H2_CONF_DIRECT, + H2_CONF_SESSION_FILES, +} h2_config_var_t; + +/* Apache httpd module configuration for h2. */ +typedef struct h2_config { + const char *name; + int h2_max_streams; /* max concurrent # streams (http2) */ + int h2_window_size; /* stream window size (http2) */ + int min_workers; /* min # of worker threads/child */ + int max_workers; /* max # of worker threads/child */ + int max_worker_idle_secs; /* max # of idle seconds for worker */ + int stream_max_mem_size; /* max # bytes held in memory/stream */ + apr_array_header_t *alt_svcs; /* h2_alt_svc specs for this server */ + int alt_svc_max_age; /* seconds clients can rely on alt-svc info*/ + int serialize_headers; /* Use serialized HTTP/1.1 headers for + processing, better compatibility */ + int h2_direct; /* if mod_h2 is active directly */ + int session_extra_files; /* # of extra files a session may keep open */ +} h2_config; + + +void *h2_config_create_dir(apr_pool_t *pool, char *x); +void *h2_config_create_svr(apr_pool_t *pool, server_rec *s); +void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv); + +apr_status_t h2_config_apply_header(h2_config *config, request_rec *r); + +extern const command_rec h2_cmds[]; + +h2_config *h2_config_get(conn_rec *c); +h2_config *h2_config_sget(server_rec *s); +h2_config *h2_config_rget(request_rec *r); + +int h2_config_geti(h2_config *conf, h2_config_var_t var); + +void h2_config_init(apr_pool_t *pool); + +#endif /* __mod_h2__h2_config_h__ */ + diff --git a/modules/http2/h2_conn.c b/modules/http2/h2_conn.c new file mode 100644 index 00000000000..38e5c11a283 --- /dev/null +++ b/modules/http2/h2_conn.c @@ -0,0 +1,532 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_config.h" +#include "h2_ctx.h" +#include "h2_mplx.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_stream_set.h" +#include "h2_task.h" +#include "h2_worker.h" +#include "h2_workers.h" +#include "h2_conn.h" + +static struct h2_workers *workers; + +static apr_status_t h2_session_process(h2_session *session); + +static h2_mpm_type_t mpm_type = H2_MPM_UNKNOWN; +static module *mpm_module; +static module *ssl_module; +static int checked; + +static void check_modules(void) +{ + int i; + if (!checked) { + for (i = 0; ap_loaded_modules[i]; ++i) { + module *m = ap_loaded_modules[i]; + if (!strcmp("event.c", m->name)) { + mpm_type = H2_MPM_EVENT; + mpm_module = m; + } + else if (!strcmp("worker.c", m->name)) { + mpm_type = H2_MPM_WORKER; + mpm_module = m; + } + else if (!strcmp("prefork.c", m->name)) { + mpm_type = H2_MPM_PREFORK; + mpm_module = m; + } + else if (!strcmp("mod_ssl.c", m->name)) { + ssl_module = m; + } + } + checked = 1; + } +} + +apr_status_t h2_conn_child_init(apr_pool_t *pool, server_rec *s) +{ + h2_config *config = h2_config_sget(s); + apr_status_t status = APR_SUCCESS; + int minw = h2_config_geti(config, H2_CONF_MIN_WORKERS); + int maxw = h2_config_geti(config, H2_CONF_MAX_WORKERS); + + int max_threads_per_child = 0; + int threads_limit = 0; + int idle_secs = 0; + int i; + + h2_config_init(pool); + + ap_mpm_query(AP_MPMQ_MAX_THREADS, &max_threads_per_child); + ap_mpm_query(AP_MPMQ_HARD_LIMIT_THREADS, &threads_limit); + + for (i = 0; ap_loaded_modules[i]; ++i) { + module *m = ap_loaded_modules[i]; + if (!strcmp("event.c", m->name)) { + mpm_type = H2_MPM_EVENT; + mpm_module = m; + } + else if (!strcmp("worker.c", m->name)) { + mpm_type = H2_MPM_WORKER; + mpm_module = m; + } + else if (!strcmp("prefork.c", m->name)) { + mpm_type = H2_MPM_PREFORK; + mpm_module = m; + } + else if (!strcmp("mod_ssl.c", m->name)) { + ssl_module = m; + } + } + + if (minw <= 0) { + minw = max_threads_per_child; + } + if (maxw <= 0) { + maxw = threads_limit; + if (maxw < minw) { + maxw = minw; + } + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, + "h2_workers: min=%d max=%d, mthrpchild=%d, thr_limit=%d", + minw, maxw, max_threads_per_child, threads_limit); + + workers = h2_workers_create(s, pool, minw, maxw); + idle_secs = h2_config_geti(config, H2_CONF_MAX_WORKER_IDLE_SECS); + h2_workers_set_max_idle_secs(workers, idle_secs); + + return status; +} + +h2_mpm_type_t h2_conn_mpm_type(void) { + check_modules(); + return mpm_type; +} + +static module *h2_conn_mpm_module(void) { + check_modules(); + return mpm_module; +} + +apr_status_t h2_conn_rprocess(request_rec *r) +{ + h2_config *config = h2_config_rget(r); + h2_session *session; + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "h2_conn_process start"); + if (!workers) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02911) + "workers not initialized"); + return APR_EGENERAL; + } + + session = h2_session_rcreate(r, config, workers); + if (!session) { + return APR_EGENERAL; + } + + return h2_session_process(session); +} + +apr_status_t h2_conn_main(conn_rec *c) +{ + h2_config *config = h2_config_get(c); + h2_session *session; + apr_status_t status; + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "h2_conn_main start"); + if (!workers) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02912) + "workers not initialized"); + return APR_EGENERAL; + } + + session = h2_session_create(c, config, workers); + if (!session) { + return APR_EGENERAL; + } + + status = h2_session_process(session); + + /* Make sure this connection gets closed properly. */ + c->keepalive = AP_CONN_CLOSE; + if (c->cs) { + c->cs->state = CONN_STATE_WRITE_COMPLETION; + } + + return status; +} + +apr_status_t h2_session_process(h2_session *session) +{ + apr_status_t status = APR_SUCCESS; + int rv = 0; + apr_interval_time_t wait_micros = 0; + static const int MAX_WAIT_MICROS = 200 * 1000; + + /* Start talking to the client. Apart from protocol meta data, + * we mainly will see new http/2 streams opened by the client, which + * basically are http requests we need to dispatch. + * + * There will be bursts of new streams, to be served concurrently, + * followed by long pauses of no activity. + * + * Since the purpose of http/2 is to allow siumultaneous streams, we + * need to dispatch the handling of each stream into a separate worker + * thread, keeping this thread open for sending responses back as + * soon as they arrive. + * At the same time, we need to continue reading new frames from + * our client, which may be meta (WINDOWS_UPDATEs, PING, SETTINGS) or + * new streams. + * + * As long as we have streams open in this session, we cannot really rest + * since there are two conditions to wait on: 1. new data from the client, + * 2. new data from the open streams to send back. + * + * Only when we have no more streams open, can we do a blocking read + * on our connection. + * + * TODO: implement graceful GO_AWAY after configurable idle time + */ + + ap_update_child_status_from_conn(session->c->sbh, SERVER_BUSY_READ, + session->c); + + if (APLOGctrace2(session->c)) { + ap_filter_t *filter = session->c->input_filters; + while (filter) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_conn(%ld), has connection filter %s", + session->id, filter->frec->name); + filter = filter->next; + } + } + + status = h2_session_start(session, &rv); + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, + "h2_session(%ld): starting on %s:%d", session->id, + session->c->base_server->defn_name, + session->c->local_addr->port); + if (status != APR_SUCCESS) { + h2_session_abort(session, status, rv); + h2_session_destroy(session); + return status; + } + + while (!h2_session_is_done(session)) { + int have_written = 0; + int have_read = 0; + int got_streams; + + status = h2_session_write(session, wait_micros); + if (status == APR_SUCCESS) { + have_written = 1; + wait_micros = 0; + } + else if (status == APR_EAGAIN) { + /* nop */ + } + else if (status == APR_TIMEUP) { + wait_micros *= 2; + if (wait_micros > MAX_WAIT_MICROS) { + wait_micros = MAX_WAIT_MICROS; + } + } + else { + ap_log_cerror( APLOG_MARK, APLOG_DEBUG, status, session->c, + "h2_session(%ld): writing, terminating", + session->id); + h2_session_abort(session, status, 0); + break; + } + + /* We would like to do blocking reads as often as possible as they + * are more efficient in regard to server resources. + * We can do them under the following circumstances: + * - we have no open streams and therefore have nothing to write + * - we have just started the session and are waiting for the first + * two frames to come in. There will always be at least 2 frames as + * * h2 will send SETTINGS and SETTINGS-ACK + * * h2c will count the header settings as one frame and we + * submit our settings and need the ACK. + */ + got_streams = !h2_stream_set_is_empty(session->streams); + status = h2_session_read(session, + (!got_streams + || session->frames_received <= 1)? + APR_BLOCK_READ : APR_NONBLOCK_READ); + switch (status) { + case APR_SUCCESS: + /* successful read, reset our idle timers */ + have_read = 1; + wait_micros = 0; + break; + case APR_EAGAIN: + break; + case APR_EBADF: + case APR_EOF: + case APR_ECONNABORTED: + case APR_ECONNRESET: + ap_log_cerror( APLOG_MARK, APLOG_DEBUG, status, session->c, + "h2_session(%ld): reading", + session->id); + h2_session_abort(session, status, 0); + break; + default: + ap_log_cerror( APLOG_MARK, APLOG_WARNING, status, session->c, + APLOGNO(02950) + "h2_session(%ld): error reading, terminating", + session->id); + h2_session_abort(session, status, 0); + break; + } + + if (!have_read && !have_written + && !h2_stream_set_is_empty(session->streams)) { + /* Nothing to read or write, we have streams, but + * the have no data yet ready to be delivered. Slowly + * back off to give others a chance to do their work. + */ + if (wait_micros == 0) { + wait_micros = 10; + } + } + } + + ap_log_cerror( APLOG_MARK, APLOG_DEBUG, status, session->c, + "h2_session(%ld): done", session->id); + + ap_update_child_status_from_conn(session->c->sbh, SERVER_CLOSING, + session->c); + + h2_session_close(session); + h2_session_destroy(session); + + return DONE; +} + + +static void fix_event_conn(conn_rec *c, conn_rec *master); + +/* + * We would like to create the connection more lightweight like + * slave connections in 2.5-DEV. But we get 500 responses on long + * cgi tests in modules/h2.t as the script parsing seems to see an + * EOF from the cgi before anything is sent. + * +conn_rec *h2_conn_create(conn_rec *master, apr_pool_t *pool) +{ + conn_rec *c = (conn_rec *) apr_palloc(pool, sizeof(conn_rec)); + + memcpy(c, master, sizeof(conn_rec)); + c->id = (master->id & (long)pool); + c->slaves = NULL; + c->master = master; + c->input_filters = NULL; + c->output_filters = NULL; + c->pool = pool; + + return c; +} +*/ + +conn_rec *h2_conn_create(conn_rec *master, apr_pool_t *pool) +{ + apr_socket_t *socket; + conn_rec *c; + + AP_DEBUG_ASSERT(master); + + /* CAVEAT: it seems necessary to setup the conn_rec in the master + * connection thread. Other attempts crashed. + * HOWEVER: we setup the connection using the pools and other items + * from the master connection, since we do not want to allocate + * lots of resources here. + * Lets allocated pools and everything else when we actually start + * working on this new connection. + */ + /* Not sure about the scoreboard handle. Reusing the one from the main + * connection could make sense, is not really correct, but we cannot + * easily create new handles for our worker threads either. + * TODO + */ + socket = ap_get_module_config(master->conn_config, &core_module); + c = ap_run_create_connection(pool, master->base_server, + socket, + master->id^((long)pool), + master->sbh, + master->bucket_alloc); + if (c == NULL) { + ap_log_perror(APLOG_MARK, APLOG_ERR, APR_ENOMEM, pool, + APLOGNO(02913) "h2_task: creating conn"); + return NULL; + } + return c; +} + +apr_status_t h2_conn_setup(h2_task_env *env, struct h2_worker *worker) +{ + conn_rec *master = env->mplx->c; + + ap_log_perror(APLOG_MARK, APLOG_TRACE3, 0, env->pool, + "h2_conn(%ld): created from master", master->id); + + /* Ok, we are just about to start processing the connection and + * the worker is calling us to setup all necessary resources. + * We can borrow some from the worker itself and some we do as + * sub-resources from it, so that we get a nice reuse of + * pools. + */ + env->c.pool = env->pool; + env->c.bucket_alloc = h2_worker_get_bucket_alloc(worker); + env->c.current_thread = h2_worker_get_thread(worker); + + env->c.conn_config = ap_create_conn_config(env->pool); + env->c.notes = apr_table_make(env->pool, 5); + + ap_set_module_config(env->c.conn_config, &core_module, + h2_worker_get_socket(worker)); + + /* If we serve http:// requests over a TLS connection, we do + * not want any mod_ssl vars to be visible. + */ + if (ssl_module && (!env->scheme || strcmp("http", env->scheme))) { + /* See #19, there is a range of SSL variables to be gotten from + * the main connection that should be available in request handlers + */ + void *sslcfg = ap_get_module_config(master->conn_config, ssl_module); + if (sslcfg) { + ap_set_module_config(env->c.conn_config, ssl_module, sslcfg); + } + } + + /* This works for mpm_worker so far. Other mpm modules have + * different needs, unfortunately. The most interesting one + * being mpm_event... + */ + switch (h2_conn_mpm_type()) { + case H2_MPM_WORKER: + /* all fine */ + break; + case H2_MPM_EVENT: + fix_event_conn(&env->c, master); + break; + default: + /* fingers crossed */ + break; + } + + /* TODO: we simulate that we had already a request on this connection. + * This keeps the mod_ssl SNI vs. Host name matcher from answering + * 400 Bad Request + * when names do not match. We prefer a predictable 421 status. + */ + env->c.keepalives = 1; + + return APR_SUCCESS; +} + +apr_status_t h2_conn_post(conn_rec *c, h2_worker *worker) +{ + (void)worker; + + /* be sure no one messes with this any more */ + memset(c, 0, sizeof(*c)); + return APR_SUCCESS; +} + +apr_status_t h2_conn_process(conn_rec *c, apr_socket_t *socket) +{ + AP_DEBUG_ASSERT(c); + + c->clogging_input_filters = 1; + ap_process_connection(c, socket); + + return APR_SUCCESS; +} + +/* This is an internal mpm event.c struct which is disguised + * as a conn_state_t so that mpm_event can have special connection + * state information without changing the struct seen on the outside. + * + * For our task connections we need to create a new beast of this type + * and fill it with enough meaningful things that mpm_event reads and + * starts processing out task request. + */ +typedef struct event_conn_state_t event_conn_state_t; +struct event_conn_state_t { + /** APR_RING of expiration timeouts */ + APR_RING_ENTRY(event_conn_state_t) timeout_list; + /** the expiration time of the next keepalive timeout */ + apr_time_t expiration_time; + /** connection record this struct refers to */ + conn_rec *c; + /** request record (if any) this struct refers to */ + request_rec *r; + /** is the current conn_rec suspended? (disassociated with + * a particular MPM thread; for suspend_/resume_connection + * hooks) + */ + int suspended; + /** memory pool to allocate from */ + apr_pool_t *p; + /** bucket allocator */ + apr_bucket_alloc_t *bucket_alloc; + /** poll file descriptor information */ + apr_pollfd_t pfd; + /** public parts of the connection state */ + conn_state_t pub; +}; +APR_RING_HEAD(timeout_head_t, event_conn_state_t); + +static void fix_event_conn(conn_rec *c, conn_rec *master) +{ + event_conn_state_t *master_cs = ap_get_module_config(master->conn_config, + h2_conn_mpm_module()); + event_conn_state_t *cs = apr_pcalloc(c->pool, sizeof(event_conn_state_t)); + cs->bucket_alloc = apr_bucket_alloc_create(c->pool); + + ap_set_module_config(c->conn_config, h2_conn_mpm_module(), cs); + + cs->c = c; + cs->r = NULL; + cs->p = master_cs->p; + cs->pfd = master_cs->pfd; + cs->pub = master_cs->pub; + cs->pub.state = CONN_STATE_READ_REQUEST_LINE; + + c->cs = &(cs->pub); +} + diff --git a/modules/http2/h2_conn.h b/modules/http2/h2_conn.h new file mode 100644 index 00000000000..49a70db8507 --- /dev/null +++ b/modules/http2/h2_conn.h @@ -0,0 +1,60 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_conn__ +#define __mod_h2__h2_conn__ + +struct h2_task; +struct h2_task_env; +struct h2_worker; + +/* Process the connection that is now starting the HTTP/2 + * conversation. Return when the HTTP/2 session is done + * and the connection will close. + */ +apr_status_t h2_conn_main(conn_rec *c); + +/* Process the request that has been upgraded to a HTTP/2 + * conversation. Return when the HTTP/2 session is done + * and the connection will close. + */ +apr_status_t h2_conn_rprocess(request_rec *r); + +/* Initialize this child process for h2 connection work, + * to be called once during child init before multi processing + * starts. + */ +apr_status_t h2_conn_child_init(apr_pool_t *pool, server_rec *s); + + +typedef enum { + H2_MPM_UNKNOWN, + H2_MPM_WORKER, + H2_MPM_EVENT, + H2_MPM_PREFORK, +} h2_mpm_type_t; + +/* Returns the type of MPM module detected */ +h2_mpm_type_t h2_conn_mpm_type(void); + + +conn_rec *h2_conn_create(conn_rec *master, apr_pool_t *stream_pool); + +apr_status_t h2_conn_setup(struct h2_task_env *env, struct h2_worker *worker); +apr_status_t h2_conn_post(conn_rec *c, struct h2_worker *worker); + +apr_status_t h2_conn_process(conn_rec *c, apr_socket_t *socket); + +#endif /* defined(__mod_h2__h2_conn__) */ diff --git a/modules/http2/h2_conn_io.c b/modules/http2/h2_conn_io.c new file mode 100644 index 00000000000..129456ebc3a --- /dev/null +++ b/modules/http2/h2_conn_io.c @@ -0,0 +1,321 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_config.h" +#include "h2_conn_io.h" +#include "h2_h2.h" +#include "h2_util.h" + +#define WRITE_BUFFER_SIZE (64*1024) +#define WRITE_SIZE_INITIAL 1300 +#define WRITE_SIZE_MAX (16*1024) +#define WRITE_SIZE_IDLE_USEC (1*APR_USEC_PER_SEC) +#define WRITE_SIZE_THRESHOLD (1*1024*1024) + +apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c) +{ + io->connection = c; + io->input = apr_brigade_create(c->pool, c->bucket_alloc); + io->output = apr_brigade_create(c->pool, c->bucket_alloc); + io->buflen = 0; + /* That is where we start with, + * see https://issues.apache.org/jira/browse/TS-2503 */ + io->write_size = WRITE_SIZE_INITIAL; + io->last_write = 0; + io->buffer_output = h2_h2_is_tls(c); + + /* Currently we buffer only for TLS output. The reason this gives + * improved performance is that buckets send to the mod_ssl network + * filter will be encrypted in chunks. There is a special filter + * that tries to aggregate data, but that does not work well when + * bucket sizes alternate between tiny frame headers and large data + * chunks. + */ + if (io->buffer_output) { + io->bufsize = WRITE_BUFFER_SIZE; + io->buffer = apr_pcalloc(c->pool, io->bufsize); + } + else { + io->bufsize = 0; + } + + return APR_SUCCESS; +} + +void h2_conn_io_destroy(h2_conn_io *io) +{ + io->input = NULL; + io->output = NULL; +} + +static apr_status_t h2_conn_io_bucket_read(h2_conn_io *io, + apr_read_type_e block, + h2_conn_io_on_read_cb on_read_cb, + void *puser, int *pdone) +{ + apr_status_t status = APR_SUCCESS; + apr_size_t readlen = 0; + *pdone = 0; + + while (status == APR_SUCCESS && !*pdone + && !APR_BRIGADE_EMPTY(io->input)) { + + apr_bucket* bucket = APR_BRIGADE_FIRST(io->input); + if (APR_BUCKET_IS_METADATA(bucket)) { + /* we do nothing regarding any meta here */ + } + else { + const char *bucket_data = NULL; + apr_size_t bucket_length = 0; + status = apr_bucket_read(bucket, &bucket_data, + &bucket_length, block); + + if (status == APR_SUCCESS && bucket_length > 0) { + if (APLOGctrace2(io->connection)) { + char buffer[32]; + h2_util_hex_dump(buffer, sizeof(buffer)/sizeof(buffer[0]), + bucket_data, bucket_length); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection, + "h2_conn_io(%ld): read %d bytes: %s", + io->connection->id, (int)bucket_length, buffer); + } + + if (bucket_length > 0) { + apr_size_t consumed = 0; + status = on_read_cb(bucket_data, bucket_length, + &consumed, pdone, puser); + if (status == APR_SUCCESS && bucket_length > consumed) { + /* We have data left in the bucket. Split it. */ + status = apr_bucket_split(bucket, consumed); + } + readlen += consumed; + } + } + } + apr_bucket_delete(bucket); + } + if (readlen == 0 && status == APR_SUCCESS && block == APR_NONBLOCK_READ) { + return APR_EAGAIN; + } + return status; +} + +apr_status_t h2_conn_io_read(h2_conn_io *io, + apr_read_type_e block, + h2_conn_io_on_read_cb on_read_cb, + void *puser) +{ + apr_status_t status; + int done = 0; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection, + "h2_conn_io: try read, block=%d", block); + + if (!APR_BRIGADE_EMPTY(io->input)) { + /* Seems something is left from a previous read, lets + * satisfy our caller with the data we already have. */ + status = h2_conn_io_bucket_read(io, block, on_read_cb, puser, &done); + if (status != APR_SUCCESS || done) { + return status; + } + apr_brigade_cleanup(io->input); + } + + /* We only do a blocking read when we have no streams to process. So, + * in httpd scoreboard lingo, we are in a KEEPALIVE connection state. + * When reading non-blocking, we do have streams to process and update + * child with NULL request. That way, any current request information + * in the scoreboard is preserved. + */ + if (block == APR_BLOCK_READ) { + ap_update_child_status_from_conn(io->connection->sbh, + SERVER_BUSY_KEEPALIVE, + io->connection); + } + else { + ap_update_child_status(io->connection->sbh, SERVER_BUSY_READ, NULL); + } + + status = ap_get_brigade(io->connection->input_filters, + io->input, AP_MODE_READBYTES, + block, 16 * 4096); + switch (status) { + case APR_SUCCESS: + return h2_conn_io_bucket_read(io, block, on_read_cb, puser, &done); + case APR_EOF: + case APR_EAGAIN: + break; + default: + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, io->connection, + "h2_conn_io: error reading"); + break; + } + return status; +} + +static apr_status_t flush_out(apr_bucket_brigade *bb, void *ctx) +{ + h2_conn_io *io = (h2_conn_io*)ctx; + apr_status_t status; + apr_off_t bblen; + + ap_update_child_status(io->connection->sbh, SERVER_BUSY_WRITE, NULL); + status = apr_brigade_length(bb, 1, &bblen); + if (status == APR_SUCCESS) { + status = ap_pass_brigade(io->connection->output_filters, bb); + if (status == APR_SUCCESS) { + io->bytes_written += (apr_size_t)bblen; + io->last_write = apr_time_now(); + } + apr_brigade_cleanup(bb); + } + return status; +} + +static apr_status_t bucketeer_buffer(h2_conn_io *io) { + const char *data = io->buffer; + apr_size_t remaining = io->buflen; + apr_bucket *b; + int bcount, i; + + if (io->write_size > WRITE_SIZE_INITIAL + && (apr_time_now() - io->last_write) >= WRITE_SIZE_IDLE_USEC) { + /* long time not written, reset write size */ + io->write_size = WRITE_SIZE_INITIAL; + io->bytes_written = 0; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection, + "h2_conn_io(%ld): timeout write size reset to %ld", + (long)io->connection->id, (long)io->write_size); + } + else if (io->write_size < WRITE_SIZE_MAX + && io->bytes_written >= WRITE_SIZE_THRESHOLD) { + /* connection is hot, use max size */ + io->write_size = WRITE_SIZE_MAX; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection, + "h2_conn_io(%ld): threshold reached, write size now %ld", + (long)io->connection->id, (long)io->write_size); + } + + bcount = (int)(remaining / io->write_size); + for (i = 0; i < bcount; ++i) { + b = apr_bucket_transient_create(data, io->write_size, + io->output->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(io->output, b); + data += io->write_size; + remaining -= io->write_size; + } + + if (remaining > 0) { + b = apr_bucket_transient_create(data, remaining, + io->output->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(io->output, b); + } + return APR_SUCCESS; +} + +apr_status_t h2_conn_io_write(h2_conn_io *io, + const char *buf, size_t length) +{ + apr_status_t status = APR_SUCCESS; + io->unflushed = 1; + + if (io->buffer_output) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection, + "h2_conn_io: buffering %ld bytes", (long)length); + while (length > 0 && (status == APR_SUCCESS)) { + apr_size_t avail = io->bufsize - io->buflen; + if (avail <= 0) { + bucketeer_buffer(io); + status = flush_out(io->output, io); + io->buflen = 0; + } + else if (length > avail) { + memcpy(io->buffer + io->buflen, buf, avail); + io->buflen += avail; + length -= avail; + buf += avail; + } + else { + memcpy(io->buffer + io->buflen, buf, length); + io->buflen += length; + length = 0; + break; + } + } + + } + else { + status = apr_brigade_write(io->output, flush_out, io, buf, length); + if (status == APR_SUCCESS + || APR_STATUS_IS_ECONNABORTED(status) + || APR_STATUS_IS_EPIPE(status)) { + /* These are all fine and no reason for concern. Everything else + * is interesting. */ + status = APR_SUCCESS; + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, io->connection, + "h2_conn_io: write error"); + } + } + + return status; +} + + +apr_status_t h2_conn_io_flush(h2_conn_io *io) +{ + if (io->unflushed) { + apr_status_t status; + if (io->buflen > 0) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection, + "h2_conn_io: flush, flushing %ld bytes", (long)io->buflen); + bucketeer_buffer(io); + io->buflen = 0; + } + /* Append flush. + */ + APR_BRIGADE_INSERT_TAIL(io->output, + apr_bucket_flush_create(io->output->bucket_alloc)); + + /* Send it out through installed filters (TLS) to the client */ + status = flush_out(io->output, io); + + if (status == APR_SUCCESS + || APR_STATUS_IS_ECONNABORTED(status) + || APR_STATUS_IS_EPIPE(status)) { + /* These are all fine and no reason for concern. Everything else + * is interesting. */ + io->unflushed = 0; + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, io->connection, + "h2_conn_io: flush error"); + } + + return status; + } + return APR_SUCCESS; +} + diff --git a/modules/http2/h2_conn_io.h b/modules/http2/h2_conn_io.h new file mode 100644 index 00000000000..084445ef2b0 --- /dev/null +++ b/modules/http2/h2_conn_io.h @@ -0,0 +1,58 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_conn_io__ +#define __mod_h2__h2_conn_io__ + +/* h2_io is the basic handler of a httpd connection. It keeps two brigades, + * one for input, one for output and works with the installed connection + * filters. + * The read is done via a callback function, so that input can be processed + * directly without copying. + */ +typedef struct { + conn_rec *connection; + apr_bucket_brigade *input; + apr_bucket_brigade *output; + int buffer_output; + int write_size; + apr_time_t last_write; + apr_size_t bytes_written; + + char *buffer; + apr_size_t buflen; + apr_size_t bufsize; + int unflushed; +} h2_conn_io; + +apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c); +void h2_conn_io_destroy(h2_conn_io *io); + +typedef apr_status_t (*h2_conn_io_on_read_cb)(const char *data, apr_size_t len, + apr_size_t *readlen, int *done, + void *puser); + +apr_status_t h2_conn_io_read(h2_conn_io *io, + apr_read_type_e block, + h2_conn_io_on_read_cb on_read_cb, + void *puser); + +apr_status_t h2_conn_io_write(h2_conn_io *io, + const char *buf, + size_t length); + +apr_status_t h2_conn_io_flush(h2_conn_io *io); + +#endif /* defined(__mod_h2__h2_conn_io__) */ diff --git a/modules/http2/h2_ctx.c b/modules/http2/h2_ctx.c new file mode 100644 index 00000000000..0e198456f23 --- /dev/null +++ b/modules/http2/h2_ctx.c @@ -0,0 +1,84 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include "h2_private.h" +#include "h2_task.h" +#include "h2_ctx.h" +#include "h2_private.h" + +static h2_ctx *h2_ctx_create(const conn_rec *c) +{ + h2_ctx *ctx = apr_pcalloc(c->pool, sizeof(h2_ctx)); + AP_DEBUG_ASSERT(ctx); + ap_set_module_config(c->conn_config, &h2_module, ctx); + return ctx; +} + +h2_ctx *h2_ctx_create_for(const conn_rec *c, h2_task_env *env) +{ + h2_ctx *ctx = h2_ctx_create(c); + if (ctx) { + ctx->task_env = env; + } + return ctx; +} + +h2_ctx *h2_ctx_get(const conn_rec *c) +{ + h2_ctx *ctx = (h2_ctx*)ap_get_module_config(c->conn_config, &h2_module); + if (ctx == NULL) { + ctx = h2_ctx_create(c); + } + return ctx; +} + +h2_ctx *h2_ctx_rget(const request_rec *r) +{ + return h2_ctx_get(r->connection); +} + +const char *h2_ctx_protocol_get(const conn_rec *c) +{ + h2_ctx *ctx = (h2_ctx*)ap_get_module_config(c->conn_config, &h2_module); + return ctx? ctx->protocol : NULL; +} + +h2_ctx *h2_ctx_protocol_set(h2_ctx *ctx, const char *proto) +{ + ctx->protocol = proto; + ctx->is_h2 = (proto != NULL); + return ctx; +} + +int h2_ctx_is_task(h2_ctx *ctx) +{ + return ctx && !!ctx->task_env; +} + +int h2_ctx_is_active(h2_ctx *ctx) +{ + return ctx && ctx->is_h2; +} + +struct h2_task_env *h2_ctx_get_task(h2_ctx *ctx) +{ + return ctx->task_env; +} diff --git a/modules/http2/h2_ctx.h b/modules/http2/h2_ctx.h new file mode 100644 index 00000000000..f9bc8278296 --- /dev/null +++ b/modules/http2/h2_ctx.h @@ -0,0 +1,59 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_ctx__ +#define __mod_h2__h2_ctx__ + +struct h2_task_env; +struct h2_config; + +/** + * The h2 module context associated with a connection. + * + * It keeps track of the different types of connections: + * - those from clients that use HTTP/2 protocol + * - those from clients that do not use HTTP/2 + * - those created by ourself to perform work on HTTP/2 streams + */ +typedef struct h2_ctx { + int is_h2; /* h2 engine is used */ + const char *protocol; /* the protocol negotiated */ + struct h2_task_env *task_env; /* the h2_task environment or NULL */ + const char *hostname; /* hostname negotiated via SNI, optional */ + server_rec *server; /* httpd server config selected. */ + struct h2_config *config; /* effective config in this context */ +} h2_ctx; + +h2_ctx *h2_ctx_get(const conn_rec *c); +h2_ctx *h2_ctx_rget(const request_rec *r); +h2_ctx *h2_ctx_create_for(const conn_rec *c, struct h2_task_env *env); + + +/* Set the h2 protocol established on this connection context or + * NULL when other protocols are in place. + */ +h2_ctx *h2_ctx_protocol_set(h2_ctx *ctx, const char *proto); + +/** + * Get the h2 protocol negotiated for this connection, or NULL. + */ +const char *h2_ctx_protocol_get(const conn_rec *c); + +int h2_ctx_is_task(h2_ctx *ctx); +int h2_ctx_is_active(h2_ctx *ctx); + +struct h2_task_env *h2_ctx_get_task(h2_ctx *ctx); + +#endif /* defined(__mod_h2__h2_ctx__) */ diff --git a/modules/http2/h2_from_h1.c b/modules/http2/h2_from_h1.c new file mode 100644 index 00000000000..be11f5c317e --- /dev/null +++ b/modules/http2/h2_from_h1.c @@ -0,0 +1,570 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_response.h" +#include "h2_from_h1.h" +#include "h2_task.h" +#include "h2_task_output.h" +#include "h2_util.h" + + +static void set_state(h2_from_h1 *from_h1, h2_from_h1_state_t state); + +h2_from_h1 *h2_from_h1_create(int stream_id, apr_pool_t *pool) +{ + h2_from_h1 *from_h1 = apr_pcalloc(pool, sizeof(h2_from_h1)); + if (from_h1) { + from_h1->stream_id = stream_id; + from_h1->pool = pool; + from_h1->state = H2_RESP_ST_STATUS_LINE; + from_h1->hlines = apr_array_make(pool, 10, sizeof(char *)); + } + return from_h1; +} + +apr_status_t h2_from_h1_destroy(h2_from_h1 *from_h1) +{ + if (from_h1->response) { + h2_response_destroy(from_h1->response); + from_h1->response = NULL; + } + from_h1->bb = NULL; + return APR_SUCCESS; +} + +h2_from_h1_state_t h2_from_h1_get_state(h2_from_h1 *from_h1) +{ + return from_h1->state; +} + +static void set_state(h2_from_h1 *from_h1, h2_from_h1_state_t state) +{ + if (from_h1->state != state) { + from_h1->state = state; + } +} + +h2_response *h2_from_h1_get_response(h2_from_h1 *from_h1) +{ + return from_h1->response; +} + +static apr_status_t make_h2_headers(h2_from_h1 *from_h1, request_rec *r) +{ + from_h1->response = h2_response_create(from_h1->stream_id, + from_h1->status, from_h1->hlines, + from_h1->pool); + if (from_h1->response == NULL) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EINVAL, r->connection, + APLOGNO(02915) + "h2_from_h1(%d): unable to create resp_head", + from_h1->stream_id); + return APR_EINVAL; + } + from_h1->content_length = from_h1->response->content_length; + from_h1->chunked = r->chunked; + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection, + "h2_from_h1(%d): converted headers, content-length: %d" + ", chunked=%d", + from_h1->stream_id, (int)from_h1->content_length, + (int)from_h1->chunked); + + set_state(from_h1, ((from_h1->chunked || from_h1->content_length > 0)? + H2_RESP_ST_BODY : H2_RESP_ST_DONE)); + /* We are ready to be sent to the client */ + return APR_SUCCESS; +} + +static apr_status_t parse_header(h2_from_h1 *from_h1, ap_filter_t* f, + char *line) { + (void)f; + + if (line[0] == ' ' || line[0] == '\t') { + char **plast; + /* continuation line from the header before this */ + while (line[0] == ' ' || line[0] == '\t') { + ++line; + } + + plast = apr_array_pop(from_h1->hlines); + if (plast == NULL) { + /* not well formed */ + return APR_EINVAL; + } + APR_ARRAY_PUSH(from_h1->hlines, const char*) = apr_psprintf(from_h1->pool, "%s %s", *plast, line); + } + else { + /* new header line */ + APR_ARRAY_PUSH(from_h1->hlines, const char*) = apr_pstrdup(from_h1->pool, line); + } + return APR_SUCCESS; +} + +static apr_status_t get_line(h2_from_h1 *from_h1, apr_bucket_brigade *bb, + ap_filter_t* f, char *line, apr_size_t len) +{ + apr_status_t status; + if (!from_h1->bb) { + from_h1->bb = apr_brigade_create(from_h1->pool, f->c->bucket_alloc); + } + else { + apr_brigade_cleanup(from_h1->bb); + } + status = apr_brigade_split_line(from_h1->bb, bb, + APR_BLOCK_READ, + HUGE_STRING_LEN); + if (status == APR_SUCCESS) { + --len; + status = apr_brigade_flatten(from_h1->bb, line, &len); + if (status == APR_SUCCESS) { + /* we assume a non-0 containing line and remove + * trailing crlf. */ + line[len] = '\0'; + if (len >= 2 && !strcmp(H2_CRLF, line + len - 2)) { + len -= 2; + line[len] = '\0'; + } + + apr_brigade_cleanup(from_h1->bb); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_from_h1(%d): read line: %s", + from_h1->stream_id, line); + } + } + return status; +} + +apr_status_t h2_from_h1_read_response(h2_from_h1 *from_h1, ap_filter_t* f, + apr_bucket_brigade* bb) +{ + apr_status_t status = APR_SUCCESS; + char line[HUGE_STRING_LEN]; + + if ((from_h1->state == H2_RESP_ST_BODY) + || (from_h1->state == H2_RESP_ST_DONE)) { + if (from_h1->chunked) { + /* The httpd core HTTP_HEADER filter has or will install the + * "CHUNK" output transcode filter, which appears further down + * the filter chain. We do not want it for HTTP/2. + * Once we successfully deinstalled it, this filter has no + * further function and we remove it. + */ + status = ap_remove_output_filter_byhandle(f->r->output_filters, + "CHUNK"); + if (status == APR_SUCCESS) { + ap_remove_output_filter(f); + } + } + + return ap_pass_brigade(f->next, bb); + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_from_h1(%d): read_response", from_h1->stream_id); + + while (!APR_BRIGADE_EMPTY(bb) && status == APR_SUCCESS) { + + switch (from_h1->state) { + + case H2_RESP_ST_STATUS_LINE: + case H2_RESP_ST_HEADERS: + status = get_line(from_h1, bb, f, line, sizeof(line)); + if (status != APR_SUCCESS) { + return status; + } + if (from_h1->state == H2_RESP_ST_STATUS_LINE) { + /* instead of parsing, just take it directly */ + from_h1->status = apr_psprintf(from_h1->pool, + "%d", f->r->status); + from_h1->state = H2_RESP_ST_HEADERS; + } + else if (line[0] == '\0') { + /* end of headers, create the h2_response and + * pass the rest of the brigade down the filter + * chain. + */ + status = make_h2_headers(from_h1, f->r); + if (from_h1->bb) { + apr_brigade_destroy(from_h1->bb); + from_h1->bb = NULL; + } + if (!APR_BRIGADE_EMPTY(bb)) { + return ap_pass_brigade(f->next, bb); + } + } + else { + status = parse_header(from_h1, f, line); + } + break; + + default: + return ap_pass_brigade(f->next, bb); + } + + } + + return status; +} + +/* This routine is called by apr_table_do and merges all instances of + * the passed field values into a single array that will be further + * processed by some later routine. Originally intended to help split + * and recombine multiple Vary fields, though it is generic to any field + * consisting of comma/space-separated tokens. + */ +static int uniq_field_values(void *d, const char *key, const char *val) +{ + apr_array_header_t *values; + char *start; + char *e; + char **strpp; + int i; + + (void)key; + values = (apr_array_header_t *)d; + + e = apr_pstrdup(values->pool, val); + + do { + /* Find a non-empty fieldname */ + + while (*e == ',' || apr_isspace(*e)) { + ++e; + } + if (*e == '\0') { + break; + } + start = e; + while (*e != '\0' && *e != ',' && !apr_isspace(*e)) { + ++e; + } + if (*e != '\0') { + *e++ = '\0'; + } + + /* Now add it to values if it isn't already represented. + * Could be replaced by a ap_array_strcasecmp() if we had one. + */ + for (i = 0, strpp = (char **) values->elts; i < values->nelts; + ++i, ++strpp) { + if (*strpp && strcasecmp(*strpp, start) == 0) { + break; + } + } + if (i == values->nelts) { /* if not found */ + *(char **)apr_array_push(values) = start; + } + } while (*e != '\0'); + + return 1; +} + +/* + * Since some clients choke violently on multiple Vary fields, or + * Vary fields with duplicate tokens, combine any multiples and remove + * any duplicates. + */ +static void fix_vary(request_rec *r) +{ + apr_array_header_t *varies; + + varies = apr_array_make(r->pool, 5, sizeof(char *)); + + /* Extract all Vary fields from the headers_out, separate each into + * its comma-separated fieldname values, and then add them to varies + * if not already present in the array. + */ + apr_table_do((int (*)(void *, const char *, const char *))uniq_field_values, + (void *) varies, r->headers_out, "Vary", NULL); + + /* If we found any, replace old Vary fields with unique-ified value */ + + if (varies->nelts > 0) { + apr_table_setn(r->headers_out, "Vary", + apr_array_pstrcat(r->pool, varies, ',')); + } +} + +static void set_basic_http_header(request_rec *r, apr_table_t *headers) +{ + char *date = NULL; + const char *proxy_date = NULL; + const char *server = NULL; + const char *us = ap_get_server_banner(); + + /* + * keep the set-by-proxy server and date headers, otherwise + * generate a new server header / date header + */ + if (r->proxyreq != PROXYREQ_NONE) { + proxy_date = apr_table_get(r->headers_out, "Date"); + if (!proxy_date) { + /* + * proxy_date needs to be const. So use date for the creation of + * our own Date header and pass it over to proxy_date later to + * avoid a compiler warning. + */ + date = apr_palloc(r->pool, APR_RFC822_DATE_LEN); + ap_recent_rfc822_date(date, r->request_time); + } + server = apr_table_get(r->headers_out, "Server"); + } + else { + date = apr_palloc(r->pool, APR_RFC822_DATE_LEN); + ap_recent_rfc822_date(date, r->request_time); + } + + apr_table_setn(headers, "Date", proxy_date ? proxy_date : date ); + apr_table_unset(r->headers_out, "Date"); + + if (!server && *us) { + server = us; + } + if (server) { + apr_table_setn(headers, "Server", server); + apr_table_unset(r->headers_out, "Server"); + } +} + +static int copy_header(void *ctx, const char *name, const char *value) +{ + apr_table_t *headers = ctx; + + apr_table_addn(headers, name, value); + return 1; +} + +static h2_response *create_response(h2_from_h1 *from_h1, request_rec *r) +{ + const char *clheader; + const char *ctype; + apr_table_t *headers; + /* + * Now that we are ready to send a response, we need to combine the two + * header field tables into a single table. If we don't do this, our + * later attempts to set or unset a given fieldname might be bypassed. + */ + if (!apr_is_empty_table(r->err_headers_out)) { + r->headers_out = apr_table_overlay(r->pool, r->err_headers_out, + r->headers_out); + } + + /* + * Remove the 'Vary' header field if the client can't handle it. + * Since this will have nasty effects on HTTP/1.1 caches, force + * the response into HTTP/1.0 mode. + */ + if (apr_table_get(r->subprocess_env, "force-no-vary") != NULL) { + apr_table_unset(r->headers_out, "Vary"); + r->proto_num = HTTP_VERSION(1,0); + apr_table_setn(r->subprocess_env, "force-response-1.0", "1"); + } + else { + fix_vary(r); + } + + /* + * Now remove any ETag response header field if earlier processing + * says so (such as a 'FileETag None' directive). + */ + if (apr_table_get(r->notes, "no-etag") != NULL) { + apr_table_unset(r->headers_out, "ETag"); + } + + /* determine the protocol and whether we should use keepalives. */ + ap_set_keepalive(r); + + if (r->chunked) { + apr_table_unset(r->headers_out, "Content-Length"); + } + + ctype = ap_make_content_type(r, r->content_type); + if (ctype) { + apr_table_setn(r->headers_out, "Content-Type", ctype); + } + + if (r->content_encoding) { + apr_table_setn(r->headers_out, "Content-Encoding", + r->content_encoding); + } + + if (!apr_is_empty_array(r->content_languages)) { + int i; + char *token; + char **languages = (char **)(r->content_languages->elts); + const char *field = apr_table_get(r->headers_out, "Content-Language"); + + while (field && (token = ap_get_list_item(r->pool, &field)) != NULL) { + for (i = 0; i < r->content_languages->nelts; ++i) { + if (!strcasecmp(token, languages[i])) + break; + } + if (i == r->content_languages->nelts) { + *((char **) apr_array_push(r->content_languages)) = token; + } + } + + field = apr_array_pstrcat(r->pool, r->content_languages, ','); + apr_table_setn(r->headers_out, "Content-Language", field); + } + + /* + * Control cachability for non-cachable responses if not already set by + * some other part of the server configuration. + */ + if (r->no_cache && !apr_table_get(r->headers_out, "Expires")) { + char *date = apr_palloc(r->pool, APR_RFC822_DATE_LEN); + ap_recent_rfc822_date(date, r->request_time); + apr_table_addn(r->headers_out, "Expires", date); + } + + /* This is a hack, but I can't find anyway around it. The idea is that + * we don't want to send out 0 Content-Lengths if it is a head request. + * This happens when modules try to outsmart the server, and return + * if they see a HEAD request. Apache 1.3 handlers were supposed to + * just return in that situation, and the core handled the HEAD. In + * 2.0, if a handler returns, then the core sends an EOS bucket down + * the filter stack, and the content-length filter computes a C-L of + * zero and that gets put in the headers, and we end up sending a + * zero C-L to the client. We can't just remove the C-L filter, + * because well behaved 2.0 handlers will send their data down the stack, + * and we will compute a real C-L for the head request. RBB + */ + if (r->header_only + && (clheader = apr_table_get(r->headers_out, "Content-Length")) + && !strcmp(clheader, "0")) { + apr_table_unset(r->headers_out, "Content-Length"); + } + + headers = apr_table_make(r->pool, 10); + + set_basic_http_header(r, headers); + if (r->status == HTTP_NOT_MODIFIED) { + apr_table_do((int (*)(void *, const char *, const char *)) copy_header, + (void *) headers, r->headers_out, + "ETag", + "Content-Location", + "Expires", + "Cache-Control", + "Vary", + "Warning", + "WWW-Authenticate", + "Proxy-Authenticate", + "Set-Cookie", + "Set-Cookie2", + NULL); + } + else { + apr_table_do((int (*)(void *, const char *, const char *)) copy_header, + (void *) headers, r->headers_out, NULL); + } + + return h2_response_rcreate(from_h1->stream_id, r, headers, r->pool); +} + +apr_status_t h2_response_output_filter(ap_filter_t *f, apr_bucket_brigade *bb) +{ + h2_task_env *env = f->ctx; + h2_from_h1 *from_h1 = env->output? env->output->from_h1 : NULL; + request_rec *r = f->r; + apr_bucket *b; + ap_bucket_error *eb = NULL; + + AP_DEBUG_ASSERT(from_h1 != NULL); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_from_h1(%d): output_filter called", from_h1->stream_id); + + if (r->header_only && env->output && from_h1->response) { + /* throw away any data after we have compiled the response */ + apr_brigade_cleanup(bb); + return OK; + } + + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) + { + if (AP_BUCKET_IS_ERROR(b) && !eb) { + eb = b->data; + continue; + } + /* + * If we see an EOC bucket it is a signal that we should get out + * of the way doing nothing. + */ + if (AP_BUCKET_IS_EOC(b)) { + ap_remove_output_filter(f); + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, f->c, + "h2_from_h1(%d): eoc bucket passed", + from_h1->stream_id); + return ap_pass_brigade(f->next, bb); + } + } + + if (eb) { + int st = eb->status; + apr_brigade_cleanup(bb); + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, f->c, + "h2_from_h1(%d): err bucket status=%d", + from_h1->stream_id, st); + ap_die(st, r); + return AP_FILTER_ERROR; + } + + from_h1->response = create_response(from_h1, r); + if (from_h1->response == NULL) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, f->c, + "h2_from_h1(%d): unable to create response", + from_h1->stream_id); + return APR_ENOMEM; + } + + if (r->header_only) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_from_h1(%d): header_only, cleanup output brigade", + from_h1->stream_id); + apr_brigade_cleanup(bb); + return OK; + } + + r->sent_bodyct = 1; /* Whatever follows is real body stuff... */ + + ap_remove_output_filter(f); + if (APLOGctrace1(f->c)) { + apr_off_t len = 0; + apr_brigade_length(bb, 0, &len); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_from_h1(%d): removed header filter, passing brigade " + "len=%ld", from_h1->stream_id, (long)len); + } + return ap_pass_brigade(f->next, bb); +} diff --git a/modules/http2/h2_from_h1.h b/modules/http2/h2_from_h1.h new file mode 100644 index 00000000000..115a3142668 --- /dev/null +++ b/modules/http2/h2_from_h1.h @@ -0,0 +1,82 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_from_h1__ +#define __mod_h2__h2_from_h1__ + +/** + * h2_from_h1 parses a HTTP/1.1 response into + * - response status + * - a list of header values + * - a series of bytes that represent the response body alone, without + * any meta data, such as inserted by chunked transfer encoding. + * + * All data is allocated from the stream memory pool. + * + * Again, see comments in h2_request: ideally we would take the headers + * and status from the httpd structures instead of parsing them here, but + * we need to have all handlers and filters involved in request/response + * processing, so this seems to be the way for now. + */ + +typedef enum { + H2_RESP_ST_STATUS_LINE, /* parsing http/1 status line */ + H2_RESP_ST_HEADERS, /* parsing http/1 response headers */ + H2_RESP_ST_BODY, /* transferring response body */ + H2_RESP_ST_DONE /* complete response converted */ +} h2_from_h1_state_t; + +struct h2_response; + +typedef struct h2_from_h1 h2_from_h1; + +struct h2_from_h1 { + int stream_id; + h2_from_h1_state_t state; + apr_pool_t *pool; + apr_bucket_brigade *bb; + + apr_size_t content_length; + int chunked; + + const char *status; + apr_array_header_t *hlines; + + struct h2_response *response; +}; + + +typedef void h2_from_h1_state_change_cb(struct h2_from_h1 *resp, + h2_from_h1_state_t prevstate, + void *cb_ctx); + +h2_from_h1 *h2_from_h1_create(int stream_id, apr_pool_t *pool); + +apr_status_t h2_from_h1_destroy(h2_from_h1 *response); + +void h2_from_h1_set_state_change_cb(h2_from_h1 *from_h1, + h2_from_h1_state_change_cb *callback, + void *cb_ctx); + +apr_status_t h2_from_h1_read_response(h2_from_h1 *from_h1, + ap_filter_t* f, apr_bucket_brigade* bb); + +struct h2_response *h2_from_h1_get_response(h2_from_h1 *from_h1); + +h2_from_h1_state_t h2_from_h1_get_state(h2_from_h1 *from_h1); + +apr_status_t h2_response_output_filter(ap_filter_t *f, apr_bucket_brigade *bb); + +#endif /* defined(__mod_h2__h2_from_h1__) */ diff --git a/modules/http2/h2_h2.c b/modules/http2/h2_h2.c new file mode 100644 index 00000000000..221f5118df9 --- /dev/null +++ b/modules/http2/h2_h2.c @@ -0,0 +1,232 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "h2_private.h" + +#include "h2_stream.h" +#include "h2_task.h" +#include "h2_config.h" +#include "h2_ctx.h" +#include "h2_conn.h" +#include "h2_h2.h" + +const char *h2_tls_protos[] = { + "h2", NULL +}; + +const char *h2_clear_protos[] = { + "h2c", NULL +}; + +const char *H2_MAGIC_TOKEN = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; + +/******************************************************************************* + * The optional mod_ssl functions we need. + */ +APR_DECLARE_OPTIONAL_FN(int, ssl_engine_disable, (conn_rec*)); +APR_DECLARE_OPTIONAL_FN(int, ssl_is_https, (conn_rec*)); + +static int (*opt_ssl_engine_disable)(conn_rec*); +static int (*opt_ssl_is_https)(conn_rec*); +/******************************************************************************* + * Hooks for processing incoming connections: + * - pre_conn_before_tls switches SSL off for stream connections + * - process_conn take over connection in case of h2 + */ +static int h2_h2_process_conn(conn_rec* c); +static int h2_h2_remove_timeout(conn_rec* c); +static int h2_h2_post_read_req(request_rec *r); + + +/******************************************************************************* + * Once per lifetime init, retrieve optional functions + */ +apr_status_t h2_h2_init(apr_pool_t *pool, server_rec *s) +{ + (void)pool; + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, "h2_h2, child_init"); + opt_ssl_engine_disable = APR_RETRIEVE_OPTIONAL_FN(ssl_engine_disable); + opt_ssl_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https); + + if (!opt_ssl_is_https) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, + APLOGNO(02951) "mod_ssl does not seem to be enabled"); + } + + return APR_SUCCESS; +} + +int h2_h2_is_tls(conn_rec *c) +{ + return opt_ssl_is_https && opt_ssl_is_https(c); +} + +int h2_tls_disable(conn_rec *c) +{ + if (opt_ssl_engine_disable) { + return opt_ssl_engine_disable(c); + } + return 0; +} + +/******************************************************************************* + * Register various hooks + */ +static const char *const mod_reqtimeout[] = { "reqtimeout.c", NULL}; + +void h2_h2_register_hooks(void) +{ + /* When the connection processing actually starts, we might to + * take over, if h2* was selected as protocol. + */ + ap_hook_process_connection(h2_h2_process_conn, + NULL, NULL, APR_HOOK_FIRST); + /* Perform connection cleanup before the actual processing happens. + */ + ap_hook_process_connection(h2_h2_remove_timeout, + mod_reqtimeout, NULL, APR_HOOK_LAST); + + /* With "H2SerializeHeaders On", we install the filter in this hook + * that parses the response. This needs to happen before any other post + * read function terminates the request with an error. Otherwise we will + * never see the response. + */ + ap_hook_post_read_request(h2_h2_post_read_req, NULL, NULL, APR_HOOK_REALLY_FIRST); +} + +int h2_h2_remove_timeout(conn_rec* c) +{ + h2_ctx *ctx = h2_ctx_get(c); + + if (h2_ctx_is_active(ctx) && !h2_ctx_is_task(ctx)) { + /* cleanup on master h2 connections */ + ap_remove_input_filter_byhandle(c->input_filters, "reqtimeout"); + } + + return DECLINED; +} + +int h2_h2_process_conn(conn_rec* c) +{ + h2_ctx *ctx = h2_ctx_get(c); + h2_config *cfg = h2_config_get(c); + apr_bucket_brigade* temp; + int is_tls = h2_h2_is_tls(c); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn"); + if (h2_ctx_is_task(ctx)) { + /* our stream pseudo connection */ + return DECLINED; + } + + /* If we have not already switched to a h2* protocol and the connection + * is on "http/1.1" + * -> sniff for the magic PRIamble. On TLS, this might trigger the ALPN. + */ + if (!h2_ctx_protocol_get(c) + && !strcmp(AP_PROTOCOL_HTTP1, ap_get_protocol(c))) { + apr_status_t status; + + temp = apr_brigade_create(c->pool, c->bucket_alloc); + status = ap_get_brigade(c->input_filters, temp, + AP_MODE_SPECULATIVE, APR_BLOCK_READ, 24); + + if (status == APR_SUCCESS) { + if (h2_ctx_protocol_get(c) + || strcmp(AP_PROTOCOL_HTTP1, ap_get_protocol(c))) { + /* h2 or another protocol has been selected. */ + } + else { + /* ALPN might have been triggered, but we're still on + * http/1.1. Check the actual bytes read for the H2 Magic + * Token, *if* H2Direct mode is enabled here. + */ + int direct_mode = h2_config_geti(cfg, H2_CONF_DIRECT); + if (direct_mode > 0 || (direct_mode < 0 && !is_tls)) { + char *s = NULL; + apr_size_t slen; + + apr_brigade_pflatten(temp, &s, &slen, c->pool); + if ((slen >= 24) && !memcmp(H2_MAGIC_TOKEN, s, 24)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_h2, direct mode detected"); + h2_ctx_protocol_set(ctx, is_tls? "h2" : "h2c"); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "h2_h2, not detected in %d bytes: %s", + (int)slen, s); + } + } + } + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, + "h2_h2, error reading 24 bytes speculative"); + } + apr_brigade_destroy(temp); + } + + /* If "h2" was selected as protocol (by whatever mechanism), take over + * the connection. + */ + if (h2_ctx_is_active(ctx)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_h2, connection, h2 active"); + + return h2_conn_main(c); + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, declined"); + return DECLINED; +} + +static int h2_h2_post_read_req(request_rec *r) +{ + h2_ctx *ctx = h2_ctx_rget(r); + struct h2_task_env *env = h2_ctx_get_task(ctx); + if (env) { + /* h2_task connection for a stream, not for h2c */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "adding h1_to_h2_resp output filter"); + if (env->serialize_headers) { + ap_remove_output_filter_byhandle(r->output_filters, "H1_TO_H2_RESP"); + ap_add_output_filter("H1_TO_H2_RESP", env, r, r->connection); + } + else { + /* replace the core http filter that formats response headers + * in HTTP/1 with our own that collects status and headers */ + ap_remove_output_filter_byhandle(r->output_filters, "HTTP_HEADER"); + ap_remove_output_filter_byhandle(r->output_filters, "H2_RESPONSE"); + ap_add_output_filter("H2_RESPONSE", env, r, r->connection); + } + } + return DECLINED; +} + + diff --git a/modules/http2/h2_h2.h b/modules/http2/h2_h2.h new file mode 100644 index 00000000000..9a1184d8b69 --- /dev/null +++ b/modules/http2/h2_h2.h @@ -0,0 +1,57 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_h2__ +#define __mod_h2__h2_h2__ + +/** + * List of ALPN protocol identifiers that we suport in cleartext + * negotiations. NULL terminated. + */ +extern const char *h2_clear_protos[]; + +/** + * List of ALPN protocol identifiers that we support in TLS encrypted + * negotiations. NULL terminated. + */ +extern const char *h2_tls_protos[]; + +/** + * The magic PRIamble of RFC 7540 that is always sent when starting + * a h2 communication. + */ +extern const char *H2_MAGIC_TOKEN; + +/* + * One time, post config intialization. + */ +apr_status_t h2_h2_init(apr_pool_t *pool, server_rec *s); + +/* Is the connection a TLS connection? + */ +int h2_h2_is_tls(conn_rec *c); + +/* Disable SSL for this connection, can only be invoked in a pre- + * connection hook before mod_ssl. + * @return != 0 iff disable worked + */ +int h2_tls_disable(conn_rec *c); + +/* Register apache hooks for h2 protocol + */ +void h2_h2_register_hooks(void); + + +#endif /* defined(__mod_h2__h2_h2__) */ diff --git a/modules/http2/h2_io.c b/modules/http2/h2_io.c new file mode 100644 index 00000000000..24d2a9feecd --- /dev/null +++ b/modules/http2/h2_io.c @@ -0,0 +1,165 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_io.h" +#include "h2_response.h" +#include "h2_util.h" + +h2_io *h2_io_create(int id, apr_pool_t *pool, apr_bucket_alloc_t *bucket_alloc) +{ + h2_io *io = apr_pcalloc(pool, sizeof(*io)); + if (io) { + io->id = id; + io->pool = pool; + io->bbin = NULL; + io->bbout = apr_brigade_create(pool, bucket_alloc); + io->response = apr_pcalloc(pool, sizeof(h2_response)); + } + return io; +} + +static void h2_io_cleanup(h2_io *io) +{ + (void)io; +} + +void h2_io_destroy(h2_io *io) +{ + h2_io_cleanup(io); +} + +int h2_io_in_has_eos_for(h2_io *io) +{ + return io->eos_in || (io->bbin && h2_util_has_eos(io->bbin, 0)); +} + +int h2_io_out_has_data(h2_io *io) +{ + return h2_util_bb_has_data_or_eos(io->bbout); +} + +apr_size_t h2_io_out_length(h2_io *io) +{ + if (io->bbout) { + apr_off_t len = 0; + apr_brigade_length(io->bbout, 0, &len); + return (len > 0)? len : 0; + } + return 0; +} + +apr_status_t h2_io_in_read(h2_io *io, apr_bucket_brigade *bb, + apr_size_t maxlen) +{ + apr_off_t start_len = 0; + apr_bucket *last; + apr_status_t status; + + if (!io->bbin || APR_BRIGADE_EMPTY(io->bbin)) { + return io->eos_in? APR_EOF : APR_EAGAIN; + } + + apr_brigade_length(bb, 1, &start_len); + last = APR_BRIGADE_LAST(bb); + status = h2_util_move(bb, io->bbin, maxlen, 0, + "h2_io_in_read"); + if (status == APR_SUCCESS) { + apr_bucket *nlast = APR_BRIGADE_LAST(bb); + apr_off_t end_len = 0; + apr_brigade_length(bb, 1, &end_len); + if (last == nlast) { + return APR_EAGAIN; + } + io->input_consumed += (end_len - start_len); + } + return status; +} + +apr_status_t h2_io_in_write(h2_io *io, apr_bucket_brigade *bb) +{ + if (io->eos_in) { + return APR_EOF; + } + io->eos_in = h2_util_has_eos(bb, 0); + if (!APR_BRIGADE_EMPTY(bb)) { + if (!io->bbin) { + io->bbin = apr_brigade_create(io->bbout->p, + io->bbout->bucket_alloc); + } + return h2_util_move(io->bbin, bb, 0, 0, "h2_io_in_write"); + } + return APR_SUCCESS; +} + +apr_status_t h2_io_in_close(h2_io *io) +{ + if (io->bbin) { + APR_BRIGADE_INSERT_TAIL(io->bbin, + apr_bucket_eos_create(io->bbin->bucket_alloc)); + } + io->eos_in = 1; + return APR_SUCCESS; +} + +apr_status_t h2_io_out_readx(h2_io *io, + h2_io_data_cb *cb, void *ctx, + apr_size_t *plen, int *peos) +{ + if (cb == NULL) { + /* just checking length available */ + return h2_util_bb_avail(io->bbout, plen, peos); + } + return h2_util_bb_readx(io->bbout, cb, ctx, plen, peos); +} + +apr_status_t h2_io_out_write(h2_io *io, apr_bucket_brigade *bb, + apr_size_t maxlen, int *pfile_handles_allowed) +{ + /* Let's move the buckets from the request processing in here, so + * that the main thread can read them when it has time/capacity. + * + * Move at most "maxlen" memory bytes. If buckets remain, it is + * the caller's responsibility to take care of this. + * + * We allow passing of file buckets as long as we do not have too + * many open files already buffered. Otherwise we will run out of + * file handles. + */ + int start_allowed = *pfile_handles_allowed; + apr_status_t status; + status = h2_util_move(io->bbout, bb, maxlen, pfile_handles_allowed, + "h2_io_out_write"); + /* track # file buckets moved into our pool */ + if (start_allowed != *pfile_handles_allowed) { + io->files_handles_owned += (start_allowed - *pfile_handles_allowed); + } + return status; +} + + +apr_status_t h2_io_out_close(h2_io *io) +{ + APR_BRIGADE_INSERT_TAIL(io->bbout, + apr_bucket_eos_create(io->bbout->bucket_alloc)); + return APR_SUCCESS; +} diff --git a/modules/http2/h2_io.h b/modules/http2/h2_io.h new file mode 100644 index 00000000000..946ee44334e --- /dev/null +++ b/modules/http2/h2_io.h @@ -0,0 +1,126 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_io__ +#define __mod_h2__h2_io__ + +struct h2_response; +struct apr_thread_cond_t; +struct h2_task; + + +typedef apr_status_t h2_io_data_cb(void *ctx, + const char *data, apr_size_t len); + + +typedef struct h2_io h2_io; + +struct h2_io { + int id; /* stream identifier */ + apr_pool_t *pool; /* stream pool */ + apr_bucket_brigade *bbin; /* input data for stream */ + int eos_in; + int task_done; + + apr_size_t input_consumed; /* how many bytes have been read */ + struct apr_thread_cond_t *input_arrived; /* block on reading */ + + apr_bucket_brigade *bbout; /* output data from stream */ + struct apr_thread_cond_t *output_drained; /* block on writing */ + + struct h2_response *response;/* submittable response created */ + int files_handles_owned; +}; + +/******************************************************************************* + * Object lifecycle and information. + ******************************************************************************/ + +/** + * Creates a new h2_io for the given stream id. + */ +h2_io *h2_io_create(int id, apr_pool_t *pool, apr_bucket_alloc_t *bucket_alloc); + +/** + * Frees any resources hold by the h2_io instance. + */ +void h2_io_destroy(h2_io *io); + +/** + * The input data is completely queued. Blocked reads will return immediately + * and give either data or EOF. + */ +int h2_io_in_has_eos_for(h2_io *io); +/** + * Output data is available. + */ +int h2_io_out_has_data(h2_io *io); + +/******************************************************************************* + * Input handling of streams. + ******************************************************************************/ +/** + * Reads the next bucket from the input. Returns APR_EAGAIN if none + * is currently available, APR_EOF if end of input has been reached. + */ +apr_status_t h2_io_in_read(h2_io *io, apr_bucket_brigade *bb, + apr_size_t maxlen); + +/** + * Appends given bucket to the input. + */ +apr_status_t h2_io_in_write(h2_io *io, apr_bucket_brigade *bb); + +/** + * Closes the input. After existing data has been read, APR_EOF will + * be returned. + */ +apr_status_t h2_io_in_close(h2_io *io); + +/******************************************************************************* + * Output handling of streams. + ******************************************************************************/ + +/** + * Read a bucket from the output head. Return APR_EAGAIN if non is available, + * APR_EOF if none available and output has been closed. + * May be called with buffer == NULL in order to find out how much data + * is available. + * @param io the h2_io to read output from + * @param buffer the buffer to copy the data to, may be NULL + * @param plen the requested max len, set to amount of data on return + * @param peos != 0 iff the end of stream has been reached + */ +apr_status_t h2_io_out_readx(h2_io *io, + h2_io_data_cb *cb, void *ctx, + apr_size_t *plen, int *peos); + +apr_status_t h2_io_out_write(h2_io *io, apr_bucket_brigade *bb, + apr_size_t maxlen, int *pfile_buckets_allowed); + +/** + * Closes the input. After existing data has been read, APR_EOF will + * be returned. + */ +apr_status_t h2_io_out_close(h2_io *io); + +/** + * Gives the overall length of the data that is currently queued for + * output. + */ +apr_size_t h2_io_out_length(h2_io *io); + + +#endif /* defined(__mod_h2__h2_io__) */ diff --git a/modules/http2/h2_io_set.c b/modules/http2/h2_io_set.c new file mode 100644 index 00000000000..91afde8f1f5 --- /dev/null +++ b/modules/http2/h2_io_set.c @@ -0,0 +1,180 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_io.h" +#include "h2_io_set.h" + +#define h2_io_IDX(list, i) ((h2_io**)(list)->elts)[i] + +struct h2_io_set { + apr_array_header_t *list; +}; + +h2_io_set *h2_io_set_create(apr_pool_t *pool) +{ + h2_io_set *sp = apr_pcalloc(pool, sizeof(h2_io_set)); + if (sp) { + sp->list = apr_array_make(pool, 100, sizeof(h2_io*)); + if (!sp->list) { + return NULL; + } + } + return sp; +} + +void h2_io_set_destroy(h2_io_set *sp) +{ + int i; + for (i = 0; i < sp->list->nelts; ++i) { + h2_io *io = h2_io_IDX(sp->list, i); + h2_io_destroy(io); + } + sp->list->nelts = 0; +} + +static int h2_stream_id_cmp(const void *s1, const void *s2) +{ + h2_io **pio1 = (h2_io **)s1; + h2_io **pio2 = (h2_io **)s2; + return (*pio1)->id - (*pio2)->id; +} + +h2_io *h2_io_set_get(h2_io_set *sp, int stream_id) +{ + /* we keep the array sorted by id, so lookup can be done + * by bsearch. + */ + h2_io **ps; + h2_io key; + h2_io *pkey = &key; + + memset(&key, 0, sizeof(key)); + key.id = stream_id; + ps = bsearch(&pkey, sp->list->elts, sp->list->nelts, + sp->list->elt_size, h2_stream_id_cmp); + return ps? *ps : NULL; +} + +h2_io *h2_io_set_get_highest_prio(h2_io_set *set) +{ + h2_io *highest = NULL; + int i; + for (i = 0; i < set->list->nelts; ++i) { + h2_io *io = h2_io_IDX(set->list, i); + if (!highest /*|| io-prio even higher */ ) { + highest = io; + } + } + return highest; +} + +static void h2_io_set_sort(h2_io_set *sp) +{ + qsort(sp->list->elts, sp->list->nelts, sp->list->elt_size, + h2_stream_id_cmp); +} + +apr_status_t h2_io_set_add(h2_io_set *sp, h2_io *io) +{ + h2_io *existing = h2_io_set_get(sp, io->id); + if (!existing) { + int last; + APR_ARRAY_PUSH(sp->list, h2_io*) = io; + /* Normally, streams get added in ascending order if id. We + * keep the array sorted, so we just need to check of the newly + * appended stream has a lower id than the last one. if not, + * sorting is not necessary. + */ + last = sp->list->nelts - 1; + if (last > 0 + && (h2_io_IDX(sp->list, last)->id + < h2_io_IDX(sp->list, last-1)->id)) { + h2_io_set_sort(sp); + } + } + return APR_SUCCESS; +} + +h2_io *h2_io_set_remove(h2_io_set *sp, h2_io *io) +{ + int i; + for (i = 0; i < sp->list->nelts; ++i) { + h2_io *e = h2_io_IDX(sp->list, i); + if (e == io) { + int n; + --sp->list->nelts; + n = sp->list->nelts - i; + if (n > 0) { + /* Close the hole in the array by moving the upper + * parts down one step. + */ + h2_io **selts = (h2_io**)sp->list->elts; + memmove(selts+i, selts+i+1, n * sizeof(h2_io*)); + } + return e; + } + } + return NULL; +} + +void h2_io_set_destroy_all(h2_io_set *sp) +{ + int i; + for (i = 0; i < sp->list->nelts; ++i) { + h2_io *io = h2_io_IDX(sp->list, i); + h2_io_destroy(io); + } + sp->list->nelts = 0; +} + +void h2_io_set_remove_all(h2_io_set *sp) +{ + sp->list->nelts = 0; +} + +int h2_io_set_is_empty(h2_io_set *sp) +{ + AP_DEBUG_ASSERT(sp); + return sp->list->nelts == 0; +} + +void h2_io_set_iter(h2_io_set *sp, + h2_io_set_iter_fn *iter, void *ctx) +{ + int i; + for (i = 0; i < sp->list->nelts; ++i) { + h2_io *s = h2_io_IDX(sp->list, i); + if (!iter(ctx, s)) { + break; + } + } +} + +apr_size_t h2_io_set_size(h2_io_set *sp) +{ + return sp->list->nelts; +} + diff --git a/modules/http2/h2_io_set.h b/modules/http2/h2_io_set.h new file mode 100644 index 00000000000..a9c6546c701 --- /dev/null +++ b/modules/http2/h2_io_set.h @@ -0,0 +1,47 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_io_set__ +#define __mod_h2__h2_io_set__ + +struct h2_io; + +/** + * A set of h2_io instances. Allows lookup by stream id + * and other criteria. + */ +typedef struct h2_io_set h2_io_set; + +h2_io_set *h2_io_set_create(apr_pool_t *pool); + +void h2_io_set_destroy(h2_io_set *set); + +apr_status_t h2_io_set_add(h2_io_set *set, struct h2_io *io); +h2_io *h2_io_set_get(h2_io_set *set, int stream_id); +h2_io *h2_io_set_get_highest_prio(h2_io_set *set); +h2_io *h2_io_set_remove(h2_io_set *set, struct h2_io *io); + +void h2_io_set_remove_all(h2_io_set *set); +void h2_io_set_destroy_all(h2_io_set *set); +int h2_io_set_is_empty(h2_io_set *set); +apr_size_t h2_io_set_size(h2_io_set *set); + + +typedef int h2_io_set_iter_fn(void *ctx, struct h2_io *io); + +void h2_io_set_iter(h2_io_set *set, + h2_io_set_iter_fn *iter, void *ctx); + +#endif /* defined(__mod_h2__h2_io_set__) */ diff --git a/modules/http2/h2_mplx.c b/modules/http2/h2_mplx.c new file mode 100644 index 00000000000..c04b05e7729 --- /dev/null +++ b/modules/http2/h2_mplx.c @@ -0,0 +1,798 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "h2_private.h" +#include "h2_config.h" +#include "h2_conn.h" +#include "h2_io.h" +#include "h2_io_set.h" +#include "h2_response.h" +#include "h2_mplx.h" +#include "h2_request.h" +#include "h2_stream.h" +#include "h2_stream_set.h" +#include "h2_task.h" +#include "h2_task_input.h" +#include "h2_task_output.h" +#include "h2_task_queue.h" +#include "h2_workers.h" + + +static int is_aborted(h2_mplx *m, apr_status_t *pstatus) { + AP_DEBUG_ASSERT(m); + if (m->aborted) { + *pstatus = APR_ECONNABORTED; + return 1; + } + return 0; +} + +static void have_out_data_for(h2_mplx *m, int stream_id); + +static void h2_mplx_destroy(h2_mplx *m) +{ + AP_DEBUG_ASSERT(m); + m->aborted = 1; + if (m->q) { + h2_tq_destroy(m->q); + m->q = NULL; + } + if (m->ready_ios) { + h2_io_set_destroy(m->ready_ios); + m->ready_ios = NULL; + } + if (m->stream_ios) { + h2_io_set_destroy(m->stream_ios); + m->stream_ios = NULL; + } + + if (m->lock) { + apr_thread_mutex_destroy(m->lock); + m->lock = NULL; + } + + if (m->pool) { + apr_pool_destroy(m->pool); + } +} + +/** + * A h2_mplx needs to be thread-safe *and* if will be called by + * the h2_session thread *and* the h2_worker threads. Therefore: + * - calls are protected by a mutex lock, m->lock + * - the pool needs its own allocator, since apr_allocator_t are + * not re-entrant. The separate allocator works without a + * separate lock since we already protect h2_mplx itself. + * Since HTTP/2 connections can be expected to live longer than + * their HTTP/1 cousins, the separate allocator seems to work better + * than protecting a shared h2_session one with an own lock. + */ +h2_mplx *h2_mplx_create(conn_rec *c, apr_pool_t *parent, h2_workers *workers) +{ + apr_status_t status = APR_SUCCESS; + h2_config *conf = h2_config_get(c); + apr_allocator_t *allocator = NULL; + h2_mplx *m; + AP_DEBUG_ASSERT(conf); + + status = apr_allocator_create(&allocator); + if (status != APR_SUCCESS) { + return NULL; + } + + m = apr_pcalloc(parent, sizeof(h2_mplx)); + if (m) { + m->id = c->id; + APR_RING_ELEM_INIT(m, link); + apr_atomic_set32(&m->refs, 1); + m->c = c; + apr_pool_create_ex(&m->pool, parent, NULL, allocator); + if (!m->pool) { + return NULL; + } + apr_allocator_owner_set(allocator, m->pool); + + status = apr_thread_mutex_create(&m->lock, APR_THREAD_MUTEX_DEFAULT, + m->pool); + if (status != APR_SUCCESS) { + h2_mplx_destroy(m); + return NULL; + } + + m->bucket_alloc = apr_bucket_alloc_create(m->pool); + + m->q = h2_tq_create(m->id, m->pool); + m->stream_ios = h2_io_set_create(m->pool); + m->ready_ios = h2_io_set_create(m->pool); + m->closed = h2_stream_set_create(m->pool); + m->stream_max_mem = h2_config_geti(conf, H2_CONF_STREAM_MAX_MEM); + m->workers = workers; + + m->file_handles_allowed = h2_config_geti(conf, H2_CONF_SESSION_FILES); + } + return m; +} + +static void reference(h2_mplx *m) +{ + apr_atomic_inc32(&m->refs); +} + +static void release(h2_mplx *m) +{ + if (!apr_atomic_dec32(&m->refs)) { + if (m->join_wait) { + apr_thread_cond_signal(m->join_wait); + } + } +} + +void h2_mplx_reference(h2_mplx *m) +{ + reference(m); +} +void h2_mplx_release(h2_mplx *m) +{ + release(m); +} + +static void workers_register(h2_mplx *m) { + /* Initially, there was ref count increase for this as well, but + * this is not needed, even harmful. + * h2_workers is only a hub for all the h2_worker instances. + * At the end-of-life of this h2_mplx, we always unregister at + * the workers. The thing to manage are all the h2_worker instances + * out there. Those may hold a reference to this h2_mplx and we cannot + * call them to unregister. + * + * Therefore: ref counting for h2_workers in not needed, ref counting + * for h2_worker using this is critical. + */ + h2_workers_register(m->workers, m); +} + +static void workers_unregister(h2_mplx *m) { + h2_workers_unregister(m->workers, m); +} + +apr_status_t h2_mplx_release_and_join(h2_mplx *m, apr_thread_cond_t *wait) +{ + apr_status_t status; + workers_unregister(m); + + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + int attempts = 0; + + release(m); + while (apr_atomic_read32(&m->refs) > 0) { + m->join_wait = wait; + ap_log_cerror(APLOG_MARK, (attempts? APLOG_INFO : APLOG_DEBUG), + 0, m->c, + "h2_mplx(%ld): release_join, refs=%d, waiting...", + m->id, m->refs); + apr_thread_cond_timedwait(wait, m->lock, apr_time_from_sec(10)); + if (++attempts >= 6) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c, + APLOGNO(02952) + "h2_mplx(%ld): join attempts exhausted, refs=%d", + m->id, m->refs); + break; + } + } + if (m->join_wait) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, + "h2_mplx(%ld): release_join -> destroy", m->id); + } + m->join_wait = NULL; + apr_thread_mutex_unlock(m->lock); + h2_mplx_destroy(m); + } + return status; +} + +void h2_mplx_abort(h2_mplx *m) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + m->aborted = 1; + h2_io_set_destroy_all(m->stream_ios); + apr_thread_mutex_unlock(m->lock); + } + workers_unregister(m); +} + + +h2_stream *h2_mplx_open_io(h2_mplx *m, int stream_id) +{ + h2_stream *stream = NULL; + apr_status_t status; + h2_io *io; + + if (m->aborted) { + return NULL; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + apr_pool_t *stream_pool = m->spare_pool; + + if (!stream_pool) { + apr_pool_create(&stream_pool, m->pool); + } + else { + m->spare_pool = NULL; + } + + stream = h2_stream_create(stream_id, stream_pool, m); + stream->state = H2_STREAM_ST_OPEN; + + io = h2_io_set_get(m->stream_ios, stream_id); + if (!io) { + io = h2_io_create(stream_id, stream_pool, m->bucket_alloc); + h2_io_set_add(m->stream_ios, io); + } + status = io? APR_SUCCESS : APR_ENOMEM; + apr_thread_mutex_unlock(m->lock); + } + return stream; +} + +static void stream_destroy(h2_mplx *m, h2_stream *stream, h2_io *io) +{ + apr_pool_t *pool = h2_stream_detach_pool(stream); + if (pool) { + apr_pool_clear(pool); + if (m->spare_pool) { + apr_pool_destroy(m->spare_pool); + } + m->spare_pool = pool; + } + h2_stream_destroy(stream); + if (io) { + /* The pool is cleared/destroyed which also closes all + * allocated file handles. Give this count back to our + * file handle pool. */ + m->file_handles_allowed += io->files_handles_owned; + h2_io_set_remove(m->stream_ios, io); + h2_io_destroy(io); + } +} + +apr_status_t h2_mplx_cleanup_stream(h2_mplx *m, h2_stream *stream) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream->id); + if (!io || io->task_done) { + /* No more io or task already done -> cleanup immediately */ + stream_destroy(m, stream, io); + } + else { + /* Add stream to closed set for cleanup when task is done */ + h2_stream_set_add(m->closed, stream); + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +void h2_mplx_task_done(h2_mplx *m, int stream_id) +{ + apr_status_t status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_stream *stream = h2_stream_set_get(m->closed, stream_id); + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, + "h2_mplx(%ld): task(%d) done", m->id, stream_id); + if (stream) { + /* stream was already closed by main connection and is in + * zombie state. Now that the task is done with it, we + * can free its resources. */ + h2_stream_set_remove(m->closed, stream); + stream_destroy(m, stream, io); + } + else if (io) { + /* main connection has not finished stream. Mark task as done + * so that eventual cleanup can start immediately. */ + io->task_done = 1; + } + apr_thread_mutex_unlock(m->lock); + } +} + +apr_status_t h2_mplx_in_read(h2_mplx *m, apr_read_type_e block, + int stream_id, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + io->input_arrived = iowait; + status = h2_io_in_read(io, bb, 0); + while (status == APR_EAGAIN + && !is_aborted(m, &status) + && block == APR_BLOCK_READ) { + apr_thread_cond_wait(io->input_arrived, m->lock); + status = h2_io_in_read(io, bb, 0); + } + io->input_arrived = NULL; + } + else { + status = APR_EOF; + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +apr_status_t h2_mplx_in_write(h2_mplx *m, int stream_id, + apr_bucket_brigade *bb) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + status = h2_io_in_write(io, bb); + if (io->input_arrived) { + apr_thread_cond_signal(io->input_arrived); + } + } + else { + status = APR_EOF; + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +apr_status_t h2_mplx_in_close(h2_mplx *m, int stream_id) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + status = h2_io_in_close(io); + if (io->input_arrived) { + apr_thread_cond_signal(io->input_arrived); + } + } + else { + status = APR_ECONNABORTED; + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +typedef struct { + h2_mplx_consumed_cb *cb; + void *cb_ctx; + int streams_updated; +} update_ctx; + +static int update_window(void *ctx, h2_io *io) +{ + if (io->input_consumed) { + update_ctx *uctx = (update_ctx*)ctx; + uctx->cb(uctx->cb_ctx, io->id, io->input_consumed); + io->input_consumed = 0; + ++uctx->streams_updated; + } + return 1; +} + +apr_status_t h2_mplx_in_update_windows(h2_mplx *m, + h2_mplx_consumed_cb *cb, void *cb_ctx) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + update_ctx ctx; + + ctx.cb = cb; + ctx.cb_ctx = cb_ctx; + ctx.streams_updated = 0; + + status = APR_EAGAIN; + h2_io_set_iter(m->stream_ios, update_window, &ctx); + + if (ctx.streams_updated) { + status = APR_SUCCESS; + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +apr_status_t h2_mplx_out_readx(h2_mplx *m, int stream_id, + h2_io_data_cb *cb, void *ctx, + apr_size_t *plen, int *peos) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + status = h2_io_out_readx(io, cb, ctx, plen, peos); + if (status == APR_SUCCESS && io->output_drained) { + apr_thread_cond_signal(io->output_drained); + } + } + else { + status = APR_ECONNABORTED; + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +h2_stream *h2_mplx_next_submit(h2_mplx *m, h2_stream_set *streams) +{ + apr_status_t status; + h2_stream *stream = NULL; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return NULL; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get_highest_prio(m->ready_ios); + if (io) { + h2_response *response = io->response; + h2_io_set_remove(m->ready_ios, io); + + stream = h2_stream_set_get(streams, response->stream_id); + if (stream) { + h2_stream_set_response(stream, response, io->bbout); + if (io->output_drained) { + apr_thread_cond_signal(io->output_drained); + } + } + else { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, APR_NOTFOUND, m->c, + APLOGNO(02953) "h2_mplx(%ld): stream for response %d", + m->id, response->stream_id); + } + } + apr_thread_mutex_unlock(m->lock); + } + return stream; +} + +static apr_status_t out_write(h2_mplx *m, h2_io *io, + ap_filter_t* f, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait) +{ + apr_status_t status = APR_SUCCESS; + /* We check the memory footprint queued for this stream_id + * and block if it exceeds our configured limit. + * We will not split buckets to enforce the limit to the last + * byte. After all, the bucket is already in memory. + */ + while (!APR_BRIGADE_EMPTY(bb) + && (status == APR_SUCCESS) + && !is_aborted(m, &status)) { + + status = h2_io_out_write(io, bb, m->stream_max_mem, + &m->file_handles_allowed); + + /* Wait for data to drain until there is room again */ + while (!APR_BRIGADE_EMPTY(bb) + && iowait + && status == APR_SUCCESS + && (m->stream_max_mem <= h2_io_out_length(io)) + && !is_aborted(m, &status)) { + io->output_drained = iowait; + if (f) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_mplx(%ld-%d): waiting for out drain", + m->id, io->id); + } + apr_thread_cond_wait(io->output_drained, m->lock); + io->output_drained = NULL; + } + } + apr_brigade_cleanup(bb); + return status; +} + +static apr_status_t out_open(h2_mplx *m, int stream_id, h2_response *response, + ap_filter_t* f, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait) +{ + apr_status_t status = APR_SUCCESS; + + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + if (f) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c, + "h2_mplx(%ld-%d): open response: %s", + m->id, stream_id, response->status); + } + + io->response = h2_response_copy(io->pool, response); + h2_io_set_add(m->ready_ios, io); + if (bb) { + status = out_write(m, io, f, bb, iowait); + } + have_out_data_for(m, stream_id); + } + else { + status = APR_ECONNABORTED; + } + return status; +} + +apr_status_t h2_mplx_out_open(h2_mplx *m, int stream_id, h2_response *response, + ap_filter_t* f, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + status = out_open(m, stream_id, response, f, bb, iowait); + if (m->aborted) { + return APR_ECONNABORTED; + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + + +apr_status_t h2_mplx_out_write(h2_mplx *m, int stream_id, + ap_filter_t* f, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + if (!m->aborted) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + status = out_write(m, io, f, bb, iowait); + have_out_data_for(m, stream_id); + if (m->aborted) { + return APR_ECONNABORTED; + } + } + else { + status = APR_ECONNABORTED; + } + } + + if (m->lock) { + apr_thread_mutex_unlock(m->lock); + } + } + return status; +} + +apr_status_t h2_mplx_out_close(h2_mplx *m, int stream_id) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + if (!m->aborted) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + if (!io->response->ngheader) { + /* In case a close comes before a response was created, + * insert an error one so that our streams can properly + * reset. + */ + h2_response *r = h2_response_create(stream_id, + "500", NULL, m->pool); + status = out_open(m, stream_id, r, NULL, NULL, NULL); + } + status = h2_io_out_close(io); + have_out_data_for(m, stream_id); + if (m->aborted) { + /* if we were the last output, the whole session might + * have gone down in the meantime. + */ + return APR_SUCCESS; + } + } + else { + status = APR_ECONNABORTED; + } + } + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +int h2_mplx_in_has_eos_for(h2_mplx *m, int stream_id) +{ + int has_eos = 0; + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return 0; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + has_eos = h2_io_in_has_eos_for(io); + } + apr_thread_mutex_unlock(m->lock); + } + return has_eos; +} + +int h2_mplx_out_has_data_for(h2_mplx *m, int stream_id) +{ + apr_status_t status; + int has_data = 0; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return 0; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + h2_io *io = h2_io_set_get(m->stream_ios, stream_id); + if (io) { + has_data = h2_io_out_has_data(io); + } + apr_thread_mutex_unlock(m->lock); + } + return has_data; +} + +apr_status_t h2_mplx_out_trywait(h2_mplx *m, apr_interval_time_t timeout, + apr_thread_cond_t *iowait) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + m->added_output = iowait; + status = apr_thread_cond_timedwait(m->added_output, m->lock, timeout); + if (APLOGctrace2(m->c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, + "h2_mplx(%ld): trywait on data for %f ms)", + m->id, timeout/1000.0); + } + m->added_output = NULL; + apr_thread_mutex_unlock(m->lock); + } + return status; +} + +static void have_out_data_for(h2_mplx *m, int stream_id) +{ + (void)stream_id; + AP_DEBUG_ASSERT(m); + if (m->added_output) { + apr_thread_cond_signal(m->added_output); + } +} + +apr_status_t h2_mplx_do_task(h2_mplx *m, struct h2_task *task) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + /* TODO: needs to sort queue by priority */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, + "h2_mplx: do task(%s)", task->id); + h2_tq_append(m->q, task); + apr_thread_mutex_unlock(m->lock); + } + workers_register(m); + return status; +} + +h2_task *h2_mplx_pop_task(h2_mplx *m, int *has_more) +{ + h2_task *task = NULL; + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + *has_more = 0; + return NULL; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + task = h2_tq_pop_first(m->q); + if (task) { + h2_task_set_started(task); + } + *has_more = !h2_tq_empty(m->q); + apr_thread_mutex_unlock(m->lock); + } + return task; +} + +apr_status_t h2_mplx_create_task(h2_mplx *m, struct h2_stream *stream) +{ + apr_status_t status; + AP_DEBUG_ASSERT(m); + if (m->aborted) { + return APR_ECONNABORTED; + } + status = apr_thread_mutex_lock(m->lock); + if (APR_SUCCESS == status) { + conn_rec *c = h2_conn_create(m->c, stream->pool); + stream->task = h2_task_create(m->id, stream->id, + stream->pool, m, c); + + apr_thread_mutex_unlock(m->lock); + } + return status; +} + diff --git a/modules/http2/h2_mplx.h b/modules/http2/h2_mplx.h new file mode 100644 index 00000000000..62977d6157c --- /dev/null +++ b/modules/http2/h2_mplx.h @@ -0,0 +1,322 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_mplx__ +#define __mod_h2__h2_mplx__ + +/** + * The stream multiplexer. It pushes buckets from the connection + * thread to the stream task threads and vice versa. It's thread-safe + * to use. + * + * There is one h2_mplx instance for each h2_session, which sits on top + * of a particular httpd conn_rec. Input goes from the connection to + * the stream tasks. Output goes from the stream tasks to the connection, + * e.g. the client. + * + * For each stream, there can be at most "H2StreamMaxMemSize" output bytes + * queued in the multiplexer. If a task thread tries to write more + * data, it is blocked until space becomes available. + * + * Writing input is never blocked. In order to use flow control on the input, + * the mplx can be polled for input data consumption. + */ + +struct apr_pool_t; +struct apr_thread_mutex_t; +struct apr_thread_cond_t; +struct h2_config; +struct h2_response; +struct h2_task; +struct h2_stream; +struct h2_io_set; +struct apr_thread_cond_t; +struct h2_workers; +struct h2_stream_set; +struct h2_task_queue; + +#include "h2_io.h" + +typedef struct h2_mplx h2_mplx; + +struct h2_mplx { + long id; + APR_RING_ENTRY(h2_mplx) link; + volatile apr_uint32_t refs; + conn_rec *c; + apr_pool_t *pool; + apr_bucket_alloc_t *bucket_alloc; + + struct h2_task_queue *q; + struct h2_io_set *stream_ios; + struct h2_io_set *ready_ios; + + apr_thread_mutex_t *lock; + struct apr_thread_cond_t *added_output; + struct apr_thread_cond_t *join_wait; + + int aborted; + apr_size_t stream_max_mem; + + apr_pool_t *spare_pool; /* spare pool, ready for next stream */ + struct h2_stream_set *closed; /* streams closed, but task ongoing */ + struct h2_workers *workers; + int file_handles_allowed; +}; + +/******************************************************************************* + * Object lifecycle and information. + ******************************************************************************/ + +/** + * Create the multiplexer for the given HTTP2 session. + * Implicitly has reference count 1. + */ +h2_mplx *h2_mplx_create(conn_rec *c, apr_pool_t *master, + struct h2_workers *workers); + +/** + * Increase the reference counter of this mplx. + */ +void h2_mplx_reference(h2_mplx *m); + +/** + * Decreases the reference counter of this mplx. + */ +void h2_mplx_release(h2_mplx *m); +/** + * Decreases the reference counter of this mplx and waits for it + * to reached 0, destroy the mplx afterwards. + * This is to be called from the thread that created the mplx in + * the first place. + * @param m the mplx to be released and destroyed + * @param wait condition var to wait on for ref counter == 0 + */ +apr_status_t h2_mplx_release_and_join(h2_mplx *m, struct apr_thread_cond_t *wait); + +/** + * Aborts the multiplexer. It will answer all future invocation with + * APR_ECONNABORTED, leading to early termination of ongoing tasks. + */ +void h2_mplx_abort(h2_mplx *mplx); + +void h2_mplx_task_done(h2_mplx *m, int stream_id); + +/******************************************************************************* + * IO lifetime of streams. + ******************************************************************************/ +/** + * Prepares the multiplexer to handle in-/output on the given stream id. + */ +struct h2_stream *h2_mplx_open_io(h2_mplx *mplx, int stream_id); + +/** + * Ends cleanup of a stream in sync with execution thread. + */ +apr_status_t h2_mplx_cleanup_stream(h2_mplx *m, struct h2_stream *stream); + +/* Return != 0 iff the multiplexer has data for the given stream. + */ +int h2_mplx_out_has_data_for(h2_mplx *m, int stream_id); + +/** + * Waits on output data from any stream in this session to become available. + * Returns APR_TIMEUP if no data arrived in the given time. + */ +apr_status_t h2_mplx_out_trywait(h2_mplx *m, apr_interval_time_t timeout, + struct apr_thread_cond_t *iowait); + +/******************************************************************************* + * Stream processing. + ******************************************************************************/ + +/** + * Perform the task on the given stream. + */ +apr_status_t h2_mplx_do_task(h2_mplx *mplx, struct h2_task *task); + +struct h2_task *h2_mplx_pop_task(h2_mplx *mplx, int *has_more); + +apr_status_t h2_mplx_create_task(h2_mplx *mplx, struct h2_stream *stream); + +/******************************************************************************* + * Input handling of streams. + ******************************************************************************/ + +/** + * Reads a buckets for the given stream_id. Will return ARP_EAGAIN when + * called with APR_NONBLOCK_READ and no data present. Will return APR_EOF + * when the end of the stream input has been reached. + * The condition passed in will be used for blocking/signalling and will + * be protected by the mplx's own mutex. + */ +apr_status_t h2_mplx_in_read(h2_mplx *m, apr_read_type_e block, + int stream_id, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait); + +/** + * Appends data to the input of the given stream. Storage of input data is + * not subject to flow control. + */ +apr_status_t h2_mplx_in_write(h2_mplx *mplx, int stream_id, + apr_bucket_brigade *bb); + +/** + * Closes the input for the given stream_id. + */ +apr_status_t h2_mplx_in_close(h2_mplx *m, int stream_id); + +/** + * Returns != 0 iff the input for the given stream has been closed. There + * could still be data queued, but it can be read without blocking. + */ +int h2_mplx_in_has_eos_for(h2_mplx *m, int stream_id); + +/** + * Callback invoked for every stream that had input data read since + * the last invocation. + */ +typedef void h2_mplx_consumed_cb(void *ctx, int stream_id, apr_size_t consumed); + +/** + * Invoke the callback for all streams that had bytes read since the last + * call to this function. If no stream had input data consumed, the callback + * is not invoked. + * Returns APR_SUCCESS when an update happened, APR_EAGAIN if no update + * happened. + */ +apr_status_t h2_mplx_in_update_windows(h2_mplx *m, + h2_mplx_consumed_cb *cb, void *ctx); + +/******************************************************************************* + * Output handling of streams. + ******************************************************************************/ + +/** + * Get a stream whose response is ready for submit. Will set response and + * any out data available in stream. + * @param m the mplxer to get a response from + * @param bb the brigade to place any existing repsonse body data into + */ +struct h2_stream *h2_mplx_next_submit(h2_mplx *m, + struct h2_stream_set *streams); + +/** + * Reads output data from the given stream. Will never block, but + * return APR_EAGAIN until data arrives or the stream is closed. + */ +apr_status_t h2_mplx_out_readx(h2_mplx *mplx, int stream_id, + h2_io_data_cb *cb, void *ctx, + apr_size_t *plen, int *peos); + +/** + * Opens the output for the given stream with the specified response. + */ +apr_status_t h2_mplx_out_open(h2_mplx *mplx, int stream_id, + struct h2_response *response, + ap_filter_t* filter, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait); + +/** + * Append the brigade to the stream output. Might block if amount + * of bytes buffered reaches configured max. + * @param stream_id the stream identifier + * @param filter the apache filter context of the data + * @param bb the bucket brigade to append + * @param iowait a conditional used for block/signalling in h2_mplx + */ +apr_status_t h2_mplx_out_write(h2_mplx *mplx, int stream_id, + ap_filter_t* filter, apr_bucket_brigade *bb, + struct apr_thread_cond_t *iowait); + +/** + * Closes the output stream. Readers of this stream will get all pending + * data and then only APR_EOF as result. + */ +apr_status_t h2_mplx_out_close(h2_mplx *m, int stream_id); + +/******************************************************************************* + * h2_mplx list Manipulation. + ******************************************************************************/ + +/** + * The magic pointer value that indicates the head of a h2_mplx list + * @param b The mplx list + * @return The magic pointer value + */ +#define H2_MPLX_LIST_SENTINEL(b) APR_RING_SENTINEL((b), h2_mplx, link) + +/** + * Determine if the mplx list is empty + * @param b The list to check + * @return true or false + */ +#define H2_MPLX_LIST_EMPTY(b) APR_RING_EMPTY((b), h2_mplx, link) + +/** + * Return the first mplx in a list + * @param b The list to query + * @return The first mplx in the list + */ +#define H2_MPLX_LIST_FIRST(b) APR_RING_FIRST(b) + +/** + * Return the last mplx in a list + * @param b The list to query + * @return The last mplx int he list + */ +#define H2_MPLX_LIST_LAST(b) APR_RING_LAST(b) + +/** + * Insert a single mplx at the front of a list + * @param b The list to add to + * @param e The mplx to insert + */ +#define H2_MPLX_LIST_INSERT_HEAD(b, e) do { \ +h2_mplx *ap__b = (e); \ +APR_RING_INSERT_HEAD((b), ap__b, h2_mplx, link); \ +} while (0) + +/** + * Insert a single mplx at the end of a list + * @param b The list to add to + * @param e The mplx to insert + */ +#define H2_MPLX_LIST_INSERT_TAIL(b, e) do { \ +h2_mplx *ap__b = (e); \ +APR_RING_INSERT_TAIL((b), ap__b, h2_mplx, link); \ +} while (0) + +/** + * Get the next mplx in the list + * @param e The current mplx + * @return The next mplx + */ +#define H2_MPLX_NEXT(e) APR_RING_NEXT((e), link) +/** + * Get the previous mplx in the list + * @param e The current mplx + * @return The previous mplx + */ +#define H2_MPLX_PREV(e) APR_RING_PREV((e), link) + +/** + * Remove a mplx from its list + * @param e The mplx to remove + */ +#define H2_MPLX_REMOVE(e) APR_RING_REMOVE((e), link) + + +#endif /* defined(__mod_h2__h2_mplx__) */ diff --git a/modules/http2/h2_private.h b/modules/http2/h2_private.h new file mode 100644 index 00000000000..6931278401d --- /dev/null +++ b/modules/http2/h2_private.h @@ -0,0 +1,36 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef mod_h2_h2_private_h +#define mod_h2_h2_private_h + +#include + +extern module AP_MODULE_DECLARE_DATA h2_module; + +APLOG_USE_MODULE(h2); + + +#define H2_HEADER_METHOD ":method" +#define H2_HEADER_METHOD_LEN 7 +#define H2_HEADER_SCHEME ":scheme" +#define H2_HEADER_SCHEME_LEN 7 +#define H2_HEADER_AUTH ":authority" +#define H2_HEADER_AUTH_LEN 10 +#define H2_HEADER_PATH ":path" +#define H2_HEADER_PATH_LEN 5 +#define H2_CRLF "\r\n" + +#endif diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c new file mode 100644 index 00000000000..ca9a362cb56 --- /dev/null +++ b/modules/http2/h2_request.c @@ -0,0 +1,182 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_mplx.h" +#include "h2_to_h1.h" +#include "h2_request.h" +#include "h2_task.h" +#include "h2_util.h" + + +h2_request *h2_request_create(int id, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc) +{ + h2_request *req = apr_pcalloc(pool, sizeof(h2_request)); + if (req) { + req->id = id; + req->pool = pool; + req->bucket_alloc = bucket_alloc; + } + return req; +} + +void h2_request_destroy(h2_request *req) +{ + if (req->to_h1) { + h2_to_h1_destroy(req->to_h1); + req->to_h1 = NULL; + } +} + +static apr_status_t insert_request_line(h2_request *req, h2_mplx *m); + +apr_status_t h2_request_rwrite(h2_request *req, request_rec *r, h2_mplx *m) +{ + apr_status_t status; + req->method = r->method; + req->authority = r->hostname; + req->path = r->uri; + if (!ap_strchr_c(req->authority, ':') && r->parsed_uri.port_str) { + req->authority = apr_psprintf(req->pool, "%s:%s", req->authority, + r->parsed_uri.port_str); + } + req->scheme = NULL; + + + status = insert_request_line(req, m); + if (status == APR_SUCCESS) { + status = h2_to_h1_add_headers(req->to_h1, r->headers_in); + } + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + "h2_request(%d): written request %s %s, host=%s", + req->id, req->method, req->path, req->authority); + + return status; +} + +apr_status_t h2_request_write_header(h2_request *req, + const char *name, size_t nlen, + const char *value, size_t vlen, + h2_mplx *m) +{ + apr_status_t status = APR_SUCCESS; + + if (nlen <= 0) { + return status; + } + + if (name[0] == ':') { + /* pseudo header, see ch. 8.1.2.3, always should come first */ + if (req->to_h1) { + ap_log_perror(APLOG_MARK, APLOG_ERR, 0, req->pool, + APLOGNO(02917) + "h2_request(%d): pseudo header after request start", + req->id); + return APR_EGENERAL; + } + + if (H2_HEADER_METHOD_LEN == nlen + && !strncmp(H2_HEADER_METHOD, name, nlen)) { + req->method = apr_pstrndup(req->pool, value, vlen); + } + else if (H2_HEADER_SCHEME_LEN == nlen + && !strncmp(H2_HEADER_SCHEME, name, nlen)) { + req->scheme = apr_pstrndup(req->pool, value, vlen); + } + else if (H2_HEADER_PATH_LEN == nlen + && !strncmp(H2_HEADER_PATH, name, nlen)) { + req->path = apr_pstrndup(req->pool, value, vlen); + } + else if (H2_HEADER_AUTH_LEN == nlen + && !strncmp(H2_HEADER_AUTH, name, nlen)) { + req->authority = apr_pstrndup(req->pool, value, vlen); + } + else { + char buffer[32]; + memset(buffer, 0, 32); + strncpy(buffer, name, (nlen > 31)? 31 : nlen); + ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, req->pool, + APLOGNO(02954) + "h2_request(%d): ignoring unknown pseudo header %s", + req->id, buffer); + } + } + else { + /* non-pseudo header, append to work bucket of stream */ + if (!req->to_h1) { + status = insert_request_line(req, m); + if (status != APR_SUCCESS) { + return status; + } + } + + if (status == APR_SUCCESS) { + status = h2_to_h1_add_header(req->to_h1, + name, nlen, value, vlen); + } + } + + return status; +} + +apr_status_t h2_request_write_data(h2_request *req, + const char *data, size_t len) +{ + return h2_to_h1_add_data(req->to_h1, data, len); +} + +apr_status_t h2_request_end_headers(h2_request *req, struct h2_mplx *m, + h2_task *task, int eos) +{ + if (!req->to_h1) { + apr_status_t status = insert_request_line(req, m); + if (status != APR_SUCCESS) { + return status; + } + } + return h2_to_h1_end_headers(req->to_h1, task, eos); +} + +apr_status_t h2_request_close(h2_request *req) +{ + return h2_to_h1_close(req->to_h1); +} + +static apr_status_t insert_request_line(h2_request *req, h2_mplx *m) +{ + req->to_h1 = h2_to_h1_create(req->id, req->pool, req->bucket_alloc, + req->method, + req->scheme, + req->authority, + req->path, m); + return req->to_h1? APR_SUCCESS : APR_ENOMEM; +} + +apr_status_t h2_request_flush(h2_request *req) +{ + return h2_to_h1_flush(req->to_h1); +} + diff --git a/modules/http2/h2_request.h b/modules/http2/h2_request.h new file mode 100644 index 00000000000..aa5e0bc3c09 --- /dev/null +++ b/modules/http2/h2_request.h @@ -0,0 +1,67 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_request__ +#define __mod_h2__h2_request__ + +/* h2_request is the transformer of HTTP2 streams into HTTP/1.1 internal + * format that will be fed to various httpd input filters to finally + * become a request_rec to be handled by soemone. + * + * Ideally, we would make a request_rec without serializing the headers + * we have only to make someone else parse them back. + */ +struct h2_to_h1; +struct h2_mplx; +struct h2_task; + +typedef struct h2_request h2_request; + +struct h2_request { + int id; /* http2 stream id */ + apr_pool_t *pool; + apr_bucket_alloc_t *bucket_alloc; + struct h2_to_h1 *to_h1; /* Converter to HTTP/1.1 format*/ + + /* pseudo header values, see ch. 8.1.2.3 */ + const char *method; + const char *scheme; + const char *authority; + const char *path; +}; + +h2_request *h2_request_create(int id, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc); +void h2_request_destroy(h2_request *req); + +apr_status_t h2_request_flush(h2_request *req); + +apr_status_t h2_request_write_header(h2_request *req, + const char *name, size_t nlen, + const char *value, size_t vlen, + struct h2_mplx *m); + +apr_status_t h2_request_write_data(h2_request *request, + const char *data, size_t len); + +apr_status_t h2_request_end_headers(h2_request *req, struct h2_mplx *m, + struct h2_task *task, int eos); + +apr_status_t h2_request_close(h2_request *req); + +apr_status_t h2_request_rwrite(h2_request *req, request_rec *r, + struct h2_mplx *m); + +#endif /* defined(__mod_h2__h2_request__) */ diff --git a/modules/http2/h2_response.c b/modules/http2/h2_response.c new file mode 100644 index 00000000000..9cedd855615 --- /dev/null +++ b/modules/http2/h2_response.c @@ -0,0 +1,232 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include + +#include + +#include "h2_private.h" +#include "h2_util.h" +#include "h2_response.h" + +static h2_ngheader *make_ngheader(apr_pool_t *pool, const char *status, + apr_table_t *header); + +static int ignore_header(const char *name) +{ + return (H2_HD_MATCH_LIT_CS("connection", name) + || H2_HD_MATCH_LIT_CS("proxy-connection", name) + || H2_HD_MATCH_LIT_CS("upgrade", name) + || H2_HD_MATCH_LIT_CS("keep-alive", name) + || H2_HD_MATCH_LIT_CS("transfer-encoding", name)); +} + +h2_response *h2_response_create(int stream_id, + const char *http_status, + apr_array_header_t *hlines, + apr_pool_t *pool) +{ + apr_table_t *header; + h2_response *response = apr_pcalloc(pool, sizeof(h2_response)); + int i; + if (response == NULL) { + return NULL; + } + + response->stream_id = stream_id; + response->status = http_status; + response->content_length = -1; + + if (hlines) { + header = apr_table_make(pool, hlines->nelts); + for (i = 0; i < hlines->nelts; ++i) { + char *hline = ((char **)hlines->elts)[i]; + char *sep = ap_strchr(hline, ':'); + if (!sep) { + ap_log_perror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, pool, + APLOGNO(02955) "h2_response(%d): invalid header[%d] '%s'", + response->stream_id, i, (char*)hline); + /* not valid format, abort */ + return NULL; + } + (*sep++) = '\0'; + while (*sep == ' ' || *sep == '\t') { + ++sep; + } + if (ignore_header(hline)) { + /* never forward, ch. 8.1.2.2 */ + } + else { + apr_table_merge(header, hline, sep); + if (*sep && H2_HD_MATCH_LIT_CS("content-length", hline)) { + char *end; + response->content_length = apr_strtoi64(sep, &end, 10); + if (sep == end) { + ap_log_perror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, + pool, APLOGNO(02956) + "h2_response(%d): content-length" + " value not parsed: %s", + response->stream_id, sep); + response->content_length = -1; + } + } + } + } + } + else { + header = apr_table_make(pool, 0); + } + + response->rheader = header; + return response; +} + +h2_response *h2_response_rcreate(int stream_id, request_rec *r, + apr_table_t *header, apr_pool_t *pool) +{ + h2_response *response = apr_pcalloc(pool, sizeof(h2_response)); + if (response == NULL) { + return NULL; + } + + response->stream_id = stream_id; + response->status = apr_psprintf(pool, "%d", r->status); + response->content_length = -1; + response->rheader = header; + + return response; +} + +void h2_response_destroy(h2_response *response) +{ + (void)response; +} + +h2_response *h2_response_copy(apr_pool_t *pool, h2_response *from) +{ + h2_response *to = apr_pcalloc(pool, sizeof(h2_response)); + to->stream_id = from->stream_id; + to->status = apr_pstrdup(pool, from->status); + to->content_length = from->content_length; + if (from->rheader) { + to->ngheader = make_ngheader(pool, to->status, from->rheader); + } + return to; +} + +typedef struct { + nghttp2_nv *nv; + size_t nvlen; + size_t nvstrlen; + size_t offset; + char *strbuf; + apr_pool_t *pool; +} nvctx_t; + +static int count_header(void *ctx, const char *key, const char *value) +{ + if (!ignore_header(key)) { + nvctx_t *nvctx = (nvctx_t*)ctx; + nvctx->nvlen++; + nvctx->nvstrlen += strlen(key) + strlen(value) + 2; + } + return 1; +} + +#define NV_ADD_LIT_CS(nv, k, v) addnv_lit_cs(nv, k, sizeof(k) - 1, v, strlen(v)) +#define NV_ADD_CS_CS(nv, k, v) addnv_cs_cs(nv, k, strlen(k), v, strlen(v)) +#define NV_BUF_ADD(nv, s, len) memcpy(nv->strbuf, s, len); \ +s = nv->strbuf; \ +nv->strbuf += len + 1 + +static void addnv_cs_cs(nvctx_t *ctx, const char *key, size_t key_len, + const char *value, size_t val_len) +{ + nghttp2_nv *nv = &ctx->nv[ctx->offset]; + + NV_BUF_ADD(ctx, key, key_len); + NV_BUF_ADD(ctx, value, val_len); + + nv->name = (uint8_t*)key; + nv->namelen = key_len; + nv->value = (uint8_t*)value; + nv->valuelen = val_len; + + ctx->offset++; +} + +static void addnv_lit_cs(nvctx_t *ctx, const char *key, size_t key_len, + const char *value, size_t val_len) +{ + nghttp2_nv *nv = &ctx->nv[ctx->offset]; + + NV_BUF_ADD(ctx, value, val_len); + + nv->name = (uint8_t*)key; + nv->namelen = key_len; + nv->value = (uint8_t*)value; + nv->valuelen = val_len; + + ctx->offset++; +} + +static int add_header(void *ctx, const char *key, const char *value) +{ + if (!ignore_header(key)) { + nvctx_t *nvctx = (nvctx_t*)ctx; + NV_ADD_CS_CS(nvctx, key, value); + } + return 1; +} + +static h2_ngheader *make_ngheader(apr_pool_t *pool, const char *status, + apr_table_t *header) +{ + size_t n; + h2_ngheader *h; + nvctx_t ctx; + + ctx.nv = NULL; + ctx.nvlen = 1; + ctx.nvstrlen = strlen(status) + 1; + ctx.offset = 0; + ctx.strbuf = NULL; + ctx.pool = pool; + + apr_table_do(count_header, &ctx, header, NULL); + + n = (sizeof(h2_ngheader) + + (ctx.nvlen * sizeof(nghttp2_nv)) + ctx.nvstrlen); + h = apr_pcalloc(pool, n); + if (h) { + ctx.nv = (nghttp2_nv*)(h + 1); + ctx.strbuf = (char*)&ctx.nv[ctx.nvlen]; + + NV_ADD_LIT_CS(&ctx, ":status", status); + apr_table_do(add_header, &ctx, header, NULL); + + h->nv = ctx.nv; + h->nvlen = ctx.nvlen; + } + return h; +} + diff --git a/modules/http2/h2_response.h b/modules/http2/h2_response.h new file mode 100644 index 00000000000..456d2226ed2 --- /dev/null +++ b/modules/http2/h2_response.h @@ -0,0 +1,47 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_response__ +#define __mod_h2__h2_response__ + +/* h2_response is just the data belonging the the head of a HTTP response, + * suitable prepared to be fed to nghttp2 for response submit. + */ +typedef struct h2_ngheader { + nghttp2_nv *nv; + apr_size_t nvlen; +} h2_ngheader; + +typedef struct h2_response { + int stream_id; + const char *status; + apr_off_t content_length; + apr_table_t *rheader; + h2_ngheader *ngheader; +} h2_response; + +h2_response *h2_response_create(int stream_id, + const char *http_status, + apr_array_header_t *hlines, + apr_pool_t *pool); + +h2_response *h2_response_rcreate(int stream_id, request_rec *r, + apr_table_t *header, apr_pool_t *pool); + +void h2_response_destroy(h2_response *response); + +h2_response *h2_response_copy(apr_pool_t *pool, h2_response *from); + +#endif /* defined(__mod_h2__h2_response__) */ diff --git a/modules/http2/h2_session.c b/modules/http2/h2_session.c new file mode 100644 index 00000000000..8c8bced32a4 --- /dev/null +++ b/modules/http2/h2_session.c @@ -0,0 +1,1257 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_config.h" +#include "h2_h2.h" +#include "h2_mplx.h" +#include "h2_response.h" +#include "h2_stream.h" +#include "h2_stream_set.h" +#include "h2_from_h1.h" +#include "h2_task.h" +#include "h2_session.h" +#include "h2_util.h" +#include "h2_version.h" +#include "h2_workers.h" + +static int frame_print(const nghttp2_frame *frame, char *buffer, size_t maxlen); + +static int h2_session_status_from_apr_status(apr_status_t rv) +{ + switch (rv) { + case APR_SUCCESS: + return NGHTTP2_NO_ERROR; + case APR_EAGAIN: + case APR_TIMEUP: + return NGHTTP2_ERR_WOULDBLOCK; + case APR_EOF: + return NGHTTP2_ERR_EOF; + default: + return NGHTTP2_ERR_PROTO; + } +} + +static int stream_open(h2_session *session, int stream_id) +{ + h2_stream * stream; + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + stream = h2_mplx_open_io(session->mplx, stream_id); + if (stream) { + h2_stream_set_add(session->streams, stream); + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_session: stream(%ld-%d): opened", + session->id, stream_id); + + return 0; + } + + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOMEM, session->c, + APLOGNO(02918) + "h2_session: stream(%ld-%d): unable to create", + session->id, stream_id); + return NGHTTP2_ERR_INVALID_STREAM_ID; +} + +static apr_status_t stream_end_headers(h2_session *session, + h2_stream *stream, int eos) +{ + (void)session; + return h2_stream_write_eoh(stream, eos); +} + +static apr_status_t send_data(h2_session *session, const char *data, + apr_size_t length); + +/* + * Callback when nghttp2 wants to send bytes back to the client. + */ +static ssize_t send_cb(nghttp2_session *ngh2, + const uint8_t *data, size_t length, + int flags, void *userp) +{ + h2_session *session = (h2_session *)userp; + apr_status_t status = send_data(session, (const char *)data, length); + + (void)ngh2; + (void)flags; + if (status == APR_SUCCESS) { + return length; + } + if (status == APR_EAGAIN || status == APR_TIMEUP) { + return NGHTTP2_ERR_WOULDBLOCK; + } + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, + "h2_session: send error"); + return h2_session_status_from_apr_status(status); +} + +static int on_invalid_frame_recv_cb(nghttp2_session *ngh2, + const nghttp2_frame *frame, + int error, void *userp) +{ + h2_session *session = (h2_session *)userp; + (void)ngh2; + + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (APLOGctrace2(session->c)) { + char buffer[256]; + + frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_session: callback on_invalid_frame_recv error=%d %s", + error, buffer); + } + return 0; +} + +static int on_data_chunk_recv_cb(nghttp2_session *ngh2, uint8_t flags, + int32_t stream_id, + const uint8_t *data, size_t len, void *userp) +{ + int rv; + h2_session *session = (h2_session *)userp; + h2_stream * stream; + apr_status_t status; + + (void)flags; + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + stream = h2_stream_set_get(session->streams, stream_id); + if (!stream) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, + APLOGNO(02919) + "h2_session: stream(%ld-%d): on_data_chunk for unknown stream", + session->id, (int)stream_id); + rv = nghttp2_submit_rst_stream(ngh2, NGHTTP2_FLAG_NONE, stream_id, + NGHTTP2_INTERNAL_ERROR); + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + + status = h2_stream_write_data(stream, (const char *)data, len); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, + "h2_stream(%ld-%d): written DATA, length %d", + session->id, stream_id, (int)len); + if (status != APR_SUCCESS) { + rv = nghttp2_submit_rst_stream(ngh2, NGHTTP2_FLAG_NONE, stream_id, + NGHTTP2_INTERNAL_ERROR); + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + return 0; +} + +static int before_frame_send_cb(nghttp2_session *ngh2, + const nghttp2_frame *frame, + void *userp) +{ + h2_session *session = (h2_session *)userp; + (void)ngh2; + + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (APLOGctrace2(session->c)) { + char buffer[256]; + frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_session(%ld): before_frame_send %s", + session->id, buffer); + } + return 0; +} + +static int on_frame_send_cb(nghttp2_session *ngh2, + const nghttp2_frame *frame, + void *userp) +{ + h2_session *session = (h2_session *)userp; + (void)ngh2; (void)frame; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_session(%ld): on_frame_send", session->id); + return 0; +} + +static int on_frame_not_send_cb(nghttp2_session *ngh2, + const nghttp2_frame *frame, + int lib_error_code, void *userp) +{ + h2_session *session = (h2_session *)userp; + (void)ngh2; + + if (APLOGctrace2(session->c)) { + char buffer[256]; + + frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_session: callback on_frame_not_send error=%d %s", + lib_error_code, buffer); + } + return 0; +} + +static apr_status_t stream_destroy(h2_session *session, h2_stream *stream) +{ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_stream(%ld-%d): closing", session->id, (int)stream->id); + h2_stream_set_remove(session->streams, stream); + return h2_mplx_cleanup_stream(session->mplx, stream); +} + +static int on_stream_close_cb(nghttp2_session *ngh2, int32_t stream_id, + uint32_t error_code, void *userp) +{ + h2_session *session = (h2_session *)userp; + h2_stream *stream; + + (void)ngh2; + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + stream = h2_stream_set_get(session->streams, stream_id); + if (stream) { + stream_destroy(session, stream); + } + + if (error_code) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_stream(%ld-%d): close error %d", + session->id, (int)stream_id, error_code); + } + + return 0; +} + +static int on_begin_headers_cb(nghttp2_session *ngh2, + const nghttp2_frame *frame, void *userp) +{ + /* This starts a new stream. */ + int rv; + (void)ngh2; + rv = stream_open((h2_session *)userp, frame->hd.stream_id); + if (rv != NGHTTP2_ERR_CALLBACK_FAILURE) { + /* on_header_cb or on_frame_recv_cb will dectect that stream + does not exist and submit RST_STREAM. */ + return 0; + } + return NGHTTP2_ERR_CALLBACK_FAILURE; +} + +static int on_header_cb(nghttp2_session *ngh2, const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, + uint8_t flags, + void *userp) +{ + h2_session *session = (h2_session *)userp; + h2_stream * stream; + apr_status_t status; + + (void)ngh2; + (void)flags; + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + stream = h2_stream_set_get(session->streams, + frame->hd.stream_id); + if (!stream) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, + APLOGNO(02920) + "h2_session: stream(%ld-%d): on_header for unknown stream", + session->id, (int)frame->hd.stream_id); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + status = h2_stream_write_header(stream, + (const char *)name, namelen, + (const char *)value, valuelen); + if (status != APR_SUCCESS) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; +} + +/** + * nghttp2 session has received a complete frame. Most, it uses + * for processing of internal state. HEADER and DATA frames however + * we need to handle ourself. + */ +static int on_frame_recv_cb(nghttp2_session *ng2s, + const nghttp2_frame *frame, + void *userp) +{ + int rv; + h2_session *session = (h2_session *)userp; + apr_status_t status = APR_SUCCESS; + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + ++session->frames_received; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_session(%ld): on_frame_rcv #%ld, type=%d", session->id, + (long)session->frames_received, frame->hd.type); + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + int eos; + h2_stream * stream = h2_stream_set_get(session->streams, + frame->hd.stream_id); + if (stream == NULL) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, + APLOGNO(02921) + "h2_session: stream(%ld-%d): HEADERS frame " + "for unknown stream", session->id, + (int)frame->hd.stream_id); + rv = nghttp2_submit_rst_stream(ng2s, NGHTTP2_FLAG_NONE, + frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + + eos = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM); + status = stream_end_headers(session, stream, eos); + + break; + } + case NGHTTP2_DATA: { + h2_stream * stream = h2_stream_set_get(session->streams, + frame->hd.stream_id); + if (stream == NULL) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, + APLOGNO(02922) + "h2_session: stream(%ld-%d): DATA frame " + "for unknown stream", session->id, + (int)frame->hd.stream_id); + rv = nghttp2_submit_rst_stream(ng2s, NGHTTP2_FLAG_NONE, + frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + break; + } + case NGHTTP2_PRIORITY: { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_session: stream(%ld-%d): PRIORITY frame " + " weight=%d, dependsOn=%d, exclusive=%d", + session->id, (int)frame->hd.stream_id, + frame->priority.pri_spec.weight, + frame->priority.pri_spec.stream_id, + frame->priority.pri_spec.exclusive); + break; + } + default: + if (APLOGctrace2(session->c)) { + char buffer[256]; + + frame_print(frame, buffer, + sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_session: on_frame_rcv %s", buffer); + } + break; + } + + /* only DATA and HEADERS frame can bear END_STREAM flag. Other + frame types may have other flag which has the same value, so we + have to check the frame type first. */ + if ((frame->hd.type == NGHTTP2_DATA || frame->hd.type == NGHTTP2_HEADERS) && + frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + h2_stream * stream = h2_stream_set_get(session->streams, + frame->hd.stream_id); + if (stream != NULL) { + status = h2_stream_write_eos(stream); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, + "h2_stream(%ld-%d): input closed", + session->id, (int)frame->hd.stream_id); + } + } + + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + APLOGNO(02923) + "h2_session: stream(%ld-%d): error handling frame", + session->id, (int)frame->hd.stream_id); + rv = nghttp2_submit_rst_stream(ng2s, NGHTTP2_FLAG_NONE, + frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + + return 0; +} + +static apr_status_t send_data(h2_session *session, const char *data, + apr_size_t length) +{ + return h2_conn_io_write(&session->io, data, length); +} + +static apr_status_t pass_data(void *ctx, + const char *data, apr_size_t length) +{ + return send_data((h2_session*)ctx, data, length); +} + +static int on_send_data_cb(nghttp2_session *ngh2, + nghttp2_frame *frame, + const uint8_t *framehd, + size_t length, + nghttp2_data_source *source, + void *userp) +{ + apr_status_t status = APR_SUCCESS; + h2_session *session = (h2_session *)userp; + int stream_id = (int)frame->hd.stream_id; + const unsigned char padlen = frame->data.padlen; + int eos; + h2_stream *stream; + + (void)ngh2; + (void)source; + if (session->aborted) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + stream = h2_stream_set_get(session->streams, stream_id); + if (!stream) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_NOTFOUND, session->c, + APLOGNO(02924) + "h2_stream(%ld-%d): send_data", + session->id, (int)stream_id); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + status = send_data(session, (const char *)framehd, 9); + if (status == APR_SUCCESS) { + if (padlen) { + status = send_data(session, (const char *)&padlen, 1); + } + + if (status == APR_SUCCESS) { + apr_size_t len = length; + status = h2_stream_readx(stream, pass_data, session, + &len, &eos); + if (status == APR_SUCCESS && len != length) { + status = APR_EINVAL; + } + } + + if (status == APR_SUCCESS && padlen) { + if (padlen) { + char pad[256]; + memset(pad, 0, padlen); + status = send_data(session, pad, padlen); + } + } + } + + if (status == APR_SUCCESS) { + return 0; + } + else if (status != APR_EOF) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + APLOGNO(02925) + "h2_stream(%ld-%d): failed send_data_cb", + session->id, (int)stream_id); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return h2_session_status_from_apr_status(status); +} + + +#define NGH2_SET_CALLBACK(callbacks, name, fn)\ +nghttp2_session_callbacks_set_##name##_callback(callbacks, fn) + +static apr_status_t init_callbacks(conn_rec *c, nghttp2_session_callbacks **pcb) +{ + int rv = nghttp2_session_callbacks_new(pcb); + if (rv != 0) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, + APLOGNO(02926) "nghttp2_session_callbacks_new: %s", + nghttp2_strerror(rv)); + return APR_EGENERAL; + } + + NGH2_SET_CALLBACK(*pcb, send, send_cb); + NGH2_SET_CALLBACK(*pcb, on_frame_recv, on_frame_recv_cb); + NGH2_SET_CALLBACK(*pcb, on_invalid_frame_recv, on_invalid_frame_recv_cb); + NGH2_SET_CALLBACK(*pcb, on_data_chunk_recv, on_data_chunk_recv_cb); + NGH2_SET_CALLBACK(*pcb, before_frame_send, before_frame_send_cb); + NGH2_SET_CALLBACK(*pcb, on_frame_send, on_frame_send_cb); + NGH2_SET_CALLBACK(*pcb, on_frame_not_send, on_frame_not_send_cb); + NGH2_SET_CALLBACK(*pcb, on_stream_close, on_stream_close_cb); + NGH2_SET_CALLBACK(*pcb, on_begin_headers, on_begin_headers_cb); + NGH2_SET_CALLBACK(*pcb, on_header, on_header_cb); + NGH2_SET_CALLBACK(*pcb, send_data, on_send_data_cb); + + return APR_SUCCESS; +} + +static h2_session *h2_session_create_int(conn_rec *c, + request_rec *r, + h2_config *config, + h2_workers *workers) +{ + nghttp2_session_callbacks *callbacks = NULL; + nghttp2_option *options = NULL; + + apr_pool_t *pool = NULL; + apr_status_t status = apr_pool_create(&pool, r? r->pool : c->pool); + h2_session *session; + if (status != APR_SUCCESS) { + return NULL; + } + + session = apr_pcalloc(pool, sizeof(h2_session)); + if (session) { + int rv; + session->id = c->id; + session->c = c; + session->r = r; + + session->max_stream_count = h2_config_geti(config, H2_CONF_MAX_STREAMS); + session->max_stream_mem = h2_config_geti(config, H2_CONF_STREAM_MAX_MEM); + + session->pool = pool; + + status = apr_thread_cond_create(&session->iowait, session->pool); + if (status != APR_SUCCESS) { + return NULL; + } + + session->streams = h2_stream_set_create(session->pool); + + session->workers = workers; + session->mplx = h2_mplx_create(c, session->pool, workers); + + h2_conn_io_init(&session->io, c); + session->bbtmp = apr_brigade_create(session->pool, c->bucket_alloc); + + status = init_callbacks(c, &callbacks); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, c, APLOGNO(02927) + "nghttp2: error in init_callbacks"); + h2_session_destroy(session); + return NULL; + } + + rv = nghttp2_option_new(&options); + if (rv != 0) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, c, + APLOGNO(02928) "nghttp2_option_new: %s", + nghttp2_strerror(rv)); + h2_session_destroy(session); + return NULL; + } + + nghttp2_option_set_peer_max_concurrent_streams(options, + (uint32_t)session->max_stream_count); + + /* We need to handle window updates ourself, otherwise we + * get flooded by nghttp2. */ + nghttp2_option_set_no_auto_window_update(options, 1); + + rv = nghttp2_session_server_new2(&session->ngh2, callbacks, + session, options); + nghttp2_session_callbacks_del(callbacks); + nghttp2_option_del(options); + + if (rv != 0) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, c, + APLOGNO(02929) "nghttp2_session_server_new: %s", + nghttp2_strerror(rv)); + h2_session_destroy(session); + return NULL; + } + + } + return session; +} + +h2_session *h2_session_create(conn_rec *c, h2_config *config, + h2_workers *workers) +{ + return h2_session_create_int(c, NULL, config, workers); +} + +h2_session *h2_session_rcreate(request_rec *r, h2_config *config, + h2_workers *workers) +{ + return h2_session_create_int(r->connection, r, config, workers); +} + +void h2_session_destroy(h2_session *session) +{ + AP_DEBUG_ASSERT(session); + if (session->mplx) { + h2_mplx_release_and_join(session->mplx, session->iowait); + session->mplx = NULL; + } + if (session->streams) { + if (h2_stream_set_size(session->streams)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_session(%ld): destroy, %d streams open", + session->id, (int)h2_stream_set_size(session->streams)); + } + h2_stream_set_destroy(session->streams); + session->streams = NULL; + } + if (session->ngh2) { + nghttp2_session_del(session->ngh2); + session->ngh2 = NULL; + } + h2_conn_io_destroy(&session->io); + + if (session->iowait) { + apr_thread_cond_destroy(session->iowait); + session->iowait = NULL; + } + + if (session->pool) { + apr_pool_destroy(session->pool); + } +} + +apr_status_t h2_session_goaway(h2_session *session, apr_status_t reason) +{ + apr_status_t status = APR_SUCCESS; + int rv; + AP_DEBUG_ASSERT(session); + if (session->aborted) { + return APR_EINVAL; + } + + rv = 0; + if (reason == APR_SUCCESS) { + rv = nghttp2_submit_shutdown_notice(session->ngh2); + } + else { + int err = 0; + int last_id = nghttp2_session_get_last_proc_stream_id(session->ngh2); + rv = nghttp2_submit_goaway(session->ngh2, last_id, + NGHTTP2_FLAG_NONE, err, NULL, 0); + } + if (rv != 0) { + status = APR_EGENERAL; + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + APLOGNO(02930) "session(%ld): submit goaway: %s", + session->id, nghttp2_strerror(rv)); + } + return status; +} + +static apr_status_t h2_session_abort_int(h2_session *session, int reason) +{ + AP_DEBUG_ASSERT(session); + if (!session->aborted) { + session->aborted = 1; + if (session->ngh2) { + if (reason) { + ap_log_cerror(APLOG_MARK, (reason == NGHTTP2_ERR_EOF)? + APLOG_DEBUG : APLOG_INFO, 0, session->c, + "session(%ld): aborting session, reason=%d %s", + session->id, reason, nghttp2_strerror(reason)); + } + nghttp2_session_terminate_session(session->ngh2, reason); + nghttp2_submit_goaway(session->ngh2, 0, 0, reason, NULL, 0); + nghttp2_session_send(session->ngh2); + h2_conn_io_flush(&session->io); + } + h2_mplx_abort(session->mplx); + } + return APR_SUCCESS; +} + +apr_status_t h2_session_abort(h2_session *session, apr_status_t reason, int rv) +{ + AP_DEBUG_ASSERT(session); + if (rv == 0) { + rv = NGHTTP2_ERR_PROTO; + switch (reason) { + case APR_ENOMEM: + rv = NGHTTP2_ERR_NOMEM; + break; + case APR_EOF: + rv = 0; + break; + case APR_EBADF: + case APR_ECONNABORTED: + rv = NGHTTP2_ERR_EOF; + break; + default: + break; + } + } + return h2_session_abort_int(session, rv); +} + +apr_status_t h2_session_start(h2_session *session, int *rv) +{ + apr_status_t status = APR_SUCCESS; + h2_config *config; + nghttp2_settings_entry settings[3]; + + AP_DEBUG_ASSERT(session); + /* Start the conversation by submitting our SETTINGS frame */ + *rv = 0; + config = h2_config_get(session->c); + if (session->r) { + const char *s, *cs; + apr_size_t dlen; + h2_stream * stream; + + /* better for vhost matching */ + config = h2_config_rget(session->r); + + /* 'h2c' mode: we should have a 'HTTP2-Settings' header with + * base64 encoded client settings. */ + s = apr_table_get(session->r->headers_in, "HTTP2-Settings"); + if (!s) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EINVAL, session->r, + APLOGNO(02931) + "HTTP2-Settings header missing in request"); + return APR_EINVAL; + } + cs = NULL; + dlen = h2_util_base64url_decode(&cs, s, session->pool); + + if (APLOGrdebug(session->r)) { + char buffer[128]; + h2_util_hex_dump(buffer, 128, (char*)cs, dlen); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, session->r, + "upgrading h2c session with HTTP2-Settings: %s -> %s (%d)", + s, buffer, (int)dlen); + } + + *rv = nghttp2_session_upgrade(session->ngh2, (uint8_t*)cs, dlen, NULL); + if (*rv != 0) { + status = APR_EINVAL; + ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, + APLOGNO(02932) "nghttp2_session_upgrade: %s", + nghttp2_strerror(*rv)); + return status; + } + + /* Now we need to auto-open stream 1 for the request we got. */ + *rv = stream_open(session, 1); + if (*rv != 0) { + status = APR_EGENERAL; + ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, + APLOGNO(02933) "open stream 1: %s", + nghttp2_strerror(*rv)); + return status; + } + + stream = h2_stream_set_get(session->streams, 1); + if (stream == NULL) { + status = APR_EGENERAL; + ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, + APLOGNO(02934) "lookup of stream 1"); + return status; + } + + status = h2_stream_rwrite(stream, session->r); + if (status != APR_SUCCESS) { + return status; + } + status = stream_end_headers(session, stream, 1); + if (status != APR_SUCCESS) { + return status; + } + } + + settings[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + settings[0].value = (uint32_t)session->max_stream_count; + settings[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + settings[1].value = h2_config_geti(config, H2_CONF_WIN_SIZE); + settings[2].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE; + settings[2].value = 64*1024; + + *rv = nghttp2_submit_settings(session->ngh2, NGHTTP2_FLAG_NONE, + settings, + sizeof(settings)/sizeof(settings[0])); + if (*rv != 0) { + status = APR_EGENERAL; + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + APLOGNO(02935) "nghttp2_submit_settings: %s", + nghttp2_strerror(*rv)); + } + + return status; +} + +static int h2_session_want_write(h2_session *session) +{ + return nghttp2_session_want_write(session->ngh2); +} + +typedef struct { + h2_session *session; + int resume_count; +} resume_ctx; + +static int resume_on_data(void *ctx, h2_stream *stream) { + resume_ctx *rctx = (resume_ctx*)ctx; + h2_session *session = rctx->session; + AP_DEBUG_ASSERT(session); + AP_DEBUG_ASSERT(stream); + + if (h2_stream_is_suspended(stream)) { + if (h2_mplx_out_has_data_for(stream->m, stream->id)) { + int rv; + h2_stream_set_suspended(stream, 0); + ++rctx->resume_count; + + rv = nghttp2_session_resume_data(session->ngh2, stream->id); + ap_log_cerror(APLOG_MARK, nghttp2_is_fatal(rv)? + APLOG_ERR : APLOG_DEBUG, 0, session->c, + APLOGNO(02936) + "h2_stream(%ld-%d): resuming stream %s", + session->id, stream->id, nghttp2_strerror(rv)); + } + } + return 1; +} + +static int h2_session_resume_streams_with_data(h2_session *session) { + AP_DEBUG_ASSERT(session); + if (!h2_stream_set_is_empty(session->streams) + && session->mplx && !session->aborted) { + resume_ctx ctx; + + ctx.session = session; + ctx.resume_count = 0; + + /* Resume all streams where we have data in the out queue and + * which had been suspended before. */ + h2_stream_set_iter(session->streams, resume_on_data, &ctx); + return ctx.resume_count; + } + return 0; +} + +static void update_window(void *ctx, int stream_id, apr_size_t bytes_read) +{ + h2_session *session = (h2_session*)ctx; + nghttp2_session_consume(session->ngh2, stream_id, bytes_read); +} + +static apr_status_t h2_session_update_windows(h2_session *session) +{ + return h2_mplx_in_update_windows(session->mplx, update_window, session); +} + +apr_status_t h2_session_write(h2_session *session, apr_interval_time_t timeout) +{ + apr_status_t status = APR_EAGAIN; + h2_stream *stream = NULL; + int flush_output = 0; + + AP_DEBUG_ASSERT(session); + + /* Check that any pending window updates are sent. */ + status = h2_session_update_windows(session); + if (status == APR_SUCCESS) { + flush_output = 1; + } + else if (status != APR_EAGAIN) { + return status; + } + + if (h2_session_want_write(session)) { + int rv; + status = APR_SUCCESS; + rv = nghttp2_session_send(session->ngh2); + if (rv != 0) { + ap_log_cerror( APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_session: send: %s", nghttp2_strerror(rv)); + if (nghttp2_is_fatal(rv)) { + h2_session_abort_int(session, rv); + status = APR_ECONNABORTED; + } + } + flush_output = 1; + } + + /* If we have responses ready, submit them now. */ + while ((stream = h2_mplx_next_submit(session->mplx, + session->streams)) != NULL) { + status = h2_session_handle_response(session, stream); + flush_output = 1; + } + + if (h2_session_resume_streams_with_data(session) > 0) { + flush_output = 1; + } + + if (!flush_output && timeout > 0 && !h2_session_want_write(session)) { + status = h2_mplx_out_trywait(session->mplx, timeout, session->iowait); + + if (status != APR_TIMEUP + && h2_session_resume_streams_with_data(session) > 0) { + flush_output = 1; + } + else { + /* nothing happened to ongoing streams, do some house-keeping */ + } + } + + if (h2_session_want_write(session)) { + int rv; + status = APR_SUCCESS; + rv = nghttp2_session_send(session->ngh2); + if (rv != 0) { + ap_log_cerror( APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_session: send2: %s", nghttp2_strerror(rv)); + if (nghttp2_is_fatal(rv)) { + h2_session_abort_int(session, rv); + status = APR_ECONNABORTED; + } + } + flush_output = 1; + } + + if (flush_output) { + h2_conn_io_flush(&session->io); + } + + return status; +} + +h2_stream *h2_session_get_stream(h2_session *session, int stream_id) +{ + AP_DEBUG_ASSERT(session); + return h2_stream_set_get(session->streams, stream_id); +} + +/* h2_io_on_read_cb implementation that offers the data read + * directly to the session for consumption. + */ +static apr_status_t session_receive(const char *data, apr_size_t len, + apr_size_t *readlen, int *done, + void *puser) +{ + h2_session *session = (h2_session *)puser; + AP_DEBUG_ASSERT(session); + if (len > 0) { + ssize_t n = nghttp2_session_mem_recv(session->ngh2, + (const uint8_t *)data, len); + if (n < 0) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, APR_EGENERAL, + session->c, + "h2_session: nghttp2_session_mem_recv error %d", + (int)n); + if (nghttp2_is_fatal((int)n)) { + *done = 1; + h2_session_abort_int(session, (int)n); + return APR_EGENERAL; + } + } + else { + *readlen = n; + } + } + return APR_SUCCESS; +} + +apr_status_t h2_session_read(h2_session *session, apr_read_type_e block) +{ + AP_DEBUG_ASSERT(session); + return h2_conn_io_read(&session->io, block, session_receive, session); +} + +apr_status_t h2_session_close(h2_session *session) +{ + AP_DEBUG_ASSERT(session); + return h2_conn_io_flush(&session->io); +} + +/* The session wants to send more DATA for the given stream. + */ +static ssize_t stream_data_cb(nghttp2_session *ng2s, + int32_t stream_id, + uint8_t *buf, + size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, + void *puser) +{ + h2_session *session = (h2_session *)puser; + apr_size_t nread = length; + int eos = 0; + apr_status_t status; + h2_stream *stream; + AP_DEBUG_ASSERT(session); + + (void)ng2s; + (void)buf; + (void)source; + stream = h2_stream_set_get(session->streams, stream_id); + if (!stream) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_NOTFOUND, session->c, + APLOGNO(02937) + "h2_stream(%ld-%d): data requested but stream not found", + session->id, (int)stream_id); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + AP_DEBUG_ASSERT(!h2_stream_is_suspended(stream)); + + status = h2_stream_prep_read(stream, &nread, &eos); + if (nread) { + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + } + + switch (status) { + case APR_SUCCESS: + break; + + case APR_EAGAIN: + /* If there is no data available, our session will automatically + * suspend this stream and not ask for more data until we resume + * it. Remember at our h2_stream that we need to do this. + */ + nread = 0; + h2_stream_set_suspended(stream, 1); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_stream(%ld-%d): suspending stream", + session->id, (int)stream_id); + return NGHTTP2_ERR_DEFERRED; + + case APR_EOF: + nread = 0; + eos = 1; + break; + + default: + nread = 0; + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + APLOGNO(02938) "h2_stream(%ld-%d): reading data", + session->id, (int)stream_id); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + if (eos) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + + return (ssize_t)nread; +} + +typedef struct { + nghttp2_nv *nv; + size_t nvlen; + size_t offset; +} nvctx_t; + +static int submit_response(h2_session *session, h2_response *response) +{ + nghttp2_data_provider provider; + int rv; + + memset(&provider, 0, sizeof(provider)); + provider.source.fd = response->stream_id; + provider.read_callback = stream_data_cb; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_stream(%ld-%d): submitting response %s", + session->id, response->stream_id, response->status); + + rv = nghttp2_submit_response(session->ngh2, response->stream_id, + response->ngheader->nv, + response->ngheader->nvlen, &provider); + + if (rv != 0) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, + APLOGNO(02939) "h2_stream(%ld-%d): submit_response: %s", + session->id, response->stream_id, nghttp2_strerror(rv)); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_stream(%ld-%d): submitted response %s, rv=%d", + session->id, response->stream_id, + response->status, rv); + } + return rv; +} + +/* Start submitting the response to a stream request. This is possible + * once we have all the response headers. The response body will be + * read by the session using the callback we supply. + */ +apr_status_t h2_session_handle_response(h2_session *session, h2_stream *stream) +{ + apr_status_t status = APR_SUCCESS; + int rv = 0; + AP_DEBUG_ASSERT(session); + AP_DEBUG_ASSERT(stream); + AP_DEBUG_ASSERT(stream->response); + + if (stream->response->ngheader) { + rv = submit_response(session, stream->response); + } + else { + rv = nghttp2_submit_rst_stream(session->ngh2, NGHTTP2_FLAG_NONE, + stream->id, NGHTTP2_PROTOCOL_ERROR); + } + + if (nghttp2_is_fatal(rv)) { + status = APR_EGENERAL; + h2_session_abort_int(session, rv); + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + APLOGNO(02940) "submit_response: %s", + nghttp2_strerror(rv)); + } + return status; +} + +int h2_session_is_done(h2_session *session) +{ + AP_DEBUG_ASSERT(session); + return (session->aborted + || !session->ngh2 + || (!nghttp2_session_want_read(session->ngh2) + && !nghttp2_session_want_write(session->ngh2))); +} + +static int log_stream(void *ctx, h2_stream *stream) +{ + h2_session *session = (h2_session *)ctx; + AP_DEBUG_ASSERT(session); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_stream(%ld-%d): in set, suspended=%d, aborted=%d, " + "has_data=%d", + session->id, stream->id, stream->suspended, stream->aborted, + h2_mplx_out_has_data_for(session->mplx, stream->id)); + return 1; +} + +void h2_session_log_stats(h2_session *session) +{ + AP_DEBUG_ASSERT(session); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_session(%ld): %d open streams", + session->id, (int)h2_stream_set_size(session->streams)); + h2_stream_set_iter(session->streams, log_stream, session); +} + +static int frame_print(const nghttp2_frame *frame, char *buffer, size_t maxlen) +{ + char scratch[128]; + size_t s_len = sizeof(scratch)/sizeof(scratch[0]); + + switch (frame->hd.type) { + case NGHTTP2_DATA: { + return apr_snprintf(buffer, maxlen, + "DATA[length=%d, flags=%d, stream=%d, padlen=%d]", + (int)frame->hd.length, frame->hd.flags, + frame->hd.stream_id, (int)frame->data.padlen); + } + case NGHTTP2_HEADERS: { + return apr_snprintf(buffer, maxlen, + "HEADERS[length=%d, hend=%d, stream=%d, eos=%d]", + (int)frame->hd.length, + !!(frame->hd.flags & NGHTTP2_FLAG_END_HEADERS), + frame->hd.stream_id, + !!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)); + } + case NGHTTP2_PRIORITY: { + return apr_snprintf(buffer, maxlen, + "PRIORITY[length=%d, flags=%d, stream=%d]", + (int)frame->hd.length, + frame->hd.flags, frame->hd.stream_id); + } + case NGHTTP2_RST_STREAM: { + return apr_snprintf(buffer, maxlen, + "RST_STREAM[length=%d, flags=%d, stream=%d]", + (int)frame->hd.length, + frame->hd.flags, frame->hd.stream_id); + } + case NGHTTP2_SETTINGS: { + if (frame->hd.flags & NGHTTP2_FLAG_ACK) { + return apr_snprintf(buffer, maxlen, + "SETTINGS[ack=1, stream=%d]", + frame->hd.stream_id); + } + return apr_snprintf(buffer, maxlen, + "SETTINGS[length=%d, stream=%d]", + (int)frame->hd.length, frame->hd.stream_id); + } + case NGHTTP2_PUSH_PROMISE: { + return apr_snprintf(buffer, maxlen, + "PUSH_PROMISE[length=%d, hend=%d, stream=%d]", + (int)frame->hd.length, + !!(frame->hd.flags & NGHTTP2_FLAG_END_HEADERS), + frame->hd.stream_id); + } + case NGHTTP2_PING: { + return apr_snprintf(buffer, maxlen, + "PING[length=%d, ack=%d, stream=%d]", + (int)frame->hd.length, + frame->hd.flags&NGHTTP2_FLAG_ACK, + frame->hd.stream_id); + } + case NGHTTP2_GOAWAY: { + size_t len = (frame->goaway.opaque_data_len < s_len)? + frame->goaway.opaque_data_len : s_len-1; + memcpy(scratch, frame->goaway.opaque_data, len); + scratch[len+1] = '\0'; + return apr_snprintf(buffer, maxlen, "GOAWAY[error=%d, reason='%s']", + frame->goaway.error_code, scratch); + } + case NGHTTP2_WINDOW_UPDATE: { + return apr_snprintf(buffer, maxlen, + "WINDOW_UPDATE[length=%d, stream=%d]", + (int)frame->hd.length, frame->hd.stream_id); + } + default: + return apr_snprintf(buffer, maxlen, + "FRAME[type=%d, length=%d, flags=%d, stream=%d]", + frame->hd.type, (int)frame->hd.length, + frame->hd.flags, frame->hd.stream_id); + } +} + diff --git a/modules/http2/h2_session.h b/modules/http2/h2_session.h new file mode 100644 index 00000000000..77dd440a55e --- /dev/null +++ b/modules/http2/h2_session.h @@ -0,0 +1,137 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_session__ +#define __mod_h2__h2_session__ + +#include "h2_conn_io.h" + +/** + * A HTTP/2 connection, a session with a specific client. + * + * h2_session sits on top of a httpd conn_rec* instance and takes complete + * control of the connection data. It receives protocol frames from the + * client. For new HTTP/2 streams it creates h2_task(s) that are sent + * via callback to a dispatcher (see h2_conn.c). + * h2_session keeps h2_io's for each ongoing stream which buffer the + * payload for that stream. + * + * New incoming HEADER frames are converted into a h2_stream+h2_task instance + * that both represent a HTTP/2 stream, but may have separate lifetimes. This + * allows h2_task to be scheduled in other threads without semaphores + * all over the place. It allows task memory to be freed independant of + * session lifetime and sessions may close down while tasks are still running. + * + * + */ + +struct apr_thread_mutext_t; +struct apr_thread_cond_t; +struct h2_config; +struct h2_mplx; +struct h2_response; +struct h2_session; +struct h2_stream; +struct h2_task; +struct h2_workers; + +struct nghttp2_session; + +typedef struct h2_session h2_session; + +struct h2_session { + long id; /* identifier of this session, unique + * inside a httpd process */ + conn_rec *c; /* the connection this session serves */ + request_rec *r; /* the request that started this in case + * of 'h2c', NULL otherwise */ + int aborted; /* this session is being aborted */ + apr_size_t frames_received; /* number of http/2 frames received */ + apr_size_t max_stream_count; /* max number of open streams */ + apr_size_t max_stream_mem; /* max buffer memory for a single stream */ + + apr_pool_t *pool; /* pool to use in session handling */ + apr_bucket_brigade *bbtmp; /* brigade for keeping temporary data */ + struct apr_thread_cond_t *iowait; /* our cond when trywaiting for data */ + + h2_conn_io io; /* io on httpd conn filters */ + struct h2_mplx *mplx; /* multiplexer for stream data */ + + struct h2_stream_set *streams; /* streams handled by this session */ + + struct nghttp2_session *ngh2; /* the nghttp2 session (internal use) */ + struct h2_workers *workers; /* for executing stream tasks */ +}; + + +/* Create a new h2_session for the given connection (mode 'h2'). + * The session will apply the configured parameter. + */ +h2_session *h2_session_create(conn_rec *c, struct h2_config *cfg, + struct h2_workers *workers); + +/* Create a new h2_session for the given request (mode 'h2c'). + * The session will apply the configured parameter. + */ +h2_session *h2_session_rcreate(request_rec *r, struct h2_config *cfg, + struct h2_workers *workers); + +/* Destroy the session and all object it still contains. This will not + * destroy h2_task instances that not finished yet. */ +void h2_session_destroy(h2_session *session); + +/* Called once at start of session. Performs initial client thingies. */ +apr_status_t h2_session_start(h2_session *session, int *rv); + +/* Return != 0 iff session is finished and connection can be closed. + */ +int h2_session_is_done(h2_session *session); + +/* Called when the session will shutdown after all open streams + * are handled. New streams will no longer be accepted. + * Call with reason APR_SUCCESS to initiate a graceful shutdown. */ +apr_status_t h2_session_goaway(h2_session *session, apr_status_t reason); + +/* Called when an error occured and the session needs to shut down. + * Status indicates the reason of the error. */ +apr_status_t h2_session_abort(h2_session *session, apr_status_t reason, int rv); + +/* Called before a session gets destroyed, might flush output etc. */ +apr_status_t h2_session_close(h2_session *session); + +/* Read more data from the client connection. Used normally with blocking + * APR_NONBLOCK_READ, which will return APR_EAGAIN when no data is available. + * Use with APR_BLOCK_READ only when certain that no data needs to be written + * while waiting. */ +apr_status_t h2_session_read(h2_session *session, apr_read_type_e block); + +/* Write data out to the client, if there is any. Otherwise, wait for + * a maximum of timeout micro-seconds and return to the caller. If timeout + * occurred, APR_TIMEUP will be returned. + */ +apr_status_t h2_session_write(h2_session *session, + apr_interval_time_t timeout); + +/* Start submitting the response to a stream request. This is possible + * once we have all the response headers. */ +apr_status_t h2_session_handle_response(h2_session *session, + struct h2_stream *stream); + +/* Get the h2_stream for the given stream idenrtifier. */ +struct h2_stream *h2_session_get_stream(h2_session *session, int stream_id); + +void h2_session_log_stats(h2_session *session); + +#endif /* defined(__mod_h2__h2_session__) */ diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c new file mode 100644 index 00000000000..08b8f5a3f84 --- /dev/null +++ b/modules/http2/h2_stream.c @@ -0,0 +1,273 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#define APR_POOL_DEBUG 7 + +#include +#include +#include +#include + +#include + +#include "h2_private.h" +#include "h2_conn.h" +#include "h2_mplx.h" +#include "h2_request.h" +#include "h2_response.h" +#include "h2_stream.h" +#include "h2_task.h" +#include "h2_ctx.h" +#include "h2_task_input.h" +#include "h2_task.h" +#include "h2_util.h" + + +static void set_state(h2_stream *stream, h2_stream_state_t state) +{ + AP_DEBUG_ASSERT(stream); + if (stream->state != state) { + stream->state = state; + } +} + +h2_stream *h2_stream_create(int id, apr_pool_t *pool, struct h2_mplx *m) +{ + h2_stream *stream = apr_pcalloc(pool, sizeof(h2_stream)); + if (stream != NULL) { + stream->id = id; + stream->state = H2_STREAM_ST_IDLE; + stream->pool = pool; + stream->m = m; + stream->request = h2_request_create(id, pool, m->c->bucket_alloc); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, + "h2_stream(%ld-%d): created", m->id, stream->id); + } + return stream; +} + +void h2_stream_cleanup(h2_stream *stream) +{ + if (stream->request) { + h2_request_destroy(stream->request); + stream->request = NULL; + } +} + +apr_status_t h2_stream_destroy(h2_stream *stream) +{ + AP_DEBUG_ASSERT(stream); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->m->c, + "h2_stream(%ld-%d): destroy", stream->m->id, stream->id); + h2_stream_cleanup(stream); + + if (stream->task) { + h2_task_destroy(stream->task); + stream->task = NULL; + } + if (stream->pool) { + apr_pool_destroy(stream->pool); + } + return APR_SUCCESS; +} + +void h2_stream_attach_pool(h2_stream *stream, apr_pool_t *pool) +{ + stream->pool = pool; +} + +apr_pool_t *h2_stream_detach_pool(h2_stream *stream) +{ + apr_pool_t *pool = stream->pool; + stream->pool = NULL; + return pool; +} + +void h2_stream_abort(h2_stream *stream) +{ + AP_DEBUG_ASSERT(stream); + stream->aborted = 1; +} + +apr_status_t h2_stream_set_response(h2_stream *stream, h2_response *response, + apr_bucket_brigade *bb) +{ + stream->response = response; + if (bb && !APR_BRIGADE_EMPTY(bb)) { + if (!stream->bbout) { + stream->bbout = apr_brigade_create(stream->pool, + stream->m->c->bucket_alloc); + } + return h2_util_move(stream->bbout, bb, 16 * 1024, NULL, + "h2_stream_set_response"); + } + return APR_SUCCESS; +} + +static int set_closed(h2_stream *stream) +{ + switch (stream->state) { + case H2_STREAM_ST_CLOSED_INPUT: + case H2_STREAM_ST_CLOSED: + return 0; /* ignore, idempotent */ + case H2_STREAM_ST_CLOSED_OUTPUT: + /* both closed now */ + set_state(stream, H2_STREAM_ST_CLOSED); + break; + default: + /* everything else we jump to here */ + set_state(stream, H2_STREAM_ST_CLOSED_INPUT); + break; + } + return 1; +} + +apr_status_t h2_stream_rwrite(h2_stream *stream, request_rec *r) +{ + apr_status_t status; + AP_DEBUG_ASSERT(stream); + set_state(stream, H2_STREAM_ST_OPEN); + status = h2_request_rwrite(stream->request, r, stream->m); + return status; +} + +apr_status_t h2_stream_write_eoh(h2_stream *stream, int eos) +{ + apr_status_t status; + AP_DEBUG_ASSERT(stream); + + /* Seeing the end-of-headers, we have everything we need to + * start processing it. + */ + status = h2_mplx_create_task(stream->m, stream); + if (status == APR_SUCCESS) { + status = h2_request_end_headers(stream->request, + stream->m, stream->task, eos); + if (status == APR_SUCCESS) { + status = h2_mplx_do_task(stream->m, stream->task); + } + if (eos) { + status = h2_stream_write_eos(stream); + } + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, stream->m->c, + "h2_mplx(%ld-%d): start stream, task %s %s (%s)", + stream->m->id, stream->id, + stream->request->method, stream->request->path, + stream->request->authority); + + } + return status; +} + +apr_status_t h2_stream_write_eos(h2_stream *stream) +{ + AP_DEBUG_ASSERT(stream); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->m->c, + "h2_stream(%ld-%d): closing input", + stream->m->id, stream->id); + if (set_closed(stream)) { + return h2_request_close(stream->request); + } + return APR_SUCCESS; +} + +apr_status_t h2_stream_write_header(h2_stream *stream, + const char *name, size_t nlen, + const char *value, size_t vlen) +{ + AP_DEBUG_ASSERT(stream); + switch (stream->state) { + case H2_STREAM_ST_IDLE: + set_state(stream, H2_STREAM_ST_OPEN); + break; + case H2_STREAM_ST_OPEN: + break; + default: + return APR_EINVAL; + } + return h2_request_write_header(stream->request, name, nlen, + value, vlen, stream->m); +} + +apr_status_t h2_stream_write_data(h2_stream *stream, + const char *data, size_t len) +{ + AP_DEBUG_ASSERT(stream); + AP_DEBUG_ASSERT(stream); + switch (stream->state) { + case H2_STREAM_ST_OPEN: + break; + default: + return APR_EINVAL; + } + return h2_request_write_data(stream->request, data, len); +} + +apr_status_t h2_stream_prep_read(h2_stream *stream, + apr_size_t *plen, int *peos) +{ + apr_status_t status = APR_SUCCESS; + const char *src; + + if (stream->bbout && !APR_BRIGADE_EMPTY(stream->bbout)) { + src = "stream"; + status = h2_util_bb_avail(stream->bbout, plen, peos); + if (status == APR_SUCCESS && !*peos && !*plen) { + apr_brigade_cleanup(stream->bbout); + return h2_stream_prep_read(stream, plen, peos); + } + } + else { + src = "mplx"; + status = h2_mplx_out_readx(stream->m, stream->id, + NULL, NULL, plen, peos); + } + if (status == APR_SUCCESS && !*peos && !*plen) { + status = APR_EAGAIN; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, stream->m->c, + "h2_stream(%ld-%d): prep_read %s, len=%ld eos=%d", + stream->m->id, stream->id, + src, (long)*plen, *peos); + return status; +} + +apr_status_t h2_stream_readx(h2_stream *stream, + h2_io_data_cb *cb, void *ctx, + apr_size_t *plen, int *peos) +{ + if (stream->bbout && !APR_BRIGADE_EMPTY(stream->bbout)) { + return h2_util_bb_readx(stream->bbout, cb, ctx, plen, peos); + } + return h2_mplx_out_readx(stream->m, stream->id, + cb, ctx, plen, peos); +} + + +void h2_stream_set_suspended(h2_stream *stream, int suspended) +{ + AP_DEBUG_ASSERT(stream); + stream->suspended = !!suspended; +} + +int h2_stream_is_suspended(h2_stream *stream) +{ + AP_DEBUG_ASSERT(stream); + return stream->suspended; +} + diff --git a/modules/http2/h2_stream.h b/modules/http2/h2_stream.h new file mode 100644 index 00000000000..f6bd71a5f84 --- /dev/null +++ b/modules/http2/h2_stream.h @@ -0,0 +1,108 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_stream__ +#define __mod_h2__h2_stream__ + +/** + * A HTTP/2 stream, e.g. a client request+response in HTTP/1.1 terms. + * + * Ok, not quite, but close enough, since we do not implement server + * pushes yet. + * + * A stream always belongs to a h2_session, the one managing the + * connection to the client. The h2_session writes to the h2_stream, + * adding HEADERS and DATA and finally an EOS. When headers are done, + * h2_stream can create a h2_task that can be scheduled to fullfill the + * request. + * + * This response headers are added directly to the h2_mplx of the session, + * but the response DATA can be read via h2_stream. Reading data will + * never block but return APR_EAGAIN when there currently is no data (and + * no eos) in the multiplexer for this stream. + */ +#include "h2_io.h" + +typedef enum { + H2_STREAM_ST_IDLE, + H2_STREAM_ST_OPEN, + H2_STREAM_ST_RESV_LOCAL, + H2_STREAM_ST_RESV_REMOTE, + H2_STREAM_ST_CLOSED_INPUT, + H2_STREAM_ST_CLOSED_OUTPUT, + H2_STREAM_ST_CLOSED, +} h2_stream_state_t; + +struct h2_mplx; +struct h2_request; +struct h2_response; +struct h2_task; + +typedef struct h2_stream h2_stream; + +struct h2_stream { + int id; /* http2 stream id */ + h2_stream_state_t state; /* http/2 state of this stream */ + struct h2_mplx *m; /* the multiplexer to work with */ + + int aborted; /* was aborted */ + int suspended; /* DATA sending has been suspended */ + + apr_pool_t *pool; /* the memory pool for this stream */ + struct h2_request *request; /* the request made in this stream */ + + struct h2_task *task; /* task created for this stream */ + struct h2_response *response; /* the response, once ready */ + apr_bucket_brigade *bbout; /* output DATA */ +}; + + +h2_stream *h2_stream_create(int id, apr_pool_t *pool, struct h2_mplx *m); + +apr_status_t h2_stream_destroy(h2_stream *stream); +void h2_stream_cleanup(h2_stream *stream); + +apr_pool_t *h2_stream_detach_pool(h2_stream *stream); +void h2_stream_attach_pool(h2_stream *stream, apr_pool_t *pool); + +void h2_stream_abort(h2_stream *stream); + +apr_status_t h2_stream_rwrite(h2_stream *stream, request_rec *r); + +apr_status_t h2_stream_write_eos(h2_stream *stream); + +apr_status_t h2_stream_write_header(h2_stream *stream, + const char *name, size_t nlen, + const char *value, size_t vlen); + +apr_status_t h2_stream_write_eoh(h2_stream *stream, int eos); + +apr_status_t h2_stream_write_data(h2_stream *stream, + const char *data, size_t len); + +apr_status_t h2_stream_set_response(h2_stream *stream, + struct h2_response *response, + apr_bucket_brigade *bb); + +apr_status_t h2_stream_prep_read(h2_stream *stream, + apr_size_t *plen, int *peos); + +apr_status_t h2_stream_readx(h2_stream *stream, h2_io_data_cb *cb, + void *ctx, apr_size_t *plen, int *peos); + +void h2_stream_set_suspended(h2_stream *stream, int suspended); +int h2_stream_is_suspended(h2_stream *stream); + +#endif /* defined(__mod_h2__h2_stream__) */ diff --git a/modules/http2/h2_stream_set.c b/modules/http2/h2_stream_set.c new file mode 100644 index 00000000000..dddd2e39908 --- /dev/null +++ b/modules/http2/h2_stream_set.c @@ -0,0 +1,164 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_task.h" +#include "h2_stream_set.h" + +#define H2_STREAM_IDX(list, i) ((h2_stream**)(list)->elts)[i] + +struct h2_stream_set { + apr_array_header_t *list; +}; + +h2_stream_set *h2_stream_set_create(apr_pool_t *pool) +{ + h2_stream_set *sp = apr_pcalloc(pool, sizeof(h2_stream_set)); + if (sp) { + sp->list = apr_array_make(pool, 100, sizeof(h2_stream*)); + if (!sp->list) { + return NULL; + } + } + return sp; +} + +void h2_stream_set_destroy(h2_stream_set *sp) +{ + (void)sp; +} + +static int h2_stream_id_cmp(const void *s1, const void *s2) +{ + h2_stream **pstream1 = (h2_stream **)s1; + h2_stream **pstream2 = (h2_stream **)s2; + return (*pstream1)->id - (*pstream2)->id; +} + +h2_stream *h2_stream_set_get(h2_stream_set *sp, int stream_id) +{ + /* we keep the array sorted by id, so lookup can be done + * by bsearch. + */ + h2_stream key; + h2_stream *pkey, **ps; + memset(&key, 0, sizeof(key)); + key.id = stream_id; + pkey = &key; + ps = bsearch(&pkey, sp->list->elts, sp->list->nelts, + sp->list->elt_size, h2_stream_id_cmp); + return ps? *ps : NULL; +} + +static void h2_stream_set_sort(h2_stream_set *sp) +{ + qsort(sp->list->elts, sp->list->nelts, sp->list->elt_size, + h2_stream_id_cmp); +} + +apr_status_t h2_stream_set_add(h2_stream_set *sp, h2_stream *stream) +{ + h2_stream *existing = h2_stream_set_get(sp, stream->id); + if (!existing) { + int last; + APR_ARRAY_PUSH(sp->list, h2_stream*) = stream; + /* Normally, streams get added in ascending order if id. We + * keep the array sorted, so we just need to check of the newly + * appended stream has a lower id than the last one. if not, + * sorting is not necessary. + */ + last = sp->list->nelts - 1; + if (last > 0 + && (H2_STREAM_IDX(sp->list, last)->id + < H2_STREAM_IDX(sp->list, last-1)->id)) { + h2_stream_set_sort(sp); + } + } + return APR_SUCCESS; +} + +h2_stream *h2_stream_set_remove(h2_stream_set *sp, h2_stream *stream) +{ + int i; + for (i = 0; i < sp->list->nelts; ++i) { + h2_stream *s = H2_STREAM_IDX(sp->list, i); + if (s == stream) { + int n; + --sp->list->nelts; + n = sp->list->nelts - i; + if (n > 0) { + /* Close the hole in the array by moving the upper + * parts down one step. + */ + h2_stream **selts = (h2_stream**)sp->list->elts; + memmove(selts+i, selts+i+1, n * sizeof(h2_stream*)); + } + return s; + } + } + return NULL; +} + +void h2_stream_set_remove_all(h2_stream_set *sp) +{ + sp->list->nelts = 0; +} + +int h2_stream_set_is_empty(h2_stream_set *sp) +{ + AP_DEBUG_ASSERT(sp); + return sp->list->nelts == 0; +} + +h2_stream *h2_stream_set_find(h2_stream_set *sp, + h2_stream_set_match_fn *match, void *ctx) +{ + h2_stream *s = NULL; + int i; + for (i = 0; !s && i < sp->list->nelts; ++i) { + s = match(ctx, H2_STREAM_IDX(sp->list, i)); + } + return s; +} + +void h2_stream_set_iter(h2_stream_set *sp, + h2_stream_set_iter_fn *iter, void *ctx) +{ + int i; + for (i = 0; i < sp->list->nelts; ++i) { + h2_stream *s = H2_STREAM_IDX(sp->list, i); + if (!iter(ctx, s)) { + break; + } + } +} + +apr_size_t h2_stream_set_size(h2_stream_set *sp) +{ + return sp->list->nelts; +} + diff --git a/modules/http2/h2_stream_set.h b/modules/http2/h2_stream_set.h new file mode 100644 index 00000000000..56075834555 --- /dev/null +++ b/modules/http2/h2_stream_set.h @@ -0,0 +1,52 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_stream_set__ +#define __mod_h2__h2_stream_set__ + +/** + * A set of h2_stream instances. Allows lookup by stream id + * and other criteria. + */ + +typedef h2_stream *h2_stream_set_match_fn(void *ctx, h2_stream *stream); +typedef int h2_stream_set_iter_fn(void *ctx, h2_stream *stream); + +typedef struct h2_stream_set h2_stream_set; + + +h2_stream_set *h2_stream_set_create(apr_pool_t *pool); + +void h2_stream_set_destroy(h2_stream_set *sp); + +apr_status_t h2_stream_set_add(h2_stream_set *sp, h2_stream *stream); + +h2_stream *h2_stream_set_get(h2_stream_set *sp, int stream_id); + +h2_stream *h2_stream_set_remove(h2_stream_set *sp,h2_stream *stream); + +void h2_stream_set_remove_all(h2_stream_set *sp); + +int h2_stream_set_is_empty(h2_stream_set *sp); + +apr_size_t h2_stream_set_size(h2_stream_set *sp); + +h2_stream *h2_stream_set_find(h2_stream_set *sp, + h2_stream_set_match_fn *match, void *ctx); + +void h2_stream_set_iter(h2_stream_set *sp, + h2_stream_set_iter_fn *iter, void *ctx); + +#endif /* defined(__mod_h2__h2_stream_set__) */ diff --git a/modules/http2/h2_switch.c b/modules/http2/h2_switch.c new file mode 100644 index 00000000000..4cdc4118b0a --- /dev/null +++ b/modules/http2/h2_switch.c @@ -0,0 +1,180 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "h2_private.h" + +#include "h2_config.h" +#include "h2_ctx.h" +#include "h2_conn.h" +#include "h2_h2.h" +#include "h2_switch.h" + +/******************************************************************************* + * SSL var lookup + */ +APR_DECLARE_OPTIONAL_FN(char *, ssl_var_lookup, + (apr_pool_t *, server_rec *, + conn_rec *, request_rec *, + char *)); +static char *(*opt_ssl_var_lookup)(apr_pool_t *, server_rec *, + conn_rec *, request_rec *, + char *); + +/******************************************************************************* + * Once per lifetime init, retrieve optional functions + */ +apr_status_t h2_switch_init(apr_pool_t *pool, server_rec *s) +{ + (void)pool; + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, "h2_switch init"); + opt_ssl_var_lookup = APR_RETRIEVE_OPTIONAL_FN(ssl_var_lookup); + + return APR_SUCCESS; +} + +static int h2_protocol_propose(conn_rec *c, request_rec *r, + server_rec *s, + const apr_array_header_t *offers, + apr_array_header_t *proposals) +{ + int proposed = 0; + const char **protos = h2_h2_is_tls(c)? h2_tls_protos : h2_clear_protos; + + (void)s; + if (strcmp(AP_PROTOCOL_HTTP1, ap_get_protocol(c))) { + /* We do not know how to switch from anything else but http/1.1. + */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, + "protocol switch: current proto != http/1.1, declined"); + return DECLINED; + } + + if (r) { + const char *p; + /* So far, this indicates an HTTP/1 Upgrade header initiated + * protocol switch. For that, the HTTP2-Settings header needs + * to be present and valid for the connection. + */ + p = apr_table_get(r->headers_in, "HTTP2-Settings"); + if (!p) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "upgrade without HTTP2-Settings declined"); + return DECLINED; + } + + p = apr_table_get(r->headers_in, "Connection"); + if (!ap_find_token(r->pool, p, "http2-settings")) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "upgrade without HTTP2-Settings declined"); + return DECLINED; + } + + /* We also allow switching only for requests that have no body. + */ + p = apr_table_get(r->headers_in, "Content-Length"); + if (p && strcmp(p, "0")) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "upgrade with content-length: %s, declined", p); + return DECLINED; + } + } + + while (*protos) { + /* Add all protocols we know (tls or clear) and that + * are part of the offerings (if there have been any). + */ + if (!offers || ap_array_str_contains(offers, *protos)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "proposing protocol '%s'", *protos); + APR_ARRAY_PUSH(proposals, const char*) = *protos; + proposed = 1; + } + ++protos; + } + return proposed? DECLINED : OK; +} + +static int h2_protocol_switch(conn_rec *c, request_rec *r, server_rec *s, + const char *protocol) +{ + int found = 0; + const char **protos = h2_h2_is_tls(c)? h2_tls_protos : h2_clear_protos; + const char **p = protos; + + (void)s; + while (*p) { + if (!strcmp(*p, protocol)) { + found = 1; + break; + } + p++; + } + + if (found) { + h2_ctx *ctx = h2_ctx_get(c); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "switching protocol to '%s'", protocol); + h2_ctx_protocol_set(ctx, protocol); + + if (r != NULL) { + apr_status_t status; + /* Switching in the middle of a request means that + * we have to send out the response to this one in h2 + * format. So we need to take over the connection + * right away. + */ + ap_remove_input_filter_byhandle(r->input_filters, "http_in"); + ap_remove_input_filter_byhandle(r->input_filters, "reqtimeout"); + + /* Ok, start an h2_conn on this one. */ + status = h2_conn_rprocess(r); + if (status != DONE) { + /* Nothing really to do about this. */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, + "session proessed, unexpected status"); + } + } + return DONE; + } + + return DECLINED; +} + +static const char *h2_protocol_get(const conn_rec *c) +{ + return h2_ctx_protocol_get(c); +} + +void h2_switch_register_hooks(void) +{ + ap_hook_protocol_propose(h2_protocol_propose, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_protocol_switch(h2_protocol_switch, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_protocol_get(h2_protocol_get, NULL, NULL, APR_HOOK_MIDDLE); +} + diff --git a/modules/http2/h2_switch.h b/modules/http2/h2_switch.h new file mode 100644 index 00000000000..3d9c628c769 --- /dev/null +++ b/modules/http2/h2_switch.h @@ -0,0 +1,29 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_switch__ +#define __mod_h2__h2_switch__ + +/* + * One time, post config intialization. + */ +apr_status_t h2_switch_init(apr_pool_t *pool, server_rec *s); + +/* Register apache hooks for protocol switching + */ +void h2_switch_register_hooks(void); + + +#endif /* defined(__mod_h2__h2_switch__) */ diff --git a/modules/http2/h2_task.c b/modules/http2/h2_task.c new file mode 100644 index 00000000000..26b8c6e40e8 --- /dev/null +++ b/modules/http2/h2_task.c @@ -0,0 +1,467 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_conn.h" +#include "h2_config.h" +#include "h2_from_h1.h" +#include "h2_h2.h" +#include "h2_mplx.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_task_input.h" +#include "h2_task_output.h" +#include "h2_task.h" +#include "h2_ctx.h" +#include "h2_worker.h" + + +static apr_status_t h2_filter_stream_input(ap_filter_t* filter, + apr_bucket_brigade* brigade, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes) { + h2_task_env *env = filter->ctx; + AP_DEBUG_ASSERT(env); + if (!env->input) { + return APR_ECONNABORTED; + } + return h2_task_input_read(env->input, filter, brigade, + mode, block, readbytes); +} + +static apr_status_t h2_filter_stream_output(ap_filter_t* filter, + apr_bucket_brigade* brigade) { + h2_task_env *env = filter->ctx; + AP_DEBUG_ASSERT(env); + if (!env->output) { + return APR_ECONNABORTED; + } + return h2_task_output_write(env->output, filter, brigade); +} + +static apr_status_t h2_filter_read_response(ap_filter_t* f, + apr_bucket_brigade* bb) { + h2_task_env *env = f->ctx; + AP_DEBUG_ASSERT(env); + if (!env->output || !env->output->from_h1) { + return APR_ECONNABORTED; + } + return h2_from_h1_read_response(env->output->from_h1, f, bb); +} + +/******************************************************************************* + * Register various hooks + */ +static const char *const mod_ssl[] = { "mod_ssl.c", NULL}; +static int h2_task_pre_conn(conn_rec* c, void *arg); +static int h2_task_process_conn(conn_rec* c); + +void h2_task_register_hooks(void) +{ + /* This hook runs on new connections before mod_ssl has a say. + * Its purpose is to prevent mod_ssl from touching our pseudo-connections + * for streams. + */ + ap_hook_pre_connection(h2_task_pre_conn, + NULL, mod_ssl, APR_HOOK_FIRST); + /* When the connection processing actually starts, we might to + * take over, if the connection is for a task. + */ + ap_hook_process_connection(h2_task_process_conn, + NULL, NULL, APR_HOOK_FIRST); + + ap_register_output_filter("H2_RESPONSE", h2_response_output_filter, + NULL, AP_FTYPE_PROTOCOL); + ap_register_input_filter("H2_TO_H1", h2_filter_stream_input, + NULL, AP_FTYPE_NETWORK); + ap_register_output_filter("H1_TO_H2", h2_filter_stream_output, + NULL, AP_FTYPE_NETWORK); + ap_register_output_filter("H1_TO_H2_RESP", h2_filter_read_response, + NULL, AP_FTYPE_PROTOCOL); +} + +static int h2_task_pre_conn(conn_rec* c, void *arg) +{ + + h2_ctx *ctx = h2_ctx_get(c); + + (void)arg; + if (h2_ctx_is_task(ctx)) { + h2_task_env *env = h2_ctx_get_task(ctx); + + /* This connection is a pseudo-connection used for a h2_task. + * Since we read/write directly from it ourselves, we need + * to disable a possible ssl connection filter. + */ + h2_tls_disable(c); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "h2_h2, pre_connection, found stream task"); + + /* Add our own, network level in- and output filters. + */ + ap_add_input_filter("H2_TO_H1", env, NULL, c); + ap_add_output_filter("H1_TO_H2", env, NULL, c); + } + return OK; +} + +static int h2_task_process_conn(conn_rec* c) +{ + h2_ctx *ctx = h2_ctx_get(c); + + if (h2_ctx_is_task(ctx)) { + if (!ctx->task_env->serialize_headers) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "h2_h2, processing request directly"); + h2_task_process_request(ctx->task_env); + return DONE; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "h2_task(%s), serialized handling", ctx->task_env->id); + } + return DECLINED; +} + + +h2_task *h2_task_create(long session_id, + int stream_id, + apr_pool_t *stream_pool, + h2_mplx *mplx, conn_rec *c) +{ + h2_task *task = apr_pcalloc(stream_pool, sizeof(h2_task)); + if (task == NULL) { + ap_log_perror(APLOG_MARK, APLOG_ERR, APR_ENOMEM, stream_pool, + APLOGNO(02941) "h2_task(%ld-%d): create stream task", + session_id, stream_id); + h2_mplx_out_close(mplx, stream_id); + return NULL; + } + + APR_RING_ELEM_INIT(task, link); + + task->id = apr_psprintf(stream_pool, "%ld-%d", session_id, stream_id); + task->stream_id = stream_id; + task->mplx = mplx; + + task->c = c; + + ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, stream_pool, + "h2_task(%s): created", task->id); + return task; +} + +void h2_task_set_request(h2_task *task, + const char *method, + const char *scheme, + const char *authority, + const char *path, + apr_table_t *headers, int eos) +{ + task->method = method; + task->scheme = scheme; + task->authority = authority; + task->path = path; + task->headers = headers; + task->input_eos = eos; +} + +apr_status_t h2_task_destroy(h2_task *task) +{ + (void)task; + return APR_SUCCESS; +} + +apr_status_t h2_task_do(h2_task *task, h2_worker *worker) +{ + apr_status_t status = APR_SUCCESS; + h2_config *cfg = h2_config_get(task->mplx->c); + h2_task_env env; + + AP_DEBUG_ASSERT(task); + + memset(&env, 0, sizeof(env)); + + env.id = task->id; + env.stream_id = task->stream_id; + env.mplx = task->mplx; + task->mplx = NULL; + + env.input_eos = task->input_eos; + env.serialize_headers = !!h2_config_geti(cfg, H2_CONF_SER_HEADERS); + + /* Create a subpool from the worker one to be used for all things + * with life-time of this task_env execution. + */ + apr_pool_create(&env.pool, h2_worker_get_pool(worker)); + + /* Link the env to the worker which provides useful things such + * as mutex, a socket etc. */ + env.io = h2_worker_get_cond(worker); + + /* Clone fields, so that lifetimes become (more) independent. */ + env.method = apr_pstrdup(env.pool, task->method); + env.scheme = apr_pstrdup(env.pool, task->scheme); + env.authority = apr_pstrdup(env.pool, task->authority); + env.path = apr_pstrdup(env.pool, task->path); + env.headers = apr_table_clone(env.pool, task->headers); + + /* Setup the pseudo connection to use our own pool and bucket_alloc */ + env.c = *task->c; + task->c = NULL; + status = h2_conn_setup(&env, worker); + + /* save in connection that this one is a pseudo connection, prevents + * other hooks from messing with it. */ + h2_ctx_create_for(&env.c, &env); + + if (status == APR_SUCCESS) { + env.input = h2_task_input_create(&env, env.pool, + env.c.bucket_alloc); + env.output = h2_task_output_create(&env, env.pool, + env.c.bucket_alloc); + status = h2_conn_process(&env.c, h2_worker_get_socket(worker)); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, &env.c, + "h2_task(%s): processing done", env.id); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, &env.c, + APLOGNO(02957) "h2_task(%s): error setting up h2_task_env", + env.id); + } + + if (env.input) { + h2_task_input_destroy(env.input); + env.input = NULL; + } + + if (env.output) { + h2_task_output_close(env.output); + h2_task_output_destroy(env.output); + env.output = NULL; + } + + h2_task_set_finished(task); + if (env.io) { + apr_thread_cond_signal(env.io); + } + + if (env.pool) { + apr_pool_destroy(env.pool); + env.pool = NULL; + } + + if (env.c.id) { + h2_conn_post(&env.c, worker); + } + + h2_mplx_task_done(env.mplx, env.stream_id); + + return status; +} + +int h2_task_has_started(h2_task *task) +{ + AP_DEBUG_ASSERT(task); + return apr_atomic_read32(&task->has_started); +} + +void h2_task_set_started(h2_task *task) +{ + AP_DEBUG_ASSERT(task); + apr_atomic_set32(&task->has_started, 1); +} + +int h2_task_has_finished(h2_task *task) +{ + return apr_atomic_read32(&task->has_finished); +} + +void h2_task_set_finished(h2_task *task) +{ + apr_atomic_set32(&task->has_finished, 1); +} + +void h2_task_die(h2_task_env *env, int status, request_rec *r) +{ + (void)env; + ap_die(status, r); +} + +static request_rec *h2_task_create_request(h2_task_env *env) +{ + conn_rec *conn = &env->c; + request_rec *r; + apr_pool_t *p; + int access_status = HTTP_OK; + + apr_pool_create(&p, conn->pool); + apr_pool_tag(p, "request"); + r = apr_pcalloc(p, sizeof(request_rec)); + AP_READ_REQUEST_ENTRY((intptr_t)r, (uintptr_t)conn); + r->pool = p; + r->connection = conn; + r->server = conn->base_server; + + r->user = NULL; + r->ap_auth_type = NULL; + + r->allowed_methods = ap_make_method_list(p, 2); + + r->headers_in = apr_table_copy(r->pool, env->headers); + r->trailers_in = apr_table_make(r->pool, 5); + r->subprocess_env = apr_table_make(r->pool, 25); + r->headers_out = apr_table_make(r->pool, 12); + r->err_headers_out = apr_table_make(r->pool, 5); + r->trailers_out = apr_table_make(r->pool, 5); + r->notes = apr_table_make(r->pool, 5); + + r->request_config = ap_create_request_config(r->pool); + /* Must be set before we run create request hook */ + + r->proto_output_filters = conn->output_filters; + r->output_filters = r->proto_output_filters; + r->proto_input_filters = conn->input_filters; + r->input_filters = r->proto_input_filters; + ap_run_create_request(r); + r->per_dir_config = r->server->lookup_defaults; + + r->sent_bodyct = 0; /* bytect isn't for body */ + + r->read_length = 0; + r->read_body = REQUEST_NO_BODY; + + r->status = HTTP_OK; /* Until further notice */ + r->header_only = 0; + r->the_request = NULL; + + /* Begin by presuming any module can make its own path_info assumptions, + * until some module interjects and changes the value. + */ + r->used_path_info = AP_REQ_DEFAULT_PATH_INFO; + + r->useragent_addr = conn->client_addr; + r->useragent_ip = conn->client_ip; + + ap_run_pre_read_request(r, conn); + + /* Time to populate r with the data we have. */ + r->request_time = apr_time_now(); + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/1.1", + env->method, env->path); + r->method = env->method; + /* Provide quick information about the request method as soon as known */ + r->method_number = ap_method_number_of(r->method); + if (r->method_number == M_GET && r->method[0] == 'H') { + r->header_only = 1; + } + + ap_parse_uri(r, env->path); + r->protocol = (char*)"HTTP/1.1"; + r->proto_num = HTTP_VERSION(1, 1); + + /* update what we think the virtual host is based on the headers we've + * now read. may update status. + * Leave r->hostname empty, vhost will parse if form our Host: header, + * otherwise we get complains about port numbers. + */ + r->hostname = NULL; + ap_update_vhost_from_headers(r); + + /* we may have switched to another server */ + r->per_dir_config = r->server->lookup_defaults; + + /* + * Add the HTTP_IN filter here to ensure that ap_discard_request_body + * called by ap_die and by ap_send_error_response works correctly on + * status codes that do not cause the connection to be dropped and + * in situations where the connection should be kept alive. + */ + ap_add_input_filter_handle(ap_http_input_filter_handle, + NULL, r, r->connection); + + if (access_status != HTTP_OK + || (access_status = ap_run_post_read_request(r))) { + /* Request check post hooks failed. An example of this would be a + * request for a vhost where h2 is disabled --> 421. + */ + h2_task_die(env, access_status, r); + ap_update_child_status(conn->sbh, SERVER_BUSY_LOG, r); + ap_run_log_transaction(r); + r = NULL; + goto traceout; + } + + AP_READ_REQUEST_SUCCESS((uintptr_t)r, (char *)r->method, + (char *)r->uri, (char *)r->server->defn_name, + r->status); + return r; +traceout: + AP_READ_REQUEST_FAILURE((uintptr_t)r); + return r; +} + + +apr_status_t h2_task_process_request(h2_task_env *env) +{ + conn_rec *c = &env->c; + request_rec *r; + conn_state_t *cs = c->cs; + + r = h2_task_create_request(env); + if (r && (r->status == HTTP_OK)) { + ap_update_child_status(c->sbh, SERVER_BUSY_READ, r); + + if (cs) + cs->state = CONN_STATE_HANDLER; + ap_process_request(r); + /* After the call to ap_process_request, the + * request pool will have been deleted. We set + * r=NULL here to ensure that any dereference + * of r that might be added later in this function + * will result in a segfault immediately instead + * of nondeterministic failures later. + */ + r = NULL; + } + ap_update_child_status(c->sbh, SERVER_BUSY_WRITE, NULL); + c->sbh = NULL; + + return APR_SUCCESS; +} + + + + diff --git a/modules/http2/h2_task.h b/modules/http2/h2_task.h new file mode 100644 index 00000000000..b66ce38c251 --- /dev/null +++ b/modules/http2/h2_task.h @@ -0,0 +1,187 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_task__ +#define __mod_h2__h2_task__ + +/** + * A h2_task fakes a HTTP/1.1 request from the data in a HTTP/2 stream + * (HEADER+CONT.+DATA) the module recieves. + * + * In order to answer a HTTP/2 stream, we want all Apache httpd infrastructure + * to be involved as usual, as if this stream can as a separate HTTP/1.1 + * request. The basic trickery to do so was derived from google's mod_spdy + * source. Basically, we fake a new conn_rec object, even with its own + * socket and give it to ap_process_connection(). + * + * Since h2_task instances are executed in separate threads, we may have + * different lifetimes than our h2_stream or h2_session instances. Basically, + * we would like to be as standalone as possible. + * + * Finally, to keep certain connection level filters, such as ourselves and + * especially mod_ssl ones, from messing with our data, we need a filter + * of our own to disble those. + */ + +struct apr_thread_cond_t; +struct h2_conn; +struct h2_mplx; +struct h2_task; +struct h2_resp_head; +struct h2_worker; + +typedef struct h2_task h2_task; + +struct h2_task { + /** Links to the rest of the tasks */ + APR_RING_ENTRY(h2_task) link; + + const char *id; + int stream_id; + struct h2_mplx *mplx; + + volatile apr_uint32_t has_started; + volatile apr_uint32_t has_finished; + + const char *method; + const char *scheme; + const char *authority; + const char *path; + apr_table_t *headers; + int input_eos; + + struct conn_rec *c; +}; + +typedef struct h2_task_env h2_task_env; + +struct h2_task_env { + const char *id; + int stream_id; + struct h2_mplx *mplx; + + apr_pool_t *pool; /* pool for task lifetime things */ + apr_bucket_alloc_t *bucket_alloc; + + const char *method; + const char *scheme; + const char *authority; + const char *path; + apr_table_t *headers; + int input_eos; + + int serialize_headers; + + struct conn_rec c; + struct h2_task_input *input; + struct h2_task_output *output; + + struct apr_thread_cond_t *io; /* used to wait for events on */ +}; + +/** + * The magic pointer value that indicates the head of a h2_task list + * @param b The task list + * @return The magic pointer value + */ +#define H2_TASK_LIST_SENTINEL(b) APR_RING_SENTINEL((b), h2_task, link) + +/** + * Determine if the task list is empty + * @param b The list to check + * @return true or false + */ +#define H2_TASK_LIST_EMPTY(b) APR_RING_EMPTY((b), h2_task, link) + +/** + * Return the first task in a list + * @param b The list to query + * @return The first task in the list + */ +#define H2_TASK_LIST_FIRST(b) APR_RING_FIRST(b) + +/** + * Return the last task in a list + * @param b The list to query + * @return The last task int he list + */ +#define H2_TASK_LIST_LAST(b) APR_RING_LAST(b) + +/** + * Insert a single task at the front of a list + * @param b The list to add to + * @param e The task to insert + */ +#define H2_TASK_LIST_INSERT_HEAD(b, e) do { \ + h2_task *ap__b = (e); \ + APR_RING_INSERT_HEAD((b), ap__b, h2_task, link); \ +} while (0) + +/** + * Insert a single task at the end of a list + * @param b The list to add to + * @param e The task to insert + */ +#define H2_TASK_LIST_INSERT_TAIL(b, e) do { \ + h2_task *ap__b = (e); \ + APR_RING_INSERT_TAIL((b), ap__b, h2_task, link); \ +} while (0) + +/** + * Get the next task in the list + * @param e The current task + * @return The next task + */ +#define H2_TASK_NEXT(e) APR_RING_NEXT((e), link) +/** + * Get the previous task in the list + * @param e The current task + * @return The previous task + */ +#define H2_TASK_PREV(e) APR_RING_PREV((e), link) + +/** + * Remove a task from its list + * @param e The task to remove + */ +#define H2_TASK_REMOVE(e) APR_RING_REMOVE((e), link) + + +h2_task *h2_task_create(long session_id, int stream_id, + apr_pool_t *pool, struct h2_mplx *mplx, + conn_rec *c); + +apr_status_t h2_task_destroy(h2_task *task); + +void h2_task_set_request(h2_task *task, + const char *method, + const char *scheme, + const char *authority, + const char *path, + apr_table_t *headers, int eos); + + +apr_status_t h2_task_do(h2_task *task, struct h2_worker *worker); +apr_status_t h2_task_process_request(h2_task_env *env); + +int h2_task_has_started(h2_task *task); +void h2_task_set_started(h2_task *task); +int h2_task_has_finished(h2_task *task); +void h2_task_set_finished(h2_task *task); + +void h2_task_register_hooks(void); +void h2_task_die(h2_task_env *env, int status, request_rec *r); + +#endif /* defined(__mod_h2__h2_task__) */ diff --git a/modules/http2/h2_task_input.c b/modules/http2/h2_task_input.c new file mode 100644 index 00000000000..cc7d8503fd5 --- /dev/null +++ b/modules/http2/h2_task_input.c @@ -0,0 +1,219 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_conn.h" +#include "h2_mplx.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_task_input.h" +#include "h2_task.h" +#include "h2_util.h" + + +static int is_aborted(ap_filter_t *f) +{ + return (f->c->aborted); +} + +static int ser_header(void *ctx, const char *name, const char *value) +{ + h2_task_input *input = (h2_task_input*)ctx; + apr_brigade_printf(input->bb, NULL, NULL, "%s: %s\r\n", name, value); + return 1; +} + +h2_task_input *h2_task_input_create(h2_task_env *env, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc) +{ + h2_task_input *input = apr_pcalloc(pool, sizeof(h2_task_input)); + if (input) { + input->env = env; + input->bb = NULL; + + if (env->serialize_headers) { + input->bb = apr_brigade_create(pool, bucket_alloc); + apr_brigade_printf(input->bb, NULL, NULL, "%s %s HTTP/1.1\r\n", + env->method, env->path); + apr_table_do(ser_header, input, env->headers, NULL); + apr_brigade_puts(input->bb, NULL, NULL, "\r\n"); + if (input->env->input_eos) { + APR_BRIGADE_INSERT_TAIL(input->bb, apr_bucket_eos_create(bucket_alloc)); + } + } + else if (!input->env->input_eos) { + input->bb = apr_brigade_create(pool, bucket_alloc); + } + else { + /* We do not serialize and have eos already, no need to + * create a bucket brigade. */ + } + + if (APLOGcdebug(&env->c)) { + char buffer[1024]; + apr_size_t len = sizeof(buffer)-1; + if (input->bb) { + apr_brigade_flatten(input->bb, buffer, &len); + } + else { + len = 0; + } + buffer[len] = 0; + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, &env->c, + "h2_task_input(%s): request is: %s", + env->id, buffer); + } + } + return input; +} + +void h2_task_input_destroy(h2_task_input *input) +{ + input->bb = NULL; +} + +apr_status_t h2_task_input_read(h2_task_input *input, + ap_filter_t* f, + apr_bucket_brigade* bb, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes) +{ + apr_status_t status = APR_SUCCESS; + apr_off_t bblen = 0; + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c, + "h2_task_input(%s): read, block=%d, mode=%d, readbytes=%ld", + input->env->id, block, mode, (long)readbytes); + + if (is_aborted(f)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_task_input(%s): is aborted", + input->env->id); + return APR_ECONNABORTED; + } + + if (mode == AP_MODE_INIT) { + return APR_SUCCESS; + } + + if (input->bb) { + status = apr_brigade_length(input->bb, 1, &bblen); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, f->c, + APLOGNO(02958) "h2_task_input(%s): brigade length fail", + input->env->id); + return status; + } + } + + if ((bblen == 0) && input->env->input_eos) { + return APR_EOF; + } + + while ((bblen == 0) || (mode == AP_MODE_READBYTES && bblen < readbytes)) { + /* Get more data for our stream from mplx. + */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_task_input(%s): get more data from mplx, block=%d, " + "readbytes=%ld, queued=%ld", + input->env->id, block, + (long)readbytes, (long)bblen); + + /* Although we sometimes get called with APR_NONBLOCK_READs, + we seem to fill our buffer blocking. Otherwise we get EAGAIN, + return that to our caller and everyone throws up their hands, + never calling us again. */ + status = h2_mplx_in_read(input->env->mplx, APR_BLOCK_READ, + input->env->stream_id, input->bb, + input->env->io); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_task_input(%s): mplx in read returned", + input->env->id); + if (status != APR_SUCCESS) { + return status; + } + status = apr_brigade_length(input->bb, 1, &bblen); + if (status != APR_SUCCESS) { + return status; + } + if ((bblen == 0) && (block == APR_NONBLOCK_READ)) { + return h2_util_has_eos(input->bb, 0)? APR_EOF : APR_EAGAIN; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_task_input(%s): mplx in read, %ld bytes in brigade", + input->env->id, (long)bblen); + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_task_input(%s): read, mode=%d, block=%d, " + "readbytes=%ld, queued=%ld", + input->env->id, mode, block, + (long)readbytes, (long)bblen); + + if (!APR_BRIGADE_EMPTY(input->bb)) { + if (mode == AP_MODE_EXHAUSTIVE) { + /* return all we have */ + return h2_util_move(bb, input->bb, readbytes, 0, + "task_input_read(exhaustive)"); + } + else if (mode == AP_MODE_READBYTES) { + return h2_util_move(bb, input->bb, readbytes, 0, + "task_input_read(readbytes)"); + } + else if (mode == AP_MODE_SPECULATIVE) { + /* return not more than was asked for */ + return h2_util_copy(bb, input->bb, readbytes, + "task_input_read(speculative)"); + } + else if (mode == AP_MODE_GETLINE) { + /* we are reading a single LF line, e.g. the HTTP headers */ + status = apr_brigade_split_line(bb, input->bb, block, + HUGE_STRING_LEN); + if (APLOGctrace1(f->c)) { + char buffer[1024]; + apr_size_t len = sizeof(buffer)-1; + apr_brigade_flatten(bb, buffer, &len); + buffer[len] = 0; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_task_input(%s): getline: %s", + input->env->id, buffer); + } + return status; + } + else { + /* Hmm, well. There is mode AP_MODE_EATCRLF, but we chose not + * to support it. Seems to work. */ + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOTIMPL, f->c, + APLOGNO(02942) + "h2_task_input, unsupported READ mode %d", mode); + return APR_ENOTIMPL; + } + } + + if (is_aborted(f)) { + return APR_ECONNABORTED; + } + + return (block == APR_NONBLOCK_READ)? APR_EAGAIN : APR_EOF; +} + diff --git a/modules/http2/h2_task_input.h b/modules/http2/h2_task_input.h new file mode 100644 index 00000000000..32adc1770df --- /dev/null +++ b/modules/http2/h2_task_input.h @@ -0,0 +1,46 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_task_input__ +#define __mod_h2__h2_task_input__ + +/* h2_task_input places the HEADER+DATA, formatted in HTTP/1.1, into + * a bucket brigade. The brigade is setup as the input brigade for our + * pseudo httpd conn_rec that is handling a specific h2_task. + */ +struct apr_thread_cond_t; +struct h2_mplx; +struct h2_task_env; + +typedef struct h2_task_input h2_task_input; +struct h2_task_input { + struct h2_task_env *env; + apr_bucket_brigade *bb; +}; + + +h2_task_input *h2_task_input_create(struct h2_task_env *env, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc); + +void h2_task_input_destroy(h2_task_input *input); + +apr_status_t h2_task_input_read(h2_task_input *input, + ap_filter_t* filter, + apr_bucket_brigade* brigade, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes); + +#endif /* defined(__mod_h2__h2_task_input__) */ diff --git a/modules/http2/h2_task_output.c b/modules/http2/h2_task_output.c new file mode 100644 index 00000000000..879cb5fa21b --- /dev/null +++ b/modules/http2/h2_task_output.c @@ -0,0 +1,132 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_conn.h" +#include "h2_mplx.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_from_h1.h" +#include "h2_response.h" +#include "h2_task_output.h" +#include "h2_task.h" +#include "h2_util.h" + + +h2_task_output *h2_task_output_create(h2_task_env *env, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc) +{ + h2_task_output *output = apr_pcalloc(pool, sizeof(h2_task_output)); + + (void)bucket_alloc; + if (output) { + output->env = env; + output->state = H2_TASK_OUT_INIT; + output->from_h1 = h2_from_h1_create(env->stream_id, pool); + if (!output->from_h1) { + return NULL; + } + } + return output; +} + +void h2_task_output_destroy(h2_task_output *output) +{ + if (output->from_h1) { + h2_from_h1_destroy(output->from_h1); + output->from_h1 = NULL; + } +} + +static apr_status_t open_if_needed(h2_task_output *output, ap_filter_t *f, + apr_bucket_brigade *bb) +{ + if (output->state == H2_TASK_OUT_INIT) { + h2_response *response; + output->state = H2_TASK_OUT_STARTED; + response = h2_from_h1_get_response(output->from_h1); + if (!response) { + if (f) { + /* This happens currently when ap_die(status, r) is invoked + * by a read request filter. + */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c, + "h2_task_output(%s): write without response " + "for %s %s %s", + output->env->id, output->env->method, + output->env->authority, output->env->path); + f->c->aborted = 1; + } + if (output->env->io) { + apr_thread_cond_broadcast(output->env->io); + } + return APR_ECONNABORTED; + } + + return h2_mplx_out_open(output->env->mplx, output->env->stream_id, + response, f, bb, output->env->io); + } + return APR_EOF; +} + +void h2_task_output_close(h2_task_output *output) +{ + open_if_needed(output, NULL, NULL); + if (output->state != H2_TASK_OUT_DONE) { + h2_mplx_out_close(output->env->mplx, output->env->stream_id); + output->state = H2_TASK_OUT_DONE; + } +} + +int h2_task_output_has_started(h2_task_output *output) +{ + return output->state >= H2_TASK_OUT_STARTED; +} + +/* Bring the data from the brigade (which represents the result of the + * request_rec out filter chain) into the h2_mplx for further sending + * on the master connection. + */ +apr_status_t h2_task_output_write(h2_task_output *output, + ap_filter_t* f, apr_bucket_brigade* bb) +{ + apr_status_t status; + if (APR_BRIGADE_EMPTY(bb)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_task_output(%s): empty write", output->env->id); + return APR_SUCCESS; + } + + status = open_if_needed(output, f, bb); + if (status != APR_EOF) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, + "h2_task_output(%s): opened and passed brigade", + output->env->id); + return status; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, + "h2_task_output(%s): write brigade", output->env->id); + return h2_mplx_out_write(output->env->mplx, output->env->stream_id, + f, bb, output->env->io); +} + diff --git a/modules/http2/h2_task_output.h b/modules/http2/h2_task_output.h new file mode 100644 index 00000000000..86571a1e107 --- /dev/null +++ b/modules/http2/h2_task_output.h @@ -0,0 +1,56 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_task_output__ +#define __mod_h2__h2_task_output__ + +/* h2_task_output reads a HTTP/1 response from the brigade and applies + * them to a h2_output_converter. The brigade is setup as the output brigade + * for our pseudo httpd conn_rec that is handling a specific h2_task. + * + */ +struct apr_thread_cond_t; +struct h2_mplx; +struct h2_task_env; +struct h2_from_h1; + +typedef enum { + H2_TASK_OUT_INIT, + H2_TASK_OUT_STARTED, + H2_TASK_OUT_DONE, +} h2_task_output_state_t; + +typedef struct h2_task_output h2_task_output; + +struct h2_task_output { + struct h2_task_env *env; + h2_task_output_state_t state; + struct h2_from_h1 *from_h1; +}; + +h2_task_output *h2_task_output_create(struct h2_task_env *env, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc); + +void h2_task_output_destroy(h2_task_output *output); + +apr_status_t h2_task_output_write(h2_task_output *output, + ap_filter_t* filter, + apr_bucket_brigade* brigade); + +void h2_task_output_close(h2_task_output *output); + +int h2_task_output_has_started(h2_task_output *output); + +#endif /* defined(__mod_h2__h2_task_output__) */ diff --git a/modules/http2/h2_task_queue.c b/modules/http2/h2_task_queue.c new file mode 100644 index 00000000000..a81cc100fb9 --- /dev/null +++ b/modules/http2/h2_task_queue.c @@ -0,0 +1,88 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include "h2_task.h" +#include "h2_task_queue.h" + + +h2_task_queue *h2_tq_create(long id, apr_pool_t *pool) +{ + h2_task_queue *q = apr_pcalloc(pool, sizeof(h2_task_queue)); + if (q) { + q->id = id; + APR_RING_ELEM_INIT(q, link); + APR_RING_INIT(&q->tasks, h2_task, link); + } + return q; +} + +void h2_tq_destroy(h2_task_queue *q) +{ + while (!H2_TASK_LIST_EMPTY(&q->tasks)) { + h2_task *task = H2_TASK_LIST_FIRST(&q->tasks); + H2_TASK_REMOVE(task); + } +} + +static int in_list(h2_task_queue *q, h2_task *task) +{ + h2_task *e; + for (e = H2_TASK_LIST_FIRST(&q->tasks); + e != H2_TASK_LIST_SENTINEL(&q->tasks); + e = H2_TASK_NEXT(e)) { + if (e == task) { + return 1; + } + } + return 0; +} + +int h2_tq_empty(h2_task_queue *q) +{ + return H2_TASK_LIST_EMPTY(&q->tasks); +} + +void h2_tq_append(h2_task_queue *q, struct h2_task *task) +{ + H2_TASK_LIST_INSERT_TAIL(&q->tasks, task); +} + +apr_status_t h2_tq_remove(h2_task_queue *q, struct h2_task *task) +{ + if (in_list(q, task)) { + H2_TASK_REMOVE(task); + return APR_SUCCESS; + } + return APR_NOTFOUND; +} + +h2_task *h2_tq_pop_first(h2_task_queue *q) +{ + if (!H2_TASK_LIST_EMPTY(&q->tasks)) { + h2_task *task = H2_TASK_LIST_FIRST(&q->tasks); + H2_TASK_REMOVE(task); + return task; + } + return NULL; +} + + + diff --git a/modules/http2/h2_task_queue.h b/modules/http2/h2_task_queue.h new file mode 100644 index 00000000000..d93d74ac502 --- /dev/null +++ b/modules/http2/h2_task_queue.h @@ -0,0 +1,148 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_task_queue__ +#define __mod_h2__h2_task_queue__ + +struct h2_task; + +/** + * A simple ring of rings that keeps a list of h2_tasks and can + * be ringed itself, using the APR RING macros. + */ +typedef struct h2_task_queue h2_task_queue; + +struct h2_task_queue { + APR_RING_ENTRY(h2_task_queue) link; + APR_RING_HEAD(h2_tasks, h2_task) tasks; + long id; +}; + +/** + * Allocate a new queue from the pool and initialize. + * @param id the identifier of the queue + * @param pool the memory pool + */ +h2_task_queue *h2_tq_create(long id, apr_pool_t *pool); + +/** + * Release all queue tasks. + * @param q the queue to destroy + */ +void h2_tq_destroy(h2_task_queue *q); + +/** + * Return != 0 iff there are no tasks in the queue. + * @param q the queue to check + */ +int h2_tq_empty(h2_task_queue *q); + +/** + * Append the task to the end of the queue. + * @param q the queue to append the task to + * @param task the task to append + */ +void h2_tq_append(h2_task_queue *q, struct h2_task *task); + +/** + * Remove a task from the queue. Return APR_SUCCESS if the task + * was indeed queued, APR_NOTFOUND otherwise. + * @param q the queue to remove from + * @param task the task to remove + */ +apr_status_t h2_tq_remove(h2_task_queue *q, struct h2_task *task); + +/** + * Get the first task from the queue or NULL if the queue is empty. The + * task will be removed. + * @param q the queue to pop the first task from + */ +h2_task *h2_tq_pop_first(h2_task_queue *q); + +/******************************************************************************* + * Queue Manipulation. + ******************************************************************************/ + +/** + * The magic pointer value that indicates the head of a h2_task_queue list + * @param b The queue list + * @return The magic pointer value + */ +#define H2_TQ_LIST_SENTINEL(b) APR_RING_SENTINEL((b), h2_task_queue, link) + +/** + * Determine if the queue list is empty + * @param b The list to check + * @return true or false + */ +#define H2_TQ_LIST_EMPTY(b) APR_RING_EMPTY((b), h2_task_queue, link) + +/** + * Return the first queue in a list + * @param b The list to query + * @return The first queue in the list + */ +#define H2_TQ_LIST_FIRST(b) APR_RING_FIRST(b) + +/** + * Return the last queue in a list + * @param b The list to query + * @return The last queue int he list + */ +#define H2_TQ_LIST_LAST(b) APR_RING_LAST(b) + +/** + * Insert a single queue at the front of a list + * @param b The list to add to + * @param e The queue to insert + */ +#define H2_TQ_LIST_INSERT_HEAD(b, e) do { \ +h2_task_queue *ap__b = (e); \ +APR_RING_INSERT_HEAD((b), ap__b, h2_task_queue, link); \ +} while (0) + +/** + * Insert a single queue at the end of a list + * @param b The list to add to + * @param e The queue to insert + */ +#define H2_TQ_LIST_INSERT_TAIL(b, e) do { \ +h2_task_queue *ap__b = (e); \ +APR_RING_INSERT_TAIL((b), ap__b, h2_task_queue, link); \ +} while (0) + +/** + * Get the next queue in the list + * @param e The current queue + * @return The next queue + */ +#define H2_TQ_NEXT(e) APR_RING_NEXT((e), link) +/** + * Get the previous queue in the list + * @param e The current queue + * @return The previous queue + */ +#define H2_TQ_PREV(e) APR_RING_PREV((e), link) + +/** + * Remove a queue from its list + * @param e The queue to remove + */ +#define H2_TQ_REMOVE(e) APR_RING_REMOVE((e), link) + + +#define H2_TQ_EMPTY(e) H2_TASK_LIST_EMPTY(&(e)->tasks) + +#endif /* defined(__mod_h2__h2_task_queue__) */ diff --git a/modules/http2/h2_to_h1.c b/modules/http2/h2_to_h1.c new file mode 100644 index 00000000000..8dacfe801fc --- /dev/null +++ b/modules/http2/h2_to_h1.c @@ -0,0 +1,305 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "h2_private.h" +#include "h2_mplx.h" +#include "h2_response.h" +#include "h2_task.h" +#include "h2_to_h1.h" +#include "h2_util.h" + + +h2_to_h1 *h2_to_h1_create(int stream_id, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc, + const char *method, + const char *scheme, + const char *authority, + const char *path, + struct h2_mplx *m) +{ + h2_to_h1 *to_h1; + if (!method) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, m->c, + APLOGNO(02943) + "h2_to_h1: header start but :method missing"); + return NULL; + } + if (!path) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, m->c, + APLOGNO(02944) + "h2_to_h1: header start but :path missing"); + return NULL; + } + + to_h1 = apr_pcalloc(pool, sizeof(h2_to_h1)); + if (to_h1) { + to_h1->stream_id = stream_id; + to_h1->pool = pool; + to_h1->method = method; + to_h1->scheme = scheme; + to_h1->authority = authority; + to_h1->path = path; + to_h1->m = m; + to_h1->headers = apr_table_make(to_h1->pool, 10); + to_h1->bb = apr_brigade_create(pool, bucket_alloc); + to_h1->chunked = 0; /* until we see a content-type and no length */ + to_h1->content_len = -1; + } + return to_h1; +} + +void h2_to_h1_destroy(h2_to_h1 *to_h1) +{ + to_h1->bb = NULL; +} + +apr_status_t h2_to_h1_add_header(h2_to_h1 *to_h1, + const char *name, size_t nlen, + const char *value, size_t vlen) +{ + char *hname, *hvalue; + if (H2_HD_MATCH_LIT("transfer-encoding", name, nlen)) { + if (!apr_strnatcasecmp("chunked", value)) { + /* This should never arrive here in a HTTP/2 request */ + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_BADARG, to_h1->m->c, + APLOGNO(02945) + "h2_to_h1: 'transfer-encoding: chunked' received"); + return APR_BADARG; + } + } + else if (H2_HD_MATCH_LIT("content-length", name, nlen)) { + char *end; + to_h1->content_len = apr_strtoi64(value, &end, 10); + if (value == end) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, to_h1->m->c, + APLOGNO(02959) + "h2_request(%d): content-length value not parsed: %s", + to_h1->stream_id, value); + return APR_EINVAL; + } + to_h1->remain_len = to_h1->content_len; + to_h1->chunked = 0; + } + else if (H2_HD_MATCH_LIT("content-type", name, nlen)) { + /* If we see a content-type and have no length (yet), + * we need to chunk. */ + to_h1->chunked = (to_h1->content_len == -1); + } + else if ((to_h1->seen_host && H2_HD_MATCH_LIT("host", name, nlen)) + || H2_HD_MATCH_LIT("expect", name, nlen) + || H2_HD_MATCH_LIT("upgrade", name, nlen) + || H2_HD_MATCH_LIT("connection", name, nlen) + || H2_HD_MATCH_LIT("proxy-connection", name, nlen) + || H2_HD_MATCH_LIT("keep-alive", name, nlen) + || H2_HD_MATCH_LIT("http2-settings", name, nlen)) { + /* ignore these. */ + return APR_SUCCESS; + } + else if (H2_HD_MATCH_LIT("cookie", name, nlen)) { + const char *existing = apr_table_get(to_h1->headers, "cookie"); + if (existing) { + char *nval; + + /* Cookie headers come separately in HTTP/2, but need + * to be merged by "; " (instead of default ", ") + */ + hvalue = apr_pstrndup(to_h1->pool, value, vlen); + nval = apr_psprintf(to_h1->pool, "%s; %s", existing, hvalue); + apr_table_setn(to_h1->headers, "Cookie", nval); + return APR_SUCCESS; + } + } + else if (H2_HD_MATCH_LIT("host", name, nlen)) { + to_h1->seen_host = 1; + } + + hname = apr_pstrndup(to_h1->pool, name, nlen); + hvalue = apr_pstrndup(to_h1->pool, value, vlen); + h2_util_camel_case_header(hname, nlen); + apr_table_mergen(to_h1->headers, hname, hvalue); + + return APR_SUCCESS; +} + +static int set_header(void *ctx, const char *key, const char *value) +{ + h2_to_h1 *to_h1 = (h2_to_h1*)ctx; + h2_to_h1_add_header(to_h1, key, strlen(key), value, strlen(value)); + return 1; +} + +apr_status_t h2_to_h1_add_headers(h2_to_h1 *to_h1, apr_table_t *headers) +{ + apr_table_do(set_header, to_h1, headers, NULL); + return APR_SUCCESS; +} + +apr_status_t h2_to_h1_end_headers(h2_to_h1 *to_h1, h2_task *task, int eos) +{ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, to_h1->m->c, + "h2_to_h1(%ld-%d): end headers", + to_h1->m->id, to_h1->stream_id); + + if (to_h1->eoh) { + return APR_EINVAL; + } + + if (!to_h1->seen_host) { + /* Need to add a "Host" header if not already there to + * make virtual hosts work correctly. */ + if (!to_h1->authority) { + return APR_BADARG; + } + apr_table_set(to_h1->headers, "Host", to_h1->authority); + } + + if (eos && to_h1->chunked) { + /* We had chunking figured out, but the EOS is already there. + * unmark chunking and set a definitive content-length. + */ + to_h1->chunked = 0; + apr_table_setn(to_h1->headers, "Content-Length", "0"); + } + else if (to_h1->chunked) { + /* We have not seen a content-length. We therefore must + * pass any request content in chunked form. + */ + apr_table_mergen(to_h1->headers, "Transfer-Encoding", "chunked"); + } + + h2_task_set_request(task, to_h1->method, + to_h1->scheme, + to_h1->authority, + to_h1->path, + to_h1->headers, eos); + to_h1->eoh = 1; + + if (eos) { + apr_status_t status = h2_to_h1_close(to_h1); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, to_h1->m->c, + APLOGNO(02960) + "h2_to_h1(%ld-%d): end headers, eos=%d", + to_h1->m->id, to_h1->stream_id, eos); + } + return status; + } + return APR_SUCCESS; +} + +static apr_status_t flush(apr_bucket_brigade *bb, void *ctx) +{ + (void)bb; + return h2_to_h1_flush((h2_to_h1*)ctx); +} + +static apr_status_t h2_to_h1_add_data_raw(h2_to_h1 *to_h1, + const char *data, size_t len) +{ + apr_status_t status = APR_SUCCESS; + + if (to_h1->eos || !to_h1->eoh) { + return APR_EINVAL; + } + + status = apr_brigade_write(to_h1->bb, flush, to_h1, data, len); + if (status == APR_SUCCESS) { + status = h2_to_h1_flush(to_h1); + } + return status; +} + + +apr_status_t h2_to_h1_add_data(h2_to_h1 *to_h1, + const char *data, size_t len) +{ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, to_h1->m->c, + "h2_to_h1(%ld-%d): add %ld data bytes", + to_h1->m->id, to_h1->stream_id, (long)len); + + if (to_h1->chunked) { + /* if input may have a body and we have not seen any + * content-length header, we need to chunk the input data. + */ + apr_status_t status = apr_brigade_printf(to_h1->bb, NULL, NULL, + "%lx\r\n", (unsigned long)len); + if (status == APR_SUCCESS) { + status = h2_to_h1_add_data_raw(to_h1, data, len); + if (status == APR_SUCCESS) { + status = apr_brigade_puts(to_h1->bb, NULL, NULL, "\r\n"); + } + } + return status; + } + else { + to_h1->remain_len -= len; + if (to_h1->remain_len < 0) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, to_h1->m->c, + APLOGNO(02961) + "h2_to_h1(%ld-%d): got %ld more content bytes than announced " + "in content-length header: %ld", + to_h1->m->id, to_h1->stream_id, + (long)to_h1->content_len, -(long)to_h1->remain_len); + } + return h2_to_h1_add_data_raw(to_h1, data, len); + } +} + +apr_status_t h2_to_h1_flush(h2_to_h1 *to_h1) +{ + apr_status_t status = APR_SUCCESS; + if (!APR_BRIGADE_EMPTY(to_h1->bb)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, to_h1->m->c, + "h2_to_h1(%ld-%d): flush request bytes", + to_h1->m->id, to_h1->stream_id); + + status = h2_mplx_in_write(to_h1->m, to_h1->stream_id, to_h1->bb); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, to_h1->m->c, + APLOGNO(02946) "h2_request(%d): pushing request data", + to_h1->stream_id); + } + } + return status; +} + +apr_status_t h2_to_h1_close(h2_to_h1 *to_h1) +{ + apr_status_t status = APR_SUCCESS; + if (!to_h1->eos) { + if (to_h1->chunked) { + status = h2_to_h1_add_data_raw(to_h1, "0\r\n\r\n", 5); + } + to_h1->eos = 1; + status = h2_to_h1_flush(to_h1); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, to_h1->m->c, + "h2_to_h1(%d): close", to_h1->stream_id); + + status = h2_mplx_in_close(to_h1->m, to_h1->stream_id); + } + return status; +} + + diff --git a/modules/http2/h2_to_h1.h b/modules/http2/h2_to_h1.h new file mode 100644 index 00000000000..74586e2b7ea --- /dev/null +++ b/modules/http2/h2_to_h1.h @@ -0,0 +1,87 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_to_h1__ +#define __mod_h2__h2_to_h1__ + +struct h2_mplx; +struct h2_task; +typedef struct h2_to_h1 h2_to_h1; + +struct h2_to_h1 { + int stream_id; + apr_pool_t *pool; + h2_mplx *m; + + const char *method; + const char *scheme; + const char *authority; + const char *path; + + int chunked; + int eoh; + int eos; + int flushed; + int seen_host; + + apr_off_t content_len; + apr_off_t remain_len; + apr_table_t *headers; + apr_bucket_brigade *bb; +}; + +/* Create a converter from a HTTP/2 request to a serialzation in + * HTTP/1.1 format. The serialized data will be written onto the + * given h2_mplx instance. + */ +h2_to_h1 *h2_to_h1_create(int stream_id, apr_pool_t *pool, + apr_bucket_alloc_t *bucket_alloc, + const char *method, + const char *scheme, + const char *authority, + const char *path, + struct h2_mplx *m); + +/* Destroy the converter and free resources. */ +void h2_to_h1_destroy(h2_to_h1 *to_h1); + +/* Add a header to the serialization. Only valid to call after start + * and before end_headers. + */ +apr_status_t h2_to_h1_add_header(h2_to_h1 *to_h1, + const char *name, size_t nlen, + const char *value, size_t vlen); + +apr_status_t h2_to_h1_add_headers(h2_to_h1 *to_h1, apr_table_t *headers); + +/** End the request headers. + */ +apr_status_t h2_to_h1_end_headers(h2_to_h1 *to_h1, + struct h2_task *task, int eos); + +/* Add request body data. + */ +apr_status_t h2_to_h1_add_data(h2_to_h1 *to_h1, + const char *data, size_t len); + +/* Flush the converted data onto the h2_mplx instance. + */ +apr_status_t h2_to_h1_flush(h2_to_h1 *to_h1); + +/* Close the request, flushed automatically. + */ +apr_status_t h2_to_h1_close(h2_to_h1 *to_h1); + +#endif /* defined(__mod_h2__h2_to_h1__) */ diff --git a/modules/http2/h2_util.c b/modules/http2/h2_util.c new file mode 100644 index 00000000000..c0c383797b4 --- /dev/null +++ b/modules/http2/h2_util.c @@ -0,0 +1,627 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include +#include + +#include + +#include "h2_private.h" +#include "h2_util.h" + +size_t h2_util_hex_dump(char *buffer, size_t maxlen, + const char *data, size_t datalen) +{ + size_t offset = 0; + size_t maxoffset = (maxlen-4); + size_t i; + for (i = 0; i < datalen && offset < maxoffset; ++i) { + const char *sep = (i && i % 16 == 0)? "\n" : " "; + int n = apr_snprintf(buffer+offset, maxoffset-offset, + "%2x%s", ((unsigned int)data[i]&0xff), sep); + offset += n; + } + strcpy(buffer+offset, (i= 'A' && *p <= 'Z') { + *p += 'a' - 'A'; + } + } + return s; +} + +void h2_util_camel_case_header(char *s, size_t len) +{ + size_t start = 1; + size_t i; + for (i = 0; i < len; ++i) { + if (start) { + if (s[i] >= 'a' && s[i] <= 'z') { + s[i] -= 'a' - 'A'; + } + + start = 0; + } + else if (s[i] == '-') { + start = 1; + } + } +} + +static const int BASE64URL_TABLE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1 +}; + +apr_size_t h2_util_base64url_decode(const char **decoded, const char *encoded, + apr_pool_t *pool) +{ + const unsigned char *e = (const unsigned char *)encoded; + const unsigned char *p = e; + unsigned char *d; + int n; + apr_size_t len, mlen, remain, i; + + while (*p && BASE64URL_TABLE[ *p ] == -1) { + ++p; + } + len = p - e; + mlen = (len/4)*4; + *decoded = apr_pcalloc(pool, len+1); + + i = 0; + d = (unsigned char*)*decoded; + for (; i < mlen; i += 4) { + n = ((BASE64URL_TABLE[ e[i+0] ] << 18) + + (BASE64URL_TABLE[ e[i+1] ] << 12) + + (BASE64URL_TABLE[ e[i+2] ] << 6) + + BASE64URL_TABLE[ e[i+3] ]); + *d++ = n >> 16; + *d++ = n >> 8 & 0xffu; + *d++ = n & 0xffu; + } + remain = len - mlen; + switch (remain) { + case 2: + n = ((BASE64URL_TABLE[ e[mlen+0] ] << 18) + + (BASE64URL_TABLE[ e[mlen+1] ] << 12)); + *d++ = n >> 16; + break; + case 3: + n = ((BASE64URL_TABLE[ e[mlen+0] ] << 18) + + (BASE64URL_TABLE[ e[mlen+1] ] << 12) + + (BASE64URL_TABLE[ e[mlen+2] ] << 6)); + *d++ = n >> 16; + *d++ = n >> 8 & 0xffu; + break; + default: /* do nothing */ + break; + } + return len; +} + +int h2_util_contains_token(apr_pool_t *pool, const char *s, const char *token) +{ + char *c; + if (s) { + if (!apr_strnatcasecmp(s, token)) { /* the simple life */ + return 1; + } + + for (c = ap_get_token(pool, &s, 0); c && *c; + c = *s? ap_get_token(pool, &s, 0) : NULL) { + if (!apr_strnatcasecmp(c, token)) { /* seeing the token? */ + return 1; + } + while (*s++ == ';') { /* skip parameters */ + ap_get_token(pool, &s, 0); + } + if (*s++ != ',') { /* need comma separation */ + return 0; + } + } + } + return 0; +} + +const char *h2_util_first_token_match(apr_pool_t *pool, const char *s, + const char *tokens[], apr_size_t len) +{ + char *c; + apr_size_t i; + if (s && *s) { + for (c = ap_get_token(pool, &s, 0); c && *c; + c = *s? ap_get_token(pool, &s, 0) : NULL) { + for (i = 0; i < len; ++i) { + if (!apr_strnatcasecmp(c, tokens[i])) { + return tokens[i]; + } + } + while (*s++ == ';') { /* skip parameters */ + ap_get_token(pool, &s, 0); + } + if (*s++ != ',') { /* need comma separation */ + return 0; + } + } + } + return NULL; +} + +/* DEEP_COPY==0 crashes under load. I think the setaside is fine, + * however buckets moved to another thread will still be + * free'd against the old bucket_alloc. *And* if the old + * pool gets destroyed too early, the bucket disappears while + * still needed. + */ +static const int DEEP_COPY = 1; +static const int FILE_MOVE = 1; + +static apr_status_t last_not_included(apr_bucket_brigade *bb, + apr_size_t maxlen, + int same_alloc, + int *pfile_buckets_allowed, + apr_bucket **pend) +{ + apr_bucket *b; + apr_status_t status = APR_SUCCESS; + int files_allowed = pfile_buckets_allowed? *pfile_buckets_allowed : 0; + + if (maxlen > 0) { + /* Find the bucket, up to which we reach maxlen/mem bytes */ + for (b = APR_BRIGADE_FIRST(bb); + (b != APR_BRIGADE_SENTINEL(bb)); + b = APR_BUCKET_NEXT(b)) { + + if (APR_BUCKET_IS_METADATA(b)) { + /* included */ + } + else { + if (maxlen == 0) { + *pend = b; + return status; + } + + if (b->length == ((apr_size_t)-1)) { + const char *ign; + apr_size_t ilen; + status = apr_bucket_read(b, &ign, &ilen, APR_BLOCK_READ); + if (status != APR_SUCCESS) { + return status; + } + } + + if (same_alloc && APR_BUCKET_IS_FILE(b)) { + /* we like it move it, always */ + } + else if (files_allowed > 0 && APR_BUCKET_IS_FILE(b)) { + /* this has no memory footprint really unless + * it is read, disregard it in length count, + * unless we do not move the file buckets */ + --files_allowed; + } + else if (maxlen < b->length) { + apr_bucket_split(b, maxlen); + maxlen = 0; + } + else { + maxlen -= b->length; + } + } + } + } + *pend = APR_BRIGADE_SENTINEL(bb); + return status; +} + +#define LOG_BUCKETS 0 +#define LOG_LEVEL APLOG_INFO + +apr_status_t h2_util_move(apr_bucket_brigade *to, apr_bucket_brigade *from, + apr_size_t maxlen, int *pfile_handles_allowed, + const char *msg) +{ + apr_status_t status = APR_SUCCESS; + int same_alloc; + + AP_DEBUG_ASSERT(to); + AP_DEBUG_ASSERT(from); + same_alloc = (to->bucket_alloc == from->bucket_alloc); + + if (!FILE_MOVE) { + pfile_handles_allowed = NULL; + } + + if (!APR_BRIGADE_EMPTY(from)) { + apr_bucket *b, *end; + + status = last_not_included(from, maxlen, same_alloc, + pfile_handles_allowed, &end); + if (status != APR_SUCCESS) { + return status; + } + + while (!APR_BRIGADE_EMPTY(from) && status == APR_SUCCESS) { + b = APR_BRIGADE_FIRST(from); + if (b == end) { + break; + } + + if (same_alloc || (b->list == to->bucket_alloc)) { + /* both brigades use the same bucket_alloc and auto-cleanups + * have the same life time. It's therefore safe to just move + * directly. */ + APR_BUCKET_REMOVE(b); + APR_BRIGADE_INSERT_TAIL(to, b); +#if LOG_BUCKETS + ap_log_perror(APLOG_MARK, LOG_LEVEL, 0, to->p, + "h2_util_move: %s, passed bucket(same bucket_alloc) " + "%ld-%ld, type=%s", + msg, (long)b->start, (long)b->length, + APR_BUCKET_IS_METADATA(b)? + (APR_BUCKET_IS_EOS(b)? "EOS": + (APR_BUCKET_IS_FLUSH(b)? "FLUSH" : "META")) : + (APR_BUCKET_IS_FILE(b)? "FILE" : "DATA")); +#endif + } + else if (DEEP_COPY) { + /* we have not managed the magic of passing buckets from + * one thread to another. Any attempts result in + * cleanup of pools scrambling memory. + */ + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + APR_BRIGADE_INSERT_TAIL(to, apr_bucket_eos_create(to->bucket_alloc)); + } + else if (APR_BUCKET_IS_FLUSH(b)) { + APR_BRIGADE_INSERT_TAIL(to, apr_bucket_flush_create(to->bucket_alloc)); + } + else { + /* ignore */ + } + } + else if (pfile_handles_allowed + && *pfile_handles_allowed > 0 + && APR_BUCKET_IS_FILE(b)) { + /* We do not want to read files when passing buckets, if + * we can avoid it. However, what we've come up so far + * is not working corrently, resulting either in crashes or + * too many open file descriptors. + */ + apr_bucket_file *f = (apr_bucket_file *)b->data; + apr_file_t *fd = f->fd; + int setaside = (f->readpool != to->p); +#if LOG_BUCKETS + ap_log_perror(APLOG_MARK, LOG_LEVEL, 0, to->p, + "h2_util_move: %s, moving FILE bucket %ld-%ld " + "from=%lx(p=%lx) to=%lx(p=%lx), setaside=%d", + msg, (long)b->start, (long)b->length, + (long)from, (long)from->p, + (long)to, (long)to->p, setaside); +#endif + if (setaside) { + status = apr_file_setaside(&fd, fd, to->p); + if (status != APR_SUCCESS) { + ap_log_perror(APLOG_MARK, APLOG_ERR, status, to->p, + APLOGNO(02947) "h2_util: %s, setaside FILE", + msg); + return status; + } + } + apr_brigade_insert_file(to, fd, b->start, b->length, + to->p); + --(*pfile_handles_allowed); + } + else { + const char *data; + apr_size_t len; + status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (status == APR_SUCCESS && len > 0) { + status = apr_brigade_write(to, NULL, NULL, data, len); +#if LOG_BUCKETS + ap_log_perror(APLOG_MARK, LOG_LEVEL, 0, to->p, + "h2_util_move: %s, copied bucket %ld-%ld " + "from=%lx(p=%lx) to=%lx(p=%lx)", + msg, (long)b->start, (long)b->length, + (long)from, (long)from->p, + (long)to, (long)to->p); +#endif + } + } + apr_bucket_delete(b); + } + else { + apr_bucket_setaside(b, to->p); + APR_BUCKET_REMOVE(b); + APR_BRIGADE_INSERT_TAIL(to, b); +#if LOG_BUCKETS + ap_log_perror(APLOG_MARK, LOG_LEVEL, 0, to->p, + "h2_util_move: %s, passed setaside bucket %ld-%ld " + "from=%lx(p=%lx) to=%lx(p=%lx)", + msg, (long)b->start, (long)b->length, + (long)from, (long)from->p, + (long)to, (long)to->p); +#endif + } + } + } + + return status; +} + +apr_status_t h2_util_copy(apr_bucket_brigade *to, apr_bucket_brigade *from, + apr_size_t maxlen, const char *msg) +{ + apr_status_t status = APR_SUCCESS; + int same_alloc; + + (void)msg; + AP_DEBUG_ASSERT(to); + AP_DEBUG_ASSERT(from); + same_alloc = (to->bucket_alloc == from->bucket_alloc); + + if (!APR_BRIGADE_EMPTY(from)) { + apr_bucket *b, *end, *cpy; + + status = last_not_included(from, maxlen, 0, 0, &end); + if (status != APR_SUCCESS) { + return status; + } + + for (b = APR_BRIGADE_FIRST(from); + b != APR_BRIGADE_SENTINEL(from) && b != end; + b = APR_BUCKET_NEXT(b)) + { + if (same_alloc) { + status = apr_bucket_copy(b, &cpy); + if (status != APR_SUCCESS) { + break; + } + APR_BRIGADE_INSERT_TAIL(to, cpy); + } + else { + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + APR_BRIGADE_INSERT_TAIL(to, apr_bucket_eos_create(to->bucket_alloc)); + } + else if (APR_BUCKET_IS_FLUSH(b)) { + APR_BRIGADE_INSERT_TAIL(to, apr_bucket_flush_create(to->bucket_alloc)); + } + else { + /* ignore */ + } + } + else { + const char *data; + apr_size_t len; + status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (status == APR_SUCCESS && len > 0) { + status = apr_brigade_write(to, NULL, NULL, data, len); +#if LOG_BUCKETS + ap_log_perror(APLOG_MARK, LOG_LEVEL, 0, to->p, + "h2_util_copy: %s, copied bucket %ld-%ld " + "from=%lx(p=%lx) to=%lx(p=%lx)", + msg, (long)b->start, (long)b->length, + (long)from, (long)from->p, + (long)to, (long)to->p); +#endif + } + } + } + } + } + return status; +} + +int h2_util_has_flush_or_eos(apr_bucket_brigade *bb) { + apr_bucket *b; + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) + { + if (APR_BUCKET_IS_EOS(b) || APR_BUCKET_IS_FLUSH(b)) { + return 1; + } + } + return 0; +} + +int h2_util_has_eos(apr_bucket_brigade *bb, apr_size_t len) +{ + apr_bucket *b, *end; + + apr_status_t status = last_not_included(bb, len, 0, 0, &end); + if (status != APR_SUCCESS) { + return status; + } + + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb) && b != end; + b = APR_BUCKET_NEXT(b)) + { + if (APR_BUCKET_IS_EOS(b)) { + return 1; + } + } + return 0; +} + +int h2_util_bb_has_data(apr_bucket_brigade *bb) +{ + apr_bucket *b; + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) + { + if (!APR_BUCKET_IS_METADATA(b)) { + return 1; + } + } + return 0; +} + +int h2_util_bb_has_data_or_eos(apr_bucket_brigade *bb) +{ + apr_bucket *b; + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) + { + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + return 1; + } + } + else { + return 1; + } + } + return 0; +} + +apr_status_t h2_util_bb_avail(apr_bucket_brigade *bb, + apr_size_t *plen, int *peos) +{ + apr_status_t status; + /* test read to determine available length */ + apr_off_t blen = 0; + status = apr_brigade_length(bb, 0, &blen); + if (blen < (apr_off_t)*plen) { + *plen = blen; + } + *peos = h2_util_has_eos(bb, *plen); + return status; +} + +apr_status_t h2_util_bb_readx(apr_bucket_brigade *bb, + h2_util_pass_cb *cb, void *ctx, + apr_size_t *plen, int *peos) +{ + apr_status_t status = APR_SUCCESS; + int consume = (cb != NULL); + apr_size_t written = 0; + apr_size_t avail = *plen; + apr_bucket *next, *b; + + /* Pass data in our brigade through the callback until the length + * is satisfied or we encounter an EOS. + */ + *peos = 0; + for (b = APR_BRIGADE_FIRST(bb); + (status == APR_SUCCESS) && (b != APR_BRIGADE_SENTINEL(bb)); + b = next) { + + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + *peos = 1; + } + else { + /* ignore */ + } + } + else if (avail <= 0) { + break; + } + else { + const char *data = NULL; + apr_size_t data_len; + + if (b->length == ((apr_size_t)-1)) { + /* read to determine length */ + status = apr_bucket_read(b, &data, &data_len, + APR_NONBLOCK_READ); + } + else { + data_len = b->length; + } + + if (data_len > avail) { + apr_bucket_split(b, avail); + data_len = avail; + } + + if (consume) { + if (!data) { + status = apr_bucket_read(b, &data, &data_len, + APR_NONBLOCK_READ); + } + if (status == APR_SUCCESS) { + status = cb(ctx, data, data_len); + } + } + else { + data_len = b->length; + } + avail -= data_len; + written += data_len; + } + + next = APR_BUCKET_NEXT(b); + if (consume) { + apr_bucket_delete(b); + } + } + + *plen = written; + if (status == APR_SUCCESS && !*peos && !*plen) { + return APR_EAGAIN; + } + return status; +} + diff --git a/modules/http2/h2_util.h b/modules/http2/h2_util.h new file mode 100644 index 00000000000..9a1b5c6d35b --- /dev/null +++ b/modules/http2/h2_util.h @@ -0,0 +1,124 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_util__ +#define __mod_h2__h2_util__ + +struct nghttp2_frame; + +size_t h2_util_hex_dump(char *buffer, size_t maxlen, + const char *data, size_t datalen); + +size_t h2_util_header_print(char *buffer, size_t maxlen, + const char *name, size_t namelen, + const char *value, size_t valuelen); + +char *h2_strlwr(char *s); + +void h2_util_camel_case_header(char *s, size_t len); + +/** + * Return != 0 iff the string s contains the token, as specified in + * HTTP header syntax, rfc7230. + */ +int h2_util_contains_token(apr_pool_t *pool, const char *s, const char *token); + +const char *h2_util_first_token_match(apr_pool_t *pool, const char *s, + const char *tokens[], apr_size_t len); + +/** + * I always wanted to write my own base64url decoder...not. See + * https://tools.ietf.org/html/rfc4648#section-5 for description. + */ +apr_size_t h2_util_base64url_decode(const char **decoded, + const char *encoded, + apr_pool_t *pool); + +#define H2_HD_MATCH_LIT(l, name, nlen) \ + ((nlen == sizeof(l) - 1) && !apr_strnatcasecmp(l, name)) + +#define H2_HD_MATCH_LIT_CS(l, name) \ + ((strlen(name) == sizeof(l) - 1) && !apr_strnatcasecmp(l, name)) + +#define H2_CREATE_NV_LIT_CS(nv, NAME, VALUE) nv->name = (uint8_t *)NAME; \ + nv->namelen = sizeof(NAME) - 1; \ + nv->value = (uint8_t *)VALUE; \ + nv->valuelen = strlen(VALUE) + +#define H2_CREATE_NV_CS_LIT(nv, NAME, VALUE) nv->name = (uint8_t *)NAME; \ + nv->namelen = strlen(NAME); \ + nv->value = (uint8_t *)VALUE; \ + nv->valuelen = sizeof(VALUE) - 1 + +#define H2_CREATE_NV_CS_CS(nv, NAME, VALUE) nv->name = (uint8_t *)NAME; \ + nv->namelen = strlen(NAME); \ + nv->value = (uint8_t *)VALUE; \ + nv->valuelen = strlen(VALUE) + +/** + * Moves data from one brigade into another. If maxlen > 0, it only + * moves up to maxlen bytes into the target brigade, making bucket splits + * if needed. + * @param to the brigade to move the data to + * @param from the brigade to get the data from + * @param maxlen of bytes to move, 0 for all + * @param pfile_buckets_allowed how many file buckets may be moved, + * may be 0 or NULL + * @param msg message for use in logging + */ +apr_status_t h2_util_move(apr_bucket_brigade *to, apr_bucket_brigade *from, + apr_size_t maxlen, int *pfile_buckets_allowed, + const char *msg); + +/** + * Copies buckets from one brigade into another. If maxlen > 0, it only + * copies up to maxlen bytes into the target brigade, making bucket splits + * if needed. + * @param to the brigade to copy the data to + * @param from the brigade to get the data from + * @param maxlen of bytes to copy, 0 for all + * @param msg message for use in logging + */ +apr_status_t h2_util_copy(apr_bucket_brigade *to, apr_bucket_brigade *from, + apr_size_t maxlen, const char *msg); + +/** + * Return != 0 iff there is a FLUSH or EOS bucket in the brigade. + * @param bb the brigade to check on + * @return != 0 iff brigade holds FLUSH or EOS bucket (or both) + */ +int h2_util_has_flush_or_eos(apr_bucket_brigade *bb); +int h2_util_has_eos(apr_bucket_brigade *bb, apr_size_t len); +int h2_util_bb_has_data(apr_bucket_brigade *bb); +int h2_util_bb_has_data_or_eos(apr_bucket_brigade *bb); + +/** + * Check how many bytes of the desired amount are available and if the + * end of stream is reached by that amount. + * @param bb the brigade to check + * @param plen the desired length and, on return, the available length + * @param on return, if eos has been reached + */ +apr_status_t h2_util_bb_avail(apr_bucket_brigade *bb, + apr_size_t *plen, int *peos); + +typedef apr_status_t h2_util_pass_cb(void *ctx, + const char *data, apr_size_t len); + +apr_status_t h2_util_bb_readx(apr_bucket_brigade *bb, + h2_util_pass_cb *cb, void *ctx, + apr_size_t *plen, int *peos); + +#endif /* defined(__mod_h2__h2_util__) */ diff --git a/modules/http2/h2_version.h b/modules/http2/h2_version.h new file mode 100644 index 00000000000..df4da02e744 --- /dev/null +++ b/modules/http2/h2_version.h @@ -0,0 +1,34 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 + +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#ifndef mod_h2_h2_version_h +#define mod_h2_h2_version_h + +/** + * @macro + * Version number of the h2 module as c string + */ +#define MOD_H2_VERSION "0.9.7" + +/** + * @macro + * Numerical representation of the version number of the h2 module + * release. This is a 24 bit number with 8 bits for major number, 8 bits + * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. + */ +#define MOD_H2_VERSION_NUM 0x000907 + + +#endif /* mod_h2_h2_version_h */ diff --git a/modules/http2/h2_worker.c b/modules/http2/h2_worker.c new file mode 100644 index 00000000000..8145b7aaa5f --- /dev/null +++ b/modules/http2/h2_worker.c @@ -0,0 +1,170 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include +#include + +#include "h2_private.h" +#include "h2_mplx.h" +#include "h2_task.h" +#include "h2_worker.h" + +static void* APR_THREAD_FUNC execute(apr_thread_t *thread, void *wctx) +{ + h2_worker *worker = (h2_worker *)wctx; + apr_status_t status = APR_SUCCESS; + h2_mplx *m; + (void)thread; + + /* Furthermore, other code might want to see the socket for + * this connection. Allocate one without further function... + */ + status = apr_socket_create(&worker->socket, + APR_INET, SOCK_STREAM, + APR_PROTO_TCP, worker->pool); + if (status != APR_SUCCESS) { + ap_log_perror(APLOG_MARK, APLOG_ERR, status, worker->pool, + APLOGNO(02948) "h2_worker(%d): alloc socket", + worker->id); + worker->worker_done(worker, worker->ctx); + return NULL; + } + + worker->task = NULL; + m = NULL; + while (!worker->aborted) { + status = worker->get_next(worker, &m, &worker->task, worker->ctx); + + if (worker->task) { + h2_task_do(worker->task, worker); + worker->task = NULL; + apr_thread_cond_signal(h2_worker_get_cond(worker)); + } + } + + status = worker->get_next(worker, &m, NULL, worker->ctx); + m = NULL; + + if (worker->socket) { + apr_socket_close(worker->socket); + worker->socket = NULL; + } + + worker->worker_done(worker, worker->ctx); + return NULL; +} + +h2_worker *h2_worker_create(int id, + apr_pool_t *parent_pool, + apr_threadattr_t *attr, + h2_worker_mplx_next_fn *get_next, + h2_worker_done_fn *worker_done, + void *ctx) +{ + apr_allocator_t *allocator = NULL; + apr_pool_t *pool = NULL; + h2_worker *w; + + apr_status_t status = apr_allocator_create(&allocator); + if (status != APR_SUCCESS) { + return NULL; + } + + status = apr_pool_create_ex(&pool, parent_pool, NULL, allocator); + if (status != APR_SUCCESS) { + return NULL; + } + apr_allocator_owner_set(allocator, pool); + + w = apr_pcalloc(pool, sizeof(h2_worker)); + if (w) { + APR_RING_ELEM_INIT(w, link); + + w->id = id; + w->pool = pool; + w->bucket_alloc = apr_bucket_alloc_create(pool); + + w->get_next = get_next; + w->worker_done = worker_done; + w->ctx = ctx; + + status = apr_thread_cond_create(&w->io, w->pool); + if (status != APR_SUCCESS) { + return NULL; + } + + apr_thread_create(&w->thread, attr, execute, w, pool); + } + return w; +} + +apr_status_t h2_worker_destroy(h2_worker *worker) +{ + if (worker->io) { + apr_thread_cond_destroy(worker->io); + worker->io = NULL; + } + if (worker->pool) { + apr_pool_destroy(worker->pool); + /* worker is gone */ + } + return APR_SUCCESS; +} + +int h2_worker_get_id(h2_worker *worker) +{ + return worker->id; +} + +void h2_worker_abort(h2_worker *worker) +{ + worker->aborted = 1; +} + +int h2_worker_is_aborted(h2_worker *worker) +{ + return worker->aborted; +} + +apr_thread_t *h2_worker_get_thread(h2_worker *worker) +{ + return worker->thread; +} + +apr_thread_cond_t *h2_worker_get_cond(h2_worker *worker) +{ + return worker->io; +} + +apr_socket_t *h2_worker_get_socket(h2_worker *worker) +{ + return worker->socket; +} + +apr_pool_t *h2_worker_get_pool(h2_worker *worker) +{ + return worker->pool; +} + +apr_bucket_alloc_t *h2_worker_get_bucket_alloc(h2_worker *worker) +{ + return worker->bucket_alloc; +} + diff --git a/modules/http2/h2_worker.h b/modules/http2/h2_worker.h new file mode 100644 index 00000000000..9c69e6b57a8 --- /dev/null +++ b/modules/http2/h2_worker.h @@ -0,0 +1,155 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_worker__ +#define __mod_h2__h2_worker__ + +struct apr_thread_cond_t; +struct h2_mplx; +struct h2_task; + +/* h2_worker is a basically a apr_thread_t that reads fromt he h2_workers + * task queue and runs h2_tasks it is given. + */ +typedef struct h2_worker h2_worker; + +/* Invoked when the worker wants a new task to process. Will block + * until a h2_mplx becomes available or the worker itself + * gets aborted (idle timeout, for example). */ +typedef apr_status_t h2_worker_mplx_next_fn(h2_worker *worker, + struct h2_mplx **pm, + struct h2_task **ptask, + void *ctx); + +/* Invoked just before the worker thread exits. */ +typedef void h2_worker_done_fn(h2_worker *worker, void *ctx); + + +struct h2_worker { + /** Links to the rest of the workers */ + APR_RING_ENTRY(h2_worker) link; + + int id; + apr_thread_t *thread; + apr_pool_t *pool; + apr_bucket_alloc_t *bucket_alloc; + struct apr_thread_cond_t *io; + apr_socket_t *socket; + + h2_worker_mplx_next_fn *get_next; + h2_worker_done_fn *worker_done; + void *ctx; + + int aborted; + struct h2_task *task; +}; + +/** + * The magic pointer value that indicates the head of a h2_worker list + * @param b The worker list + * @return The magic pointer value + */ +#define H2_WORKER_LIST_SENTINEL(b) APR_RING_SENTINEL((b), h2_worker, link) + +/** + * Determine if the worker list is empty + * @param b The list to check + * @return true or false + */ +#define H2_WORKER_LIST_EMPTY(b) APR_RING_EMPTY((b), h2_worker, link) + +/** + * Return the first worker in a list + * @param b The list to query + * @return The first worker in the list + */ +#define H2_WORKER_LIST_FIRST(b) APR_RING_FIRST(b) + +/** + * Return the last worker in a list + * @param b The list to query + * @return The last worker int he list + */ +#define H2_WORKER_LIST_LAST(b) APR_RING_LAST(b) + +/** + * Insert a single worker at the front of a list + * @param b The list to add to + * @param e The worker to insert + */ +#define H2_WORKER_LIST_INSERT_HEAD(b, e) do { \ + h2_worker *ap__b = (e); \ + APR_RING_INSERT_HEAD((b), ap__b, h2_worker, link); \ + } while (0) + +/** + * Insert a single worker at the end of a list + * @param b The list to add to + * @param e The worker to insert + */ +#define H2_WORKER_LIST_INSERT_TAIL(b, e) do { \ + h2_worker *ap__b = (e); \ + APR_RING_INSERT_TAIL((b), ap__b, h2_worker, link); \ + } while (0) + +/** + * Get the next worker in the list + * @param e The current worker + * @return The next worker + */ +#define H2_WORKER_NEXT(e) APR_RING_NEXT((e), link) +/** + * Get the previous worker in the list + * @param e The current worker + * @return The previous worker + */ +#define H2_WORKER_PREV(e) APR_RING_PREV((e), link) + +/** + * Remove a worker from its list + * @param e The worker to remove + */ +#define H2_WORKER_REMOVE(e) APR_RING_REMOVE((e), link) + + +/* Create a new worker with given id, pool and attributes, callbacks + * callback parameter. + */ +h2_worker *h2_worker_create(int id, + apr_pool_t *pool, + apr_threadattr_t *attr, + h2_worker_mplx_next_fn *get_next, + h2_worker_done_fn *worker_done, + void *ctx); + +apr_status_t h2_worker_destroy(h2_worker *worker); + +void h2_worker_abort(h2_worker *worker); + +int h2_worker_get_id(h2_worker *worker); + +int h2_worker_is_aborted(h2_worker *worker); + +apr_pool_t *h2_worker_get_pool(h2_worker *worker); + +apr_bucket_alloc_t *h2_worker_get_bucket_alloc(h2_worker *worker); + +apr_socket_t *h2_worker_get_socket(h2_worker *worker); + +apr_thread_t *h2_worker_get_thread(h2_worker *worker); + +struct apr_thread_cond_t *h2_worker_get_cond(h2_worker *worker); + +#endif /* defined(__mod_h2__h2_worker__) */ diff --git a/modules/http2/h2_workers.c b/modules/http2/h2_workers.c new file mode 100644 index 00000000000..cf3009585b7 --- /dev/null +++ b/modules/http2/h2_workers.c @@ -0,0 +1,352 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "h2_private.h" +#include "h2_mplx.h" +#include "h2_task.h" +#include "h2_task_queue.h" +#include "h2_worker.h" +#include "h2_workers.h" + +static int in_list(h2_workers *workers, h2_mplx *m) +{ + h2_mplx *e; + for (e = H2_MPLX_LIST_FIRST(&workers->mplxs); + e != H2_MPLX_LIST_SENTINEL(&workers->mplxs); + e = H2_MPLX_NEXT(e)) { + if (e == m) { + return 1; + } + } + return 0; +} + + +/** + * Get the next task for the given worker. Will block until a task arrives + * or the max_wait timer expires and more than min workers exist. + * The previous h2_mplx instance might be passed in and will be served + * with preference, since we can ask it for the next task without aquiring + * the h2_workers lock. + */ +static apr_status_t get_mplx_next(h2_worker *worker, h2_mplx **pm, + h2_task **ptask, void *ctx) +{ + apr_status_t status; + h2_mplx *m = NULL; + h2_task *task = NULL; + apr_time_t max_wait, start_wait; + int has_more = 0; + h2_workers *workers = (h2_workers *)ctx; + + if (*pm && ptask != NULL) { + /* We have a h2_mplx instance and the worker wants the next task. + * Try to get one from the given mplx. */ + *ptask = h2_mplx_pop_task(*pm, &has_more); + if (*ptask) { + return APR_SUCCESS; + } + } + + if (*pm) { + /* Got a mplx handed in, but did not get or want a task from it. + * Release it, as the workers reference will be wiped. + */ + h2_mplx_release(*pm); + *pm = NULL; + } + + if (!ptask) { + /* the worker does not want a next task, we're done. + */ + return APR_SUCCESS; + } + + max_wait = apr_time_from_sec(apr_atomic_read32(&workers->max_idle_secs)); + start_wait = apr_time_now(); + + status = apr_thread_mutex_lock(workers->lock); + if (status == APR_SUCCESS) { + ++workers->idle_worker_count; + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s, + "h2_worker(%d): looking for work", h2_worker_get_id(worker)); + + while (!task && !h2_worker_is_aborted(worker) && !workers->aborted) { + + /* Get the next h2_mplx to process that has a task to hand out. + * If it does, place it at the end of the queu and return the + * task to the worker. + * If it (currently) has no tasks, remove it so that it needs + * to register again for scheduling. + * If we run out of h2_mplx in the queue, we need to wait for + * new mplx to arrive. Depending on how many workers do exist, + * we do a timed wait or block indefinitely. + */ + m = NULL; + while (!task && !H2_MPLX_LIST_EMPTY(&workers->mplxs)) { + m = H2_MPLX_LIST_FIRST(&workers->mplxs); + H2_MPLX_REMOVE(m); + + task = h2_mplx_pop_task(m, &has_more); + if (task) { + if (has_more) { + H2_MPLX_LIST_INSERT_TAIL(&workers->mplxs, m); + } + else { + has_more = !H2_MPLX_LIST_EMPTY(&workers->mplxs); + } + break; + } + } + + if (!task) { + /* Need to wait for either a new mplx to arrive. + */ + if (workers->worker_count > workers->min_size) { + apr_time_t now = apr_time_now(); + if (now >= (start_wait + max_wait)) { + /* waited long enough without getting a task. */ + status = APR_TIMEUP; + } + else { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s, + "h2_worker(%d): waiting signal, " + "worker_count=%d", worker->id, + (int)workers->worker_count); + status = apr_thread_cond_timedwait(workers->mplx_added, + workers->lock, max_wait); + } + + if (status == APR_TIMEUP) { + /* waited long enough */ + if (workers->worker_count > workers->min_size) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, + workers->s, + "h2_workers: aborting idle worker"); + h2_worker_abort(worker); + break; + } + } + } + else { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s, + "h2_worker(%d): waiting signal (eternal), " + "worker_count=%d", worker->id, + (int)workers->worker_count); + apr_thread_cond_wait(workers->mplx_added, workers->lock); + } + } + } + + /* Here, we either have gotten task and mplx for the worker or + * needed to give up with more than enough workers. + */ + if (task) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + "h2_worker(%d): start task(%s)", + h2_worker_get_id(worker), task->id); + /* Since we hand out a reference to the worker, we increase + * its ref count. + */ + h2_mplx_reference(m); + *pm = m; + *ptask = task; + + if (has_more && workers->idle_worker_count > 1) { + apr_thread_cond_signal(workers->mplx_added); + } + status = APR_SUCCESS; + } + else { + status = APR_EOF; + } + + --workers->idle_worker_count; + apr_thread_mutex_unlock(workers->lock); + } + + return status; +} + +static void worker_done(h2_worker *worker, void *ctx) +{ + h2_workers *workers = (h2_workers *)ctx; + apr_status_t status = apr_thread_mutex_lock(workers->lock); + if (status == APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + "h2_worker(%d): done", h2_worker_get_id(worker)); + H2_WORKER_REMOVE(worker); + --workers->worker_count; + h2_worker_destroy(worker); + + apr_thread_mutex_unlock(workers->lock); + } +} + + +static apr_status_t add_worker(h2_workers *workers) +{ + h2_worker *w = h2_worker_create(workers->next_worker_id++, + workers->pool, workers->thread_attr, + get_mplx_next, worker_done, workers); + if (!w) { + return APR_ENOMEM; + } + ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, workers->s, + "h2_workers: adding worker(%d)", h2_worker_get_id(w)); + ++workers->worker_count; + H2_WORKER_LIST_INSERT_TAIL(&workers->workers, w); + return APR_SUCCESS; +} + +static apr_status_t h2_workers_start(h2_workers *workers) { + apr_status_t status = apr_thread_mutex_lock(workers->lock); + if (status == APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + "h2_workers: starting"); + + while (workers->worker_count < workers->min_size + && status == APR_SUCCESS) { + status = add_worker(workers); + } + apr_thread_mutex_unlock(workers->lock); + } + return status; +} + +h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pool, + int min_size, int max_size) +{ + apr_status_t status; + h2_workers *workers; + AP_DEBUG_ASSERT(s); + AP_DEBUG_ASSERT(pool); + status = APR_SUCCESS; + + workers = apr_pcalloc(pool, sizeof(h2_workers)); + if (workers) { + workers->s = s; + workers->pool = pool; + workers->min_size = min_size; + workers->max_size = max_size; + apr_atomic_set32(&workers->max_idle_secs, 10); + + apr_threadattr_create(&workers->thread_attr, workers->pool); + + APR_RING_INIT(&workers->workers, h2_worker, link); + APR_RING_INIT(&workers->mplxs, h2_mplx, link); + + status = apr_thread_mutex_create(&workers->lock, + APR_THREAD_MUTEX_DEFAULT, + workers->pool); + if (status == APR_SUCCESS) { + status = apr_thread_cond_create(&workers->mplx_added, workers->pool); + } + + if (status == APR_SUCCESS) { + status = h2_workers_start(workers); + } + + if (status != APR_SUCCESS) { + h2_workers_destroy(workers); + workers = NULL; + } + } + return workers; +} + +void h2_workers_destroy(h2_workers *workers) +{ + if (workers->mplx_added) { + apr_thread_cond_destroy(workers->mplx_added); + workers->mplx_added = NULL; + } + if (workers->lock) { + apr_thread_mutex_destroy(workers->lock); + workers->lock = NULL; + } + while (!H2_MPLX_LIST_EMPTY(&workers->mplxs)) { + h2_mplx *m = H2_MPLX_LIST_FIRST(&workers->mplxs); + H2_MPLX_REMOVE(m); + } + while (!H2_WORKER_LIST_EMPTY(&workers->workers)) { + h2_worker *w = H2_WORKER_LIST_FIRST(&workers->workers); + H2_WORKER_REMOVE(w); + } +} + +apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m) +{ + apr_status_t status = apr_thread_mutex_lock(workers->lock); + if (status == APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, status, workers->s, + "h2_workers: register mplx(%ld)", m->id); + if (in_list(workers, m)) { + status = APR_EAGAIN; + } + else { + H2_MPLX_LIST_INSERT_TAIL(&workers->mplxs, m); + status = APR_SUCCESS; + } + + if (workers->idle_worker_count > 0) { + apr_thread_cond_signal(workers->mplx_added); + } + else if (workers->worker_count < workers->max_size) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s, + "h2_workers: got %d worker, adding 1", + workers->worker_count); + add_worker(workers); + } + + apr_thread_mutex_unlock(workers->lock); + } + return status; +} + +apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m) +{ + apr_status_t status = apr_thread_mutex_lock(workers->lock); + if (status == APR_SUCCESS) { + status = APR_EAGAIN; + if (in_list(workers, m)) { + H2_MPLX_REMOVE(m); + status = APR_SUCCESS; + } + apr_thread_mutex_unlock(workers->lock); + } + return status; +} + +void h2_workers_set_max_idle_secs(h2_workers *workers, int idle_secs) +{ + if (idle_secs <= 0) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, workers->s, + APLOGNO(02962) "h2_workers: max_worker_idle_sec value of %d" + " is not valid, ignored.", idle_secs); + return; + } + apr_atomic_set32(&workers->max_idle_secs, idle_secs); +} + diff --git a/modules/http2/h2_workers.h b/modules/http2/h2_workers.h new file mode 100644 index 00000000000..50fd6b8ad58 --- /dev/null +++ b/modules/http2/h2_workers.h @@ -0,0 +1,87 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_workers__ +#define __mod_h2__h2_workers__ + +/* Thread pool specific to executing h2_tasks. Has a minimum and maximum + * number of workers it creates. Starts with minimum workers and adds + * some on load, reduces the number again when idle. + * + */ +struct apr_thread_mutex_t; +struct apr_thread_cond_t; +struct h2_mplx; +struct h2_task; +struct h2_task_queue; + +typedef struct h2_workers h2_workers; + +struct h2_workers { + server_rec *s; + apr_pool_t *pool; + int aborted; + + int next_worker_id; + int min_size; + int max_size; + + apr_threadattr_t *thread_attr; + + APR_RING_HEAD(h2_worker_list, h2_worker) workers; + APR_RING_HEAD(h2_mplx_list, h2_mplx) mplxs; + + int worker_count; + volatile apr_uint32_t max_idle_secs; + volatile apr_uint32_t idle_worker_count; + + struct apr_thread_mutex_t *lock; + struct apr_thread_cond_t *mplx_added; +}; + + +/* Create a worker pool with the given minimum and maximum number of + * threads. + */ +h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pool, + int min_size, int max_size); + +/* Destroy the worker pool and all its threads. + */ +void h2_workers_destroy(h2_workers *workers); + +/** + * Registers a h2_mplx for task scheduling. If this h2_mplx runs + * out of tasks, it will be automatically be unregistered. Should + * new tasks arrive, it needs to be registered again. + */ +apr_status_t h2_workers_register(h2_workers *workers, + struct h2_mplx *m); + +/** + * Remove a h2_mplx from the worker registry. + */ +apr_status_t h2_workers_unregister(h2_workers *workers, + struct h2_mplx *m); + +/** + * Set the amount of seconds a h2_worker should wait for new tasks + * before shutting down (if there are more than the minimum number of + * workers). + */ +void h2_workers_set_max_idle_secs(h2_workers *workers, int idle_secs); + +#endif /* defined(__mod_h2__h2_workers__) */ diff --git a/modules/http2/mod_h2.c b/modules/http2/mod_h2.c new file mode 100644 index 00000000000..1c7cd2d8928 --- /dev/null +++ b/modules/http2/mod_h2.c @@ -0,0 +1,146 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include + +#include "mod_h2.h" + +#include +#include "h2_stream.h" +#include "h2_alt_svc.h" +#include "h2_conn.h" +#include "h2_task.h" +#include "h2_session.h" +#include "h2_config.h" +#include "h2_ctx.h" +#include "h2_h2.h" +#include "h2_switch.h" +#include "h2_version.h" + + +static void h2_hooks(apr_pool_t *pool); + +AP_DECLARE_MODULE(h2) = { + STANDARD20_MODULE_STUFF, + NULL, + NULL, + h2_config_create_svr, /* func to create per server config */ + h2_config_merge, /* func to merge per server config */ + h2_cmds, /* command handlers */ + h2_hooks +}; + +/* The module initialization. Called once as apache hook, before any multi + * processing (threaded or not) happens. It is typically at least called twice, + * see + * http://wiki.apache.org/httpd/ModuleLife + * Since the first run is just a "practise" run, we want to initialize for real + * only on the second try. This defeats the purpose of the first dry run a bit, + * since apache wants to verify that a new configuration actually will work. + * So if we have trouble with the configuration, this will only be detected + * when the server has already switched. + * On the other hand, when we initialize lib nghttp2, all possible crazy things + * might happen and this might even eat threads. So, better init on the real + * invocation, for now at least. + */ +static int h2_post_config(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *s) +{ + void *data = NULL; + const char *mod_h2_init_key = "mod_h2_init_counter"; + nghttp2_info *ngh2; + apr_status_t status; + (void)plog;(void)ptemp; + + apr_pool_userdata_get(&data, mod_h2_init_key, s->process->pool); + if ( data == NULL ) { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, + "initializing post config dry run"); + apr_pool_userdata_set((const void *)1, mod_h2_init_key, + apr_pool_cleanup_null, s->process->pool); + return APR_SUCCESS; + } + + ngh2 = nghttp2_version(0); + ap_log_error( APLOG_MARK, APLOG_INFO, 0, s, + "mod_h2 (v%s, nghttp2 %s), initializing...", + MOD_H2_VERSION, ngh2? ngh2->version_str : "unknown"); + + switch (h2_conn_mpm_type()) { + case H2_MPM_EVENT: + case H2_MPM_WORKER: + /* all fine, we know these ones */ + break; + case H2_MPM_PREFORK: + /* ok, we now know how to handle that one */ + break; + case H2_MPM_UNKNOWN: + /* ??? */ + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, + "post_config: mpm type unknown"); + break; + } + + status = h2_h2_init(p, s); + if (status == APR_SUCCESS) { + status = h2_switch_init(p, s); + } + + return status; +} + +/* Runs once per created child process. Perform any process + * related initionalization here. + */ +static void h2_child_init(apr_pool_t *pool, server_rec *s) +{ + /* Set up our connection processing */ + apr_status_t status = h2_conn_child_init(pool, s); + if (status != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, status, s, + APLOGNO(02949) "initializing connection handling"); + } +} + +/* Install this module into the apache2 infrastructure. + */ +static void h2_hooks(apr_pool_t *pool) +{ + static const char *const mod_ssl[] = { "mod_ssl.c", NULL}; + + ap_log_perror(APLOG_MARK, APLOG_INFO, 0, pool, "installing hooks"); + + /* Run once after configuration is set, but before mpm children initialize. + */ + ap_hook_post_config(h2_post_config, mod_ssl, NULL, APR_HOOK_MIDDLE); + + /* Run once after a child process has been created. + */ + ap_hook_child_init(h2_child_init, NULL, NULL, APR_HOOK_MIDDLE); + + h2_h2_register_hooks(); + h2_switch_register_hooks(); + h2_task_register_hooks(); + + h2_alt_svc_register_hooks(); + +} + + diff --git a/modules/http2/mod_h2.dsp b/modules/http2/mod_h2.dsp new file mode 100644 index 00000000000..7147e2d5631 --- /dev/null +++ b/modules/http2/mod_h2.dsp @@ -0,0 +1,211 @@ +# Microsoft Developer Studio Project File - Name="mod_h2" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 + +CFG=mod_h2 - Win32 Release +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "mod_h2.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "mod_h2.mak" CFG="mod_h2 - Win32 Release" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "mod_h2 - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE "mod_h2 - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "mod_h2 - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MD /W3 /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c +# ADD CPP /nologo /MD /W3 /O2 /Oy- /Zi /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/nghttp2/lib/includes" /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Release\mod_h2_src" /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /fo"Release/mod_h2.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d BIN_NAME="mod_h2.so" /d LONG_NAME="http2_module for Apache" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib nghttp2.lib /nologo /subsystem:windows /dll /libpath:"..\..\srclib\nghttp2\lib\MSVC_obj" /out:".\Release\mod_h2.so" /base:@..\..\os\win32\BaseAddr.ref,mod_h2.so +# ADD LINK32 kernel32.lib nghttp2.lib /nologo /subsystem:windows /dll /libpath:"..\..\srclib\nghttp2\lib\MSVC_obj" /incremental:no /debug /out:".\Release\mod_h2.so" /base:@..\..\os\win32\BaseAddr.ref,mod_h2.so /opt:ref +# Begin Special Build Tool +TargetPath=.\Release\mod_h2.so +SOURCE="$(InputPath)" +PostBuild_Desc=Embed .manifest +PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2 +# End Special Build Tool + +!ELSEIF "$(CFG)" == "mod_h2 - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MDd /W3 /EHsc /Zi /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c +# ADD CPP /nologo /MDd /W3 /EHsc /Zi /Od /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/nghttp2/lib/includes" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Debug\mod_h2_src" /FD /c +# ADD BASE MTL /nologo /D "_DEBUG" /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /fo"Debug/mod_h2.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d BIN_NAME="mod_h2.so" /d LONG_NAME="http2_module for Apache" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib nghttp2d.lib /nologo /subsystem:windows /dll /libpath:"..\..\srclib\nghttp2\lib\MSVC_obj" /incremental:no /debug /out:".\Debug\mod_h2.so" /base:@..\..\os\win32\BaseAddr.ref,mod_h2.so +# ADD LINK32 kernel32.lib nghttp2d.lib /nologo /subsystem:windows /dll /libpath:"..\..\srclib\nghttp2\lib\MSVC_obj" /incremental:no /debug /out:".\Debug\mod_h2.so" /base:@..\..\os\win32\BaseAddr.ref,mod_h2.so +# Begin Special Build Tool +TargetPath=.\Debug\mod_h2.so +SOURCE="$(InputPath)" +PostBuild_Desc=Embed .manifest +PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2 +# End Special Build Tool + +!ENDIF + +# Begin Target + +# Name "mod_h2 - Win32 Release" +# Name "mod_h2 - Win32 Debug" +# Begin Source File + +SOURCE=./h2_alt_svc.c +# End Source File +# Begin Source File + +SOURCE=./h2_config.c +# End Source File +# Begin Source File + +SOURCE=./h2_conn.c +# End Source File +# Begin Source File + +SOURCE=./h2_conn_io.c +# End Source File +# Begin Source File + +SOURCE=./h2_ctx.c +# End Source File +# Begin Source File + +SOURCE=./h2_from_h1.c +# End Source File +# Begin Source File + +SOURCE=./h2_h2.c +# End Source File +# Begin Source File + +SOURCE=./h2_io.c +# End Source File +# Begin Source File + +SOURCE=./h2_io_set.c +# End Source File +# Begin Source File + +SOURCE=./h2_mplx.c +# End Source File +# Begin Source File + +SOURCE=./h2_request.c +# End Source File +# Begin Source File + +SOURCE=./h2_response.c +# End Source File +# Begin Source File + +SOURCE=./h2_session.c +# End Source File +# Begin Source File + +SOURCE=./h2_stream.c +# End Source File +# Begin Source File + +SOURCE=./h2_stream_set.c +# End Source File +# Begin Source File + +SOURCE=./h2_switch.c +# End Source File +# Begin Source File + +SOURCE=./h2_task.c +# End Source File +# Begin Source File + +SOURCE=./h2_task_input.c +# End Source File +# Begin Source File + +SOURCE=./h2_task_output.c +# End Source File +# Begin Source File + +SOURCE=./h2_task_queue.c +# End Source File +# Begin Source File + +SOURCE=./h2_to_h1.c +# End Source File +# Begin Source File + +SOURCE=./h2_util.c +# End Source File +# Begin Source File + +SOURCE=./h2_worker.c +# End Source File +# Begin Source File + +SOURCE=./h2_workers.c +# End Source File +# Begin Source File + +SOURCE=./h2_workers.c +# End Source File +# Begin Source File + +SOURCE=./mod_h2.c +# End Source File +# Begin Source File + +SOURCE=..\..\build\win32\httpd.rc +# End Source File +# End Target +# End Project diff --git a/modules/http2/mod_h2.h b/modules/http2/mod_h2.h new file mode 100644 index 00000000000..bb895dd2f19 --- /dev/null +++ b/modules/http2/mod_h2.h @@ -0,0 +1,19 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef mod_h2_mod_h2_h +#define mod_h2_mod_h2_h + +#endif diff --git a/modules/ssl/ssl_engine_init.c b/modules/ssl/ssl_engine_init.c index 6baf2f514dc..72b458e8754 100644 --- a/modules/ssl/ssl_engine_init.c +++ b/modules/ssl/ssl_engine_init.c @@ -625,6 +625,10 @@ static void ssl_init_ctx_callbacks(server_rec *s, SSL_CTX_set_tmp_dh_callback(ctx, ssl_callback_TmpDH); SSL_CTX_set_info_callback(ctx, ssl_callback_Info); + +#ifdef HAVE_TLS_ALPN + SSL_CTX_set_alpn_select_cb(ctx, ssl_callback_alpn_select, NULL); +#endif } static apr_status_t ssl_init_ctx_verify(server_rec *s, diff --git a/modules/ssl/ssl_engine_io.c b/modules/ssl/ssl_engine_io.c index ce8abe5d54b..5ea76c6b673 100644 --- a/modules/ssl/ssl_engine_io.c +++ b/modules/ssl/ssl_engine_io.c @@ -297,6 +297,9 @@ typedef struct { apr_pool_t *pool; char buffer[AP_IOBUFSIZE]; ssl_filter_ctx_t *filter_ctx; +#ifdef HAVE_TLS_ALPN + int alpn_finished; /* 1 if ALPN has finished, 0 otherwise */ +#endif } bio_filter_in_ctx_t; /* @@ -1412,6 +1415,42 @@ static apr_status_t ssl_io_filter_input(ap_filter_t *f, APR_BRIGADE_INSERT_TAIL(bb, bucket); } +#ifdef HAVE_TLS_ALPN + /* By this point, Application-Layer Protocol Negotiation (ALPN) should be + * completed (if our version of OpenSSL supports it). If we haven't already, + * find out which protocol was decided upon and inform other modules + * by calling alpn_proto_negotiated_hook. + */ + if (!inctx->alpn_finished) { + SSLConnRec *sslconn = myConnConfig(f->c); + const unsigned char *next_proto = NULL; + unsigned next_proto_len = 0; + const char *protocol; + int n; + + SSL_get0_alpn_selected(inctx->ssl, &next_proto, &next_proto_len); + if (next_proto && next_proto_len) { + protocol = apr_pstrmemdup(f->c->pool, (const char *)next_proto, + next_proto_len); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, f->c, + APLOGNO(02836) "ALPN selected protocol: '%s'", + protocol); + + if (strcmp(protocol, ap_get_protocol(f->c))) { + status = ap_switch_protocol(f->c, NULL, sslconn->server, + protocol); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, f->c, + APLOGNO(02908) "protocol switch to '%s' failed", + protocol); + return status; + } + } + } + inctx->alpn_finished = 1; + } +#endif + return APR_SUCCESS; } @@ -1893,6 +1932,9 @@ static void ssl_io_input_add_filter(ssl_filter_ctx_t *filter_ctx, conn_rec *c, inctx->block = APR_BLOCK_READ; inctx->pool = c->pool; inctx->filter_ctx = filter_ctx; +#ifdef HAVE_TLS_ALPN + inctx->alpn_finished = 0; +#endif } /* The request_rec pointer is passed in here only to ensure that the diff --git a/modules/ssl/ssl_engine_kernel.c b/modules/ssl/ssl_engine_kernel.c index d0f9489a87b..cad7cc9e6f2 100644 --- a/modules/ssl/ssl_engine_kernel.c +++ b/modules/ssl/ssl_engine_kernel.c @@ -199,11 +199,18 @@ int ssl_hook_ReadReq(request_rec *r) if (rv != APR_SUCCESS || scope_id) { return HTTP_BAD_REQUEST; } - if (strcasecmp(host, servername)) { + if (strcasecmp(host, servername) + || !sslconn->server + || !ssl_util_vhost_matches(host, sslconn->server)) { + /* + * We are really not in Kansas anymore... + * The request hostname does not match the SNI and does not + * select the virtual host that was selected by the SNI. + */ ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, APLOGNO(02032) - "Hostname %s provided via SNI and hostname %s provided" - " via HTTP are different", servername, host); - return HTTP_BAD_REQUEST; + "Hostname %s provided via SNI and hostname %s provided" + " via HTTP are different", servername, host); + return HTTP_MISDIRECTED_REQUEST; } } else if (((sc->strict_sni_vhost_check == SSL_ENABLED_TRUE) @@ -1903,23 +1910,29 @@ void ssl_callback_Info(const SSL *ssl, int where, int rc) #ifdef HAVE_TLSEXT /* - * This callback function is executed when OpenSSL encounters an extended + * This function sets the virtual host from an extended * client hello with a server name indication extension ("SNI", cf. RFC 6066). */ -int ssl_callback_ServerNameIndication(SSL *ssl, int *al, modssl_ctx_t *mctx) +static apr_status_t init_vhost(conn_rec *c, SSL *ssl) { - const char *servername = - SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); - conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); - + const char *servername; + if (c) { + SSLConnRec *sslcon = myConnConfig(c); + + if (sslcon->server != c->base_server) { + /* already found the vhost */ + return APR_SUCCESS; + } + + servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); if (servername) { if (ap_vhost_iterate_given_conn(c, ssl_find_vhost, (void *)servername)) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(02043) "SSL virtual host for servername %s found", servername); - return SSL_TLSEXT_ERR_OK; + return APR_SUCCESS; } else { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(02044) @@ -1949,8 +1962,20 @@ int ssl_callback_ServerNameIndication(SSL *ssl, int *al, modssl_ctx_t *mctx) "(using default/first virtual host)"); } } + + return APR_NOTFOUND; +} - return SSL_TLSEXT_ERR_NOACK; +/* + * This callback function is executed when OpenSSL encounters an extended + * client hello with a server name indication extension ("SNI", cf. RFC 6066). + */ +int ssl_callback_ServerNameIndication(SSL *ssl, int *al, modssl_ctx_t *mctx) +{ + conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); + apr_status_t status = init_vhost(c, ssl); + + return (status == APR_SUCCESS)? SSL_TLSEXT_ERR_OK : SSL_TLSEXT_ERR_NOACK; } /* @@ -1962,50 +1987,10 @@ static int ssl_find_vhost(void *servername, conn_rec *c, server_rec *s) { SSLSrvConfigRec *sc; SSL *ssl; - BOOL found = FALSE; - apr_array_header_t *names; - int i; + BOOL found; SSLConnRec *sslcon; - /* check ServerName */ - if (!strcasecmp(servername, s->server_hostname)) { - found = TRUE; - } - - /* - * if not matched yet, check ServerAlias entries - * (adapted from vhost.c:matches_aliases()) - */ - if (!found) { - names = s->names; - if (names) { - char **name = (char **)names->elts; - for (i = 0; i < names->nelts; ++i) { - if (!name[i]) - continue; - if (!strcasecmp(servername, name[i])) { - found = TRUE; - break; - } - } - } - } - - /* if still no match, check ServerAlias entries with wildcards */ - if (!found) { - names = s->wild_names; - if (names) { - char **name = (char **)names->elts; - for (i = 0; i < names->nelts; ++i) { - if (!name[i]) - continue; - if (!ap_strcasecmp_match(servername, name[i])) { - found = TRUE; - break; - } - } - } - } + found = ssl_util_vhost_matches(servername, s); /* set SSL_CTX (if matched) */ sslcon = myConnConfig(c); @@ -2149,6 +2134,80 @@ int ssl_callback_SessionTicket(SSL *ssl, } #endif /* HAVE_TLS_SESSION_TICKETS */ + +#ifdef HAVE_TLS_ALPN +/* + * This callback function is executed when the TLS Application-Layer + * Protocol Negotiation Extension (ALPN, RFC 7301) is triggered by the Client + * Hello, giving a list of desired protocol names (in descending preference) + * to the server. + * The callback has to select a protocol name or return an error if none of + * the clients preferences is supported. + * The selected protocol does not have to be on the client list, according + * to RFC 7301, so no checks are performed. + * The client protocol list is serialized as length byte followed by ASCII + * characters (not null-terminated), followed by the next protocol name. + */ +int ssl_callback_alpn_select(SSL *ssl, + const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + void *arg) +{ + conn_rec *c = (conn_rec*)SSL_get_app_data(ssl); + SSLConnRec *sslconn = myConnConfig(c); + apr_array_header_t *client_protos; + const char *proposed; + size_t len; + int i; + + /* If the connection object is not available, + * then there's nothing for us to do. */ + if (c == NULL) { + return SSL_TLSEXT_ERR_OK; + } + + if (inlen == 0) { + // someone tries to trick us? + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02837) + "ALPN client protocol list empty"); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + client_protos = apr_array_make(c->pool, 0, sizeof(char *)); + for (i = 0; i < inlen; /**/) { + unsigned int plen = in[i++]; + if (plen + i > inlen) { + // someone tries to trick us? + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02838) + "ALPN protocol identifier too long"); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + APR_ARRAY_PUSH(client_protos, char *) = + apr_pstrndup(c->pool, (const char *)in+i, plen); + i += plen; + } + + /* The order the callbacks are invoked from TLS extensions is, unfortunately + * not defined and older openssl versions do call ALPN selection before + * they callback the SNI. We need to make sure that we know which vhost + * we are dealing with so we respect the correct protocols. + */ + init_vhost(c, ssl); + + proposed = ap_select_protocol(c, NULL, sslconn->server, client_protos); + *out = (const unsigned char *)(proposed? proposed : ap_get_protocol(c)); + len = strlen((const char*)*out); + if (len > 255) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02840) + "ALPN negotiated protocol name too long"); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + *outlen = (unsigned char)len; + + return SSL_TLSEXT_ERR_OK; +} +#endif /* HAVE_TLS_ALPN */ + #ifdef HAVE_SRP int ssl_callback_SRPServerParams(SSL *ssl, int *ad, void *arg) diff --git a/modules/ssl/ssl_private.h b/modules/ssl/ssl_private.h index 8f889971c15..1c91ffdf8d1 100644 --- a/modules/ssl/ssl_private.h +++ b/modules/ssl/ssl_private.h @@ -182,6 +182,11 @@ #include #endif +/* ALPN Protocol Negotiation */ +#if defined(TLSEXT_TYPE_application_layer_protocol_negotiation) +#define HAVE_TLS_ALPN +#endif + #endif /* !defined(OPENSSL_NO_TLSEXT) && defined(SSL_set_tlsext_host_name) */ /* mod_ssl headers */ @@ -798,6 +803,12 @@ int ssl_callback_SessionTicket(SSL *, unsigned char *, unsigned char *, EVP_CIPHER_CTX *, HMAC_CTX *, int); #endif +#ifdef HAVE_TLS_ALPN +int ssl_callback_alpn_select(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg); +#endif + /** Session Cache Support */ apr_status_t ssl_scache_init(server_rec *, apr_pool_t *); void ssl_scache_status_register(apr_pool_t *p); @@ -856,6 +867,8 @@ BOOL ssl_util_path_check(ssl_pathcheck_t, const char *, apr_pool_t *); void ssl_util_thread_setup(apr_pool_t *); int ssl_init_ssl_connection(conn_rec *c, request_rec *r); +BOOL ssl_util_vhost_matches(const char *servername, server_rec *s); + /** Pass Phrase Support */ apr_status_t ssl_load_encrypted_pkey(server_rec *, apr_pool_t *, int, const char *, apr_array_header_t **); diff --git a/modules/ssl/ssl_util.c b/modules/ssl/ssl_util.c index 476aa0b6d4b..ddde3c74ef5 100644 --- a/modules/ssl/ssl_util.c +++ b/modules/ssl/ssl_util.c @@ -60,6 +60,52 @@ char *ssl_util_vhostid(apr_pool_t *p, server_rec *s) return id; } +/* + * Return TRUE iff the given servername matches the server record when + * selecting virtual hosts. + */ +BOOL ssl_util_vhost_matches(const char *servername, server_rec *s) +{ + apr_array_header_t *names; + int i; + + /* check ServerName */ + if (!strcasecmp(servername, s->server_hostname)) { + return TRUE; + } + + /* + * if not matched yet, check ServerAlias entries + * (adapted from vhost.c:matches_aliases()) + */ + names = s->names; + if (names) { + char **name = (char **)names->elts; + for (i = 0; i < names->nelts; ++i) { + if (!name[i]) + continue; + if (!strcasecmp(servername, name[i])) { + return TRUE; + } + } + } + + /* if still no match, check ServerAlias entries with wildcards */ + names = s->wild_names; + if (names) { + char **name = (char **)names->elts; + for (i = 0; i < names->nelts; ++i) { + if (!name[i]) + continue; + if (!ap_strcasecmp_match(servername, name[i])) { + return TRUE; + } + } + } + + return FALSE; +} + apr_file_t *ssl_util_ppopen(server_rec *s, apr_pool_t *p, const char *cmd, const char * const *argv) { diff --git a/os/win32/BaseAddr.ref b/os/win32/BaseAddr.ref index 678e7be1d6f..1a221f87a14 100644 --- a/os/win32/BaseAddr.ref +++ b/os/win32/BaseAddr.ref @@ -127,3 +127,5 @@ mod_optional_fn_import.so 0x70BC0000 0x00010000 mod_optional_hook_export.so 0x70BD0000 0x00010000 mod_optional_hook_import.so 0x70BE0000 0x00010000 mod_authnz_fcgi.so 0x70BF0000 0x00020000 +mod_h2.so 0x70D00000 0x00020000 +>>>>>>> .merge-right.r1698023 diff --git a/server/core.c b/server/core.c index 802d8fdcdb8..ad777fadea0 100644 --- a/server/core.c +++ b/server/core.c @@ -423,6 +423,7 @@ static void *merge_core_dir_configs(apr_pool_t *a, void *basev, void *newv) static void *create_core_server_config(apr_pool_t *a, server_rec *s) { core_server_config *conf; + const char **np; int is_virtual = s->is_virtual; conf = (core_server_config *)apr_pcalloc(a, sizeof(core_server_config)); @@ -468,6 +469,9 @@ static void *create_core_server_config(apr_pool_t *a, server_rec *s) conf->trace_enable = AP_TRACE_UNSET; + conf->protocols = apr_array_make(a, 5, sizeof(const char *)); + conf->protocols_honor_order = -1; + return (void *)conf; } @@ -518,6 +522,12 @@ static void *merge_core_server_configs(apr_pool_t *p, void *basev, void *virtv) ? virt->merge_trailers : base->merge_trailers; + conf->protocols = ((virt->protocols->nelts > 0)? + virt->protocols : base->protocols); + conf->protocols_honor_order = ((virt->protocols_honor_order < 0)? + base->protocols_honor_order : + virt->protocols_honor_order); + return conf; } @@ -3692,6 +3702,48 @@ static const char *set_trace_enable(cmd_parms *cmd, void *dummy, return NULL; } +static const char *set_protocols(cmd_parms *cmd, void *dummy, + const char *arg) +{ + core_server_config *conf = + ap_get_core_module_config(cmd->server->module_config); + const char **np; + const char *err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE); + + if (err) { + return err; + } + + np = (const char **)apr_array_push(conf->protocols); + *np = arg; + + return NULL; +} + +static const char *set_protocols_honor_order(cmd_parms *cmd, void *dummy, + const char *arg) +{ + core_server_config *conf = + ap_get_core_module_config(cmd->server->module_config); + const char *err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE); + + if (err) { + return err; + } + + if (strcasecmp(arg, "on") == 0) { + conf->protocols_honor_order = 1; + } + else if (strcasecmp(arg, "off") == 0) { + conf->protocols_honor_order = 0; + } + else { + return "ProtocolsHonorOrder must be 'on' or 'off'"; + } + + return NULL; +} + static apr_hash_t *errorlog_hash; static int log_constant_item(const ap_errorlog_info *info, const char *arg, @@ -4205,6 +4257,11 @@ AP_INIT_TAKE1("TraceEnable", set_trace_enable, NULL, RSRC_CONF, "'on' (default), 'off' or 'extended' to trace request body content"), AP_INIT_FLAG("MergeTrailers", set_merge_trailers, NULL, RSRC_CONF, "merge request trailers into request headers or not"), +AP_INIT_ITERATE("Protocols", set_protocols, NULL, RSRC_CONF, + "Controls which protocols are allowed"), +AP_INIT_TAKE1("ProtocolsHonorOrder", set_protocols_honor_order, NULL, RSRC_CONF, + "'off' (default) or 'on' to respect given order of protocols, " + "by default the client specified order determines selection"), { NULL } }; @@ -4936,6 +4993,61 @@ static void core_dump_config(apr_pool_t *p, server_rec *s) } } +static int core_upgrade_handler(request_rec *r) +{ + conn_rec *c = r->connection; + const char *upgrade = apr_table_get(r->headers_in, "Upgrade"); + + if (upgrade && *upgrade) { + const char *conn = apr_table_get(r->headers_in, "Connection"); + if (ap_find_token(r->pool, conn, "upgrade")) { + apr_array_header_t *offers = NULL; + const char *err; + + err = ap_parse_token_list_strict(r->pool, upgrade, &offers, 0); + if (err) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02910) + "parsing Upgrade header: %s", err); + return DECLINED; + } + + if (offers && offers->nelts > 0) { + const char *protocol = ap_select_protocol(c, r, r->server, + offers); + if (protocol && strcmp(protocol, ap_get_protocol(c))) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02909) + "Upgrade selects '%s'", protocol); + /* Let the client know what we are upgrading to. */ + apr_table_clear(r->headers_out); + apr_table_setn(r->headers_out, "Upgrade", protocol); + apr_table_setn(r->headers_out, "Connection", "Upgrade"); + + r->status = HTTP_SWITCHING_PROTOCOLS; + r->status_line = ap_get_status_line(r->status); + ap_send_interim_response(r, 1); + + ap_switch_protocol(c, r, r->server, protocol); + + /* make sure httpd closes the connection after this */ + c->keepalive = AP_CONN_CLOSE; + return DONE; + } + } + } + } + + return DECLINED; +} + +static int core_upgrade_storage(request_rec *r) +{ + if ((r->method_number == M_OPTIONS) && r->uri && (r->uri[0] == '*') && + (r->uri[1] == '\0')) { + return core_upgrade_handler(r); + } + return DECLINED; +} + static void register_hooks(apr_pool_t *p) { errorlog_hash = apr_hash_make(p); @@ -4958,10 +5070,12 @@ static void register_hooks(apr_pool_t *p) ap_hook_check_config(core_check_config,NULL,NULL,APR_HOOK_FIRST); ap_hook_test_config(core_dump_config,NULL,NULL,APR_HOOK_FIRST); ap_hook_translate_name(ap_core_translate,NULL,NULL,APR_HOOK_REALLY_LAST); + ap_hook_map_to_storage(core_upgrade_storage,NULL,NULL,APR_HOOK_REALLY_FIRST); ap_hook_map_to_storage(core_map_to_storage,NULL,NULL,APR_HOOK_REALLY_LAST); ap_hook_open_logs(ap_open_logs,NULL,NULL,APR_HOOK_REALLY_FIRST); ap_hook_child_init(core_child_init,NULL,NULL,APR_HOOK_REALLY_FIRST); ap_hook_child_init(ap_logs_child_init,NULL,NULL,APR_HOOK_MIDDLE); + ap_hook_handler(core_upgrade_handler,NULL,NULL,APR_HOOK_REALLY_FIRST); ap_hook_handler(default_handler,NULL,NULL,APR_HOOK_REALLY_LAST); /* FIXME: I suspect we can eliminate the need for these do_nothings - Ben */ ap_hook_type_checker(do_nothing,NULL,NULL,APR_HOOK_REALLY_LAST); diff --git a/server/protocol.c b/server/protocol.c index 8ebf4f41f63..fc507fa0897 100644 --- a/server/protocol.c +++ b/server/protocol.c @@ -67,6 +67,9 @@ APR_HOOK_STRUCT( APR_HOOK_LINK(http_scheme) APR_HOOK_LINK(default_port) APR_HOOK_LINK(note_auth_failure) + APR_HOOK_LINK(protocol_propose) + APR_HOOK_LINK(protocol_switch) + APR_HOOK_LINK(protocol_get) ) AP_DECLARE_DATA ap_filter_rec_t *ap_old_write_func = NULL; @@ -1790,6 +1793,150 @@ AP_DECLARE(void) ap_send_interim_response(request_rec *r, int send_headers) apr_brigade_destroy(x.bb); } +/* + * Compare two protocol identifier. Result is similar to strcmp(): + * 0 gives same precedence, >0 means proto1 is preferred. + */ +static int protocol_cmp(const apr_array_header_t *preferences, + const char *proto1, + const char *proto2) +{ + if (preferences && preferences->nelts > 0) { + int index1 = ap_array_str_index(preferences, proto1, 0); + int index2 = ap_array_str_index(preferences, proto2, 0); + if (index2 > index1) { + return (index1 >= 0) ? 1 : -1; + } + else if (index1 > index2) { + return (index2 >= 0) ? -1 : 1; + } + } + /* both have the same index (mabye -1 or no pref configured) and we compare + * the names so that spdy3 gets precedence over spdy2. That makes + * the outcome at least deterministic. */ + return strcmp(proto1, proto2); +} + +AP_DECLARE(const char *) ap_get_protocol(conn_rec *c) +{ + const char *protocol = ap_run_protocol_get(c); + return protocol? protocol : AP_PROTOCOL_HTTP1; +} + +AP_DECLARE(const char *) ap_select_protocol(conn_rec *c, request_rec *r, + server_rec *s, + const apr_array_header_t *choices) +{ + apr_pool_t *pool = r? r->pool : c->pool; + core_server_config *conf = ap_get_core_module_config(s->module_config); + const char *protocol = NULL, *existing; + apr_array_header_t *proposals; + + if (APLOGcdebug(c)) { + const char *p = apr_array_pstrcat(pool, conf->protocols, ','); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, + "select protocol from %s, choices=%s for server %s", + p, apr_array_pstrcat(pool, choices, ','), + s->server_hostname); + } + + if (conf->protocols->nelts <= 0) { + /* nothing configured, by default, we only allow http/1.1 here. + * For now... + */ + if (ap_array_str_contains(choices, AP_PROTOCOL_HTTP1)) { + return AP_PROTOCOL_HTTP1; + } + else { + return NULL; + } + } + + proposals = apr_array_make(pool, choices->nelts + 1, sizeof(char *)); + ap_run_protocol_propose(c, r, s, choices, proposals); + + /* If the existing protocol has not been proposed, but is a choice, + * add it to the proposals implicitly. + */ + existing = ap_get_protocol(c); + if (!ap_array_str_contains(proposals, existing) + && ap_array_str_contains(choices, existing)) { + APR_ARRAY_PUSH(proposals, const char*) = existing; + } + + if (proposals->nelts > 0) { + int i; + const apr_array_header_t *prefs = NULL; + + /* Default for protocols_honor_order is 'on' or != 0 */ + if (conf->protocols_honor_order == 0 && choices->nelts > 0) { + prefs = choices; + } + else { + prefs = conf->protocols; + } + + /* Select the most preferred protocol */ + if (APLOGcdebug(c)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, + "select protocol, proposals=%s preferences=%s configured=%s", + apr_array_pstrcat(pool, proposals, ','), + apr_array_pstrcat(pool, prefs, ','), + apr_array_pstrcat(pool, conf->protocols, ',')); + } + for (i = 0; i < proposals->nelts; ++i) { + const char *p = APR_ARRAY_IDX(proposals, i, const char *); + if (!ap_array_str_contains(conf->protocols, p)) { + /* not a configured protocol here */ + continue; + } + else if (!protocol + || (protocol_cmp(prefs, protocol, p) < 0)) { + /* none selected yet or this one has preference */ + protocol = p; + } + } + } + if (APLOGcdebug(c)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "selected protocol=%s", + protocol? protocol : "(none)"); + } + + return protocol; +} + +AP_DECLARE(apr_status_t) ap_switch_protocol(conn_rec *c, request_rec *r, + server_rec *s, + const char *protocol) +{ + const char *current = ap_get_protocol(c); + int rc; + + if (!strcmp(current, protocol)) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, APLOGNO(02906) + "already at it, protocol_switch to %s", + protocol); + return APR_SUCCESS; + } + + rc = ap_run_protocol_switch(c, r, s, protocol); + switch (rc) { + case DECLINED: + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02907) + "no implementation for protocol_switch to %s", + protocol); + return APR_ENOTIMPL; + case OK: + case DONE: + return APR_SUCCESS; + default: + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02905) + "unexpected return code %d from protocol_switch to %s" + , rc, protocol); + return APR_EOF; + } +} + AP_IMPLEMENT_HOOK_VOID(pre_read_request, (request_rec *r, conn_rec *c), @@ -1805,3 +1952,14 @@ AP_IMPLEMENT_HOOK_RUN_FIRST(unsigned short,default_port, AP_IMPLEMENT_HOOK_RUN_FIRST(int, note_auth_failure, (request_rec *r, const char *auth_type), (r, auth_type), DECLINED) +AP_IMPLEMENT_HOOK_RUN_ALL(int,protocol_propose, + (conn_rec *c, request_rec *r, server_rec *s, + const apr_array_header_t *offers, + apr_array_header_t *proposals), + (c, r, s, offers, proposals), OK, DECLINED) +AP_IMPLEMENT_HOOK_RUN_FIRST(int,protocol_switch, + (conn_rec *c, request_rec *r, server_rec *s, + const char *protocol), + (c, r, s, protocol), DECLINED) +AP_IMPLEMENT_HOOK_RUN_FIRST(const char *,protocol_get, + (const conn_rec *c), (c), NULL) diff --git a/server/util.c b/server/util.c index 464b07f4903..916213c3801 100644 --- a/server/util.c +++ b/server/util.c @@ -1451,6 +1451,95 @@ AP_DECLARE(int) ap_find_etag_weak(apr_pool_t *p, const char *line, return find_list_item(p, line, tok, AP_ETAG_WEAK); } +/* Grab a list of tokens of the format 1#token (from RFC7230) */ +AP_DECLARE(const char *) ap_parse_token_list_strict(apr_pool_t *p, + const char *str_in, + apr_array_header_t **tokens, + int skip_invalid) +{ + int in_leading_space = 1; + int in_trailing_space = 0; + int string_end = 0; + const char *tok_begin; + const char *cur; + + if (!str_in) { + return NULL; + } + + tok_begin = cur = str_in; + + while (!string_end) { + const unsigned char c = (unsigned char)*cur; + + if (!TEST_CHAR(c, T_HTTP_TOKEN_STOP) && c != '\0') { + /* Non-separator character; we are finished with leading + * whitespace. We must never have encountered any trailing + * whitespace before the delimiter (comma) */ + in_leading_space = 0; + if (in_trailing_space) { + return "Encountered illegal whitespace in token"; + } + } + else if (c == ' ' || c == '\t') { + /* "Linear whitespace" only includes ASCII CRLF, space, and tab; + * we can't get a CRLF since headers are split on them already, + * so only look for a space or a tab */ + if (in_leading_space) { + /* We're still in leading whitespace */ + ++tok_begin; + } + else { + /* We must be in trailing whitespace */ + ++in_trailing_space; + } + } + else if (c == ',' || c == '\0') { + if (!in_leading_space) { + /* If we're out of the leading space, we know we've read some + * characters of a token */ + if (*tokens == NULL) { + *tokens = apr_array_make(p, 4, sizeof(char *)); + } + APR_ARRAY_PUSH(*tokens, char *) = + apr_pstrmemdup((*tokens)->pool, tok_begin, + (cur - tok_begin) - in_trailing_space); + } + /* We're allowed to have null elements, just don't add them to the + * array */ + + tok_begin = cur + 1; + in_leading_space = 1; + in_trailing_space = 0; + string_end = (c == '\0'); + } + else { + /* Encountered illegal separator char */ + if (skip_invalid) { + /* Skip to the next separator */ + const char *temp; + temp = ap_strchr_c(cur, ','); + if(!temp) { + temp = ap_strchr_c(cur, '\0'); + } + + /* Act like we haven't seen a token so we reset */ + cur = temp - 1; + in_leading_space = 1; + in_trailing_space = 0; + } + else { + return apr_psprintf(p, "Encountered illegal separator " + "'\\x%.2x'", (unsigned int)c); + } + } + + ++cur; + } + + return NULL; +} + /* Retrieve a token, spacing over it and returning a pointer to * the first non-white byte afterwards. Note that these tokens * are delimited by semis and commas; and can also be delimited @@ -3005,3 +3094,28 @@ AP_DECLARE(char *) ap_get_exec_line(apr_pool_t *p, return apr_pstrndup(p, buf, k); } + +AP_DECLARE(int) ap_array_str_index(const apr_array_header_t *array, + const char *s, + int start) +{ + if (start >= 0) { + int i; + + for (i = start; i < array->nelts; i++) { + const char *p = APR_ARRAY_IDX(array, i, const char *); + if (!strcmp(p, s)) { + return i; + } + } + } + + return -1; +} + +AP_DECLARE(int) ap_array_str_contains(const apr_array_header_t *array, + const char *s) +{ + return (ap_array_str_index(array, s, 0) >= 0); +} +