├── NEWS ├── README ├── test ├── unit │ ├── .dirstamp │ ├── main.c │ ├── test_common.h │ └── test_h2_util.c ├── pyhttpd │ ├── __init__.py │ ├── htdocs │ │ ├── test2 │ │ │ ├── x%2f.test │ │ │ ├── 10%abnormal.txt │ │ │ ├── alive.json │ │ │ └── 006 │ │ │ │ └── 006.css │ │ ├── cgi │ │ │ ├── files │ │ │ │ └── empty.txt │ │ │ ├── echo.py │ │ │ ├── mnot164.py │ │ │ ├── echohd.py │ │ │ ├── hello.py │ │ │ ├── env.py │ │ │ ├── hecho.py │ │ │ ├── necho.py │ │ │ └── upload.py │ │ ├── test1 │ │ │ ├── apache.org-files │ │ │ │ ├── cse.js │ │ │ │ ├── css.css │ │ │ │ ├── jsapi.js │ │ │ │ ├── ant.jpg │ │ │ │ ├── asf_logo.png │ │ │ │ ├── min.css.br │ │ │ │ ├── mrunit.jpg │ │ │ │ ├── synapse.jpg │ │ │ │ ├── small-logo.png │ │ │ │ ├── async-ads.js.br │ │ │ │ ├── search_box_icon.png │ │ │ │ └── styles.css │ │ │ ├── 006 │ │ │ │ ├── header.html │ │ │ │ ├── 006.css │ │ │ │ └── 006.js │ │ │ ├── alive.json │ │ │ ├── 002.jpg │ │ │ ├── 003 │ │ │ │ └── 003_img.jpg │ │ │ ├── 004 │ │ │ │ └── gophertiles.jpg │ │ │ ├── 001.html │ │ │ ├── 003.html │ │ │ ├── 009.py │ │ │ ├── 006.html │ │ │ ├── 007 │ │ │ │ └── 007.py │ │ │ ├── 007.html │ │ │ └── index.html │ │ ├── alive.json │ │ ├── noh2 │ │ │ ├── alive.json │ │ │ └── index.html │ │ ├── index.html │ │ └── forbidden.html │ ├── conf │ │ ├── test.conf │ │ ├── stop.conf.template │ │ └── httpd.conf.template │ ├── config.ini.in │ ├── result.py │ ├── curl.py │ └── log.py ├── modules │ └── http2 │ │ ├── __init__.py │ │ ├── test_001_httpd_alive.py │ │ ├── mod_h2test │ │ └── mod_h2test.h │ │ ├── test_600_h2proxy.py │ │ ├── test_501_proxy_serverheader.py │ │ ├── test_102_require.py │ │ ├── test_300_interim.py │ │ ├── test_005_files.py │ │ ├── conftest.py │ │ ├── test_401_early_hints.py │ │ ├── test_700_load_get.py │ │ ├── test_106_shutdown.py │ │ ├── test_710_load_post_static.py │ │ ├── test_711_load_post_cgi.py │ │ ├── test_712_buffering.py │ │ ├── test_100_conn_reuse.py │ │ ├── test_202_trailer.py │ │ ├── test_002_curl_basics.py │ │ ├── test_006_assets.py │ │ ├── test_201_header_conditional.py │ │ ├── data │ │ └── nghttp-output-1k-1.txt │ │ ├── test_104_padding.py │ │ ├── test_103_upgrade.py │ │ ├── test_105_timeout.py │ │ ├── test_500_proxy.py │ │ ├── env.py │ │ └── test_101_ssl_reneg.py ├── Makefile.am └── scoreboard.py ├── m4 ├── h2.m4 └── ax_check_compile_flag.m4 ├── AUTHORS ├── mod_http2 ├── m4 │ └── h2.m4 ├── .gitignore ├── mod_proxy_http2.h ├── h2_private.h ├── h2_switch.h ├── h2_bucket_eos.h ├── h2_version.h ├── h2_version.h.in ├── h2_c2.h ├── h2_c2_filter.h ├── h2_protocol.h ├── h2_request.h ├── Makefile.am ├── h2_workers.h ├── h2_c1.h ├── h2_c1_io.h ├── h2_config.h ├── h2_bucket_eos.c ├── h2_headers.h ├── mod_http2.h ├── h2_conn_ctx.h ├── h2_conn_ctx.c ├── h2_proxy_session.h └── h2_push.h ├── .env ├── COPYING ├── .gitignore ├── .travis.yml ├── docker ├── archlinux │ ├── Dockerfile │ └── bin │ │ └── mod_h2_test.sh ├── debian-sid │ ├── Dockerfile │ └── bin │ │ └── mod_h2_test.sh └── ubuntu-focal │ ├── Dockerfile │ └── bin │ └── mod_h2_test.sh ├── http2_test.sh ├── Makefile.am ├── docker-compose.yml └── README.md /NEWS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | See README.md -------------------------------------------------------------------------------- /test/unit/.dirstamp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/pyhttpd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/modules/http2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /m4/h2.m4: -------------------------------------------------------------------------------- 1 | # just so it is not empty 2 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test2/x%2f.test: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/files/empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test2/10%abnormal.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Stefan Eissing -------------------------------------------------------------------------------- /mod_http2/m4/h2.m4: -------------------------------------------------------------------------------- 1 | # just so it is not empty 2 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/cse.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/css.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/jsapi.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DOCKER_REGISTRY=docker-registry.greenbytes.de 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Please see the file called LICENSE. 3 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/006/header.html: -------------------------------------------------------------------------------- 1 | My Header Title 2 | -------------------------------------------------------------------------------- /test/pyhttpd/conf/test.conf: -------------------------------------------------------------------------------- 1 | # empty placeholder for test specific configurations 2 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/alive.json: -------------------------------------------------------------------------------- 1 | { 2 | "host" : "generic", 3 | "alive" : true 4 | } 5 | -------------------------------------------------------------------------------- /mod_http2/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.slo 3 | *.lo 4 | *.la 5 | .libs 6 | Makefile.in 7 | Makefile 8 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test2/alive.json: -------------------------------------------------------------------------------- 1 | { 2 | "host" : "test2", 3 | "alive" : true 4 | } 5 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/noh2/alive.json: -------------------------------------------------------------------------------- 1 | { 2 | "host" : "noh2", 3 | "alive" : true 4 | } 5 | 6 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/alive.json: -------------------------------------------------------------------------------- 1 | { 2 | "host" : "test1", 3 | "alive" : true 4 | } 5 | 6 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/002.jpg -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/003/003_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/003/003_img.jpg -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/004/gophertiles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/004/gophertiles.jpg -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/ant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/ant.jpg -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/asf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/asf_logo.png -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/min.css.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/min.css.br -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/mrunit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/mrunit.jpg -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/synapse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/synapse.jpg -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/small-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/small-logo.png -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/async-ads.js.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/async-ads.js.br -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/search_box_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0nuss/vuejs/HEAD/test/pyhttpd/htdocs/test1/apache.org-files/search_box_icon.png -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | mod_h2 test site generic 4 | 5 | 6 |

mod_h2 test site generic

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/noh2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | mod_h2 test site noh2 4 | 5 | 6 |

mod_h2 test site noh2

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/forbidden.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 403 - Forbidden 4 | 5 | 6 |

403 - Forbidden

7 |

8 | An example of an error document. 9 |

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/001.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML/2.0 Test File: 001 5 | 6 | 7 |

HTML/2.0 Test File: 001

8 |

This file only contains a simple HTML structure with plain text.

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/006/006.css: -------------------------------------------------------------------------------- 1 | @CHARSET "ISO-8859-1"; 2 | body{ 3 | background:HoneyDew; 4 | } 5 | p{ 6 | color:#0000FF; 7 | text-align:left; 8 | } 9 | 10 | h1{ 11 | color:#FF0000; 12 | text-align:center; 13 | } 14 | 15 | .listTitle{ 16 | font-size:large; 17 | } 18 | 19 | .listElements{ 20 | color:#3366FF 21 | } -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test2/006/006.css: -------------------------------------------------------------------------------- 1 | @CHARSET "ISO-8859-1"; 2 | body{ 3 | background:HoneyDew; 4 | } 5 | p{ 6 | color:#0000FF; 7 | text-align:left; 8 | } 9 | 10 | h1{ 11 | color:#FF0000; 12 | text-align:center; 13 | } 14 | 15 | .listTitle{ 16 | font-size:large; 17 | } 18 | 19 | .listElements{ 20 | color:#3366FF 21 | } -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys, cgi, os 3 | 4 | status = '200 Ok' 5 | 6 | content = '' 7 | for line in sys.stdin: 8 | content += line 9 | 10 | # Just echo what we get 11 | print("Status: 200") 12 | print(f"Request-Length: {len(content)}") 13 | print("Content-Type: application/data\n") 14 | sys.stdout.write(content) 15 | 16 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/003.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML/2.0 Test File: 003 5 | 6 | 7 |

HTML/2.0 Test File: 003

8 |

This is a text HTML file with a big image:

9 |

GSMA Logo

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/mnot164.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import cgi 4 | import cgitb; cgitb.enable() 5 | import os 6 | import sys 7 | 8 | try: 9 | form = cgi.FieldStorage() 10 | count = form['count'].value 11 | text = form['text'].value 12 | except KeyError: 13 | text="a" 14 | count=77784 15 | 16 | 17 | print("Status: 200 OK") 18 | print("Content-Type: text/html") 19 | print() 20 | sys.stdout.write(text*int(count)) 21 | 22 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/echohd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cgi, os 3 | import cgitb; cgitb.enable() 4 | 5 | status = '200 Ok' 6 | 7 | form = cgi.FieldStorage() 8 | name = form.getvalue('name') 9 | 10 | if name: 11 | print("Status: 200") 12 | print("""\ 13 | Content-Type: text/plain\n""") 14 | print("""%s: %s""" % (name, os.environ['HTTP_'+name])) 15 | else: 16 | print("Status: 400 Parameter Missing") 17 | print("""\ 18 | Content-Type: text/html\n 19 | 20 |

No name was specified

21 | """) 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | print("Content-Type: application/json") 6 | print() 7 | print("{") 8 | print(" \"https\" : \"%s\"," % (os.getenv('HTTPS', ''))) 9 | print(" \"host\" : \"%s\"," % (os.getenv('SERVER_NAME', ''))) 10 | print(" \"protocol\" : \"%s\"," % (os.getenv('SERVER_PROTOCOL', ''))) 11 | print(" \"ssl_protocol\" : \"%s\"," % (os.getenv('SSL_PROTOCOL', ''))) 12 | print(" \"h2\" : \"%s\"," % (os.getenv('HTTP2', ''))) 13 | print(" \"h2push\" : \"%s\"" % (os.getenv('H2PUSH', ''))) 14 | print("}") 15 | 16 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/009.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import cgi, sys, time 4 | import cgitb; cgitb.enable() 5 | 6 | print "Content-Type: text/html;charset=UTF-8" 7 | print 8 | 9 | print """\ 10 | 11 | HTML/2.0 Test File: 009 (server time) 12 |

HTML/2.0 Test File: 009

13 |

60 seconds of server time, one by one.

""" 14 | 15 | for i in range(60): 16 | s = time.strftime("%Y-%m-%d %H:%M:%S") 17 | print "
", s, "
" 18 | sys.stdout.flush() 19 | time.sleep(1) 20 | 21 | print "

done.

" -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/006.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML/2.0 Test File: 006 5 | 6 | 7 | 8 | 9 |

HTML/2.0 Test File: 006

10 |
This page contains: 11 | 16 |
17 |
18 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.slo 3 | *.lo 4 | *.la 5 | *.pcap 6 | .libs 7 | .configured 8 | .deps 9 | compile 10 | aclocal.m4 11 | autom4te.cache 12 | autoscan.log 13 | config.guess 14 | config.log 15 | config.status 16 | config.sub 17 | config.h 18 | config.h.in 19 | config.h.in~ 20 | configure 21 | configure.scan 22 | depcomp 23 | install-sh 24 | libtool 25 | ltmain.sh 26 | missing 27 | stamp-h1 28 | Makefile.in 29 | Makefile 30 | mod_h2-*.tar.gz 31 | mod_http2-*.tar.gz 32 | m4 33 | test-driver 34 | test/.cache/ 35 | test/__pycache__ 36 | test/pyhttpd/__pycache__ 37 | test/modules/http2/__pycache__ 38 | test/*.pyc 39 | test/gen/ 40 | test/pyhttpd/config.ini 41 | test/.pytest_cache 42 | .idea 43 | -------------------------------------------------------------------------------- /test/pyhttpd/config.ini.in: -------------------------------------------------------------------------------- 1 | [global] 2 | curl_bin = curl 3 | nghttp = nghttp 4 | h2load = h2load 5 | 6 | prefix = @prefix@ 7 | exec_prefix = @exec_prefix@ 8 | bindir = @bindir@ 9 | sbindir = @sbindir@ 10 | libdir = @libdir@ 11 | libexecdir = @libexecdir@ 12 | 13 | apr_bindir = @APR_BINDIR@ 14 | apxs = @bindir@/apxs 15 | apachectl = @sbindir@/apachectl 16 | 17 | [httpd] 18 | version = @HTTPD_VERSION@ 19 | name = @progname@ 20 | dso_modules = @DSO_MODULES@ 21 | static_modules = @STATIC_MODULES@ 22 | 23 | [test] 24 | gen_dir = @abs_srcdir@/../gen 25 | http_port = 5002 26 | https_port = 5001 27 | proxy_port = 5003 28 | http_tld = tests.httpd.apache.org 29 | test_dir = @abs_srcdir@ 30 | test_src_dir = @abs_srcdir@ 31 | -------------------------------------------------------------------------------- /test/modules/http2/test_001_httpd_alive.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestBasicAlive: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add_vhost_test1().install() 11 | assert env.apache_restart() == 0 12 | 13 | # we expect to see the document from the generic server 14 | def test_h2_001_01(self, env): 15 | url = env.mkurl("https", "test1", "/alive.json") 16 | r = env.curl_get(url, 5) 17 | assert r.exit_code == 0, r.stderr + r.stdout 18 | assert r.response["json"] 19 | assert r.response["json"]["alive"] is True 20 | assert r.response["json"]["host"] == "test1" 21 | 22 | -------------------------------------------------------------------------------- /test/Makefile.am: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | # 13 | 14 | .PHONY: test loadtest 15 | 16 | test: 17 | pytest 18 | 19 | loadtest: 20 | python3 load_test.py 21 | 22 | clean-local: 23 | rm -rf *.pyc __pycache__ 24 | rm -rf $(GEN) 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: required 3 | 4 | os: 5 | - linux 6 | 7 | addons: 8 | apt: 9 | update: true 10 | config: 11 | retries: true 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - apache2 16 | - apache2-dev 17 | - libnghttp2-14 18 | - libnghttp2-dev 19 | 20 | # lets see if bionic's apache2 is recent enough 21 | matrix: 22 | include: 23 | - os: linux 24 | dist: bionic 25 | compiler: gcc 26 | env: 27 | - MY_ENV="BASE_OS=xenial-gcc" 28 | 29 | 30 | before_install: 31 | - eval "${MY_ENV}" 32 | 33 | before_script: 34 | - ./buildconf 35 | 36 | branches: 37 | only: 38 | - master 39 | 40 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/007/007.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import cgi, sys 4 | import cgitb; cgitb.enable() 5 | 6 | print "Content-Type: text/html;charset=UTF-8" 7 | print 8 | 9 | print """\ 10 | 11 | HTML/2.0 Test File: 007 (received data) 12 |

HTML/2.0 Test File: 007

""" 13 | 14 | # alternative output: parsed form params <-> plain POST body 15 | parseContent = True # <-> False 16 | 17 | if parseContent: 18 | print '

Data processed:

' 23 | else: 24 | print '

POST data output:

'
25 | 	data = sys.stdin.read()
26 | 	print data
27 | 	print '
' 28 | 29 | print '' -------------------------------------------------------------------------------- /docker/archlinux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux 2 | 3 | RUN pacman -Sy --noconfirm --needed \ 4 | git base-devel gcc python python-pip make openssl iputils 5 | RUN pacman -Sy --noconfirm --needed \ 6 | curl libcurl-compat apr apr-util nghttp2 pcre jansson rsync 7 | 8 | RUN pacman -Sy --noconfirm --needed apache 9 | 10 | RUN pip install pytest tqdm pycurl cryptography requests 11 | 12 | COPY docker/archlinux/bin/* /apache-httpd/bin/ 13 | COPY configure.ac Makefile.am NEWS README* AUTHORS ChangeLog COPYING LICENSE /apache-httpd/mod_h2/ 14 | COPY m4 /apache-httpd/mod_h2/m4 15 | COPY mod_http2 /apache-httpd/mod_h2/mod_http2 16 | COPY test test/Makefile.am /apache-httpd/mod_h2/test/ 17 | COPY test/pyhttpd /apache-httpd/mod_h2/test/pyhttpd 18 | COPY test/modules /apache-httpd/mod_h2/test/modules 19 | COPY test/unit /apache-httpd/mod_h2/test/unit 20 | 21 | CMD ["/bin/bash", "-c", "/apache-httpd/bin/mod_h2_test.sh"] -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/007.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML/2.0 Test File: 007 6 | 7 | 8 |

HTML/2.0 Test File: 007

9 |

This page is used to send data from the client to the server:

10 |
11 | 12 | Name:
13 | Age:
14 | Gender: Male 15 | Female
16 | 17 | 18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /mod_http2/mod_proxy_http2.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __MOD_PROXY_HTTP2_H__ 18 | #define __MOD_PROXY_HTTP2_H__ 19 | 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /test/modules/http2/mod_h2test/mod_h2test.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __MOD_H2TEST_H__ 18 | #define __MOD_H2TEST_H__ 19 | 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/006/006.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript Functions File 3 | */ 4 | function returnDate() 5 | { 6 | var currentDate; 7 | currentDate=new Date(); 8 | var dateString=(currentDate.getMonth()+1)+'/'+currentDate.getDate()+'/'+currentDate.getFullYear(); 9 | return dateString; 10 | } 11 | 12 | function returnHour() 13 | { 14 | var currentDate; 15 | currentDate=new Date(); 16 | var hourString=currentDate.getHours()+':'+currentDate.getMinutes()+':'+currentDate.getSeconds(); 17 | return hourString; 18 | } 19 | 20 | function javaScriptMessage(){ 21 | return 'This section is generated under JavaScript:
'; 22 | } 23 | 24 | function mainJavascript(){ 25 | document.write(javaScriptMessage()) 26 | document.write(''); 31 | } -------------------------------------------------------------------------------- /test/modules/http2/test_600_h2proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestH2Proxy: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | conf = H2Conf(env) 11 | conf.add_vhost_cgi(h2proxy_self=True) 12 | if env.verbosity > 1: 13 | conf.add("LogLevel proxy:trace2 proxy_http2:trace2") 14 | conf.install() 15 | assert env.apache_restart() == 0 16 | 17 | def test_h2_600_01(self, env): 18 | url = env.mkurl("https", "cgi", "/h2proxy/hello.py") 19 | r = env.curl_get(url, 5) 20 | assert r.response["status"] == 200 21 | assert r.response["json"]["protocol"] == "HTTP/2.0" 22 | assert r.response["json"]["https"] == "on" 23 | assert r.response["json"]["ssl_protocol"] != "" 24 | assert r.response["json"]["h2"] == "on" 25 | assert r.response["json"]["h2push"] == "off" 26 | assert r.response["json"]["host"] == f"cgi.{env.http_tld}" 27 | -------------------------------------------------------------------------------- /docker/debian-sid/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:sid 2 | 3 | RUN apt-get update; apt-get upgrade -y 4 | RUN apt-get install -y apt-listchanges \ 5 | make openssl libssl-dev libcurl4 libcurl4-openssl-dev \ 6 | gcc subversion git cargo python3 iputils-ping \ 7 | libapr1-dev libaprutil1-dev libnghttp2-dev pip \ 8 | autoconf libtool libtool-bin libpcre3-dev libjansson-dev curl rsync nghttp2-client 9 | 10 | RUN pip install pytest tqdm pycurl cryptography requests 11 | 12 | RUN apt-get install -y apache2 apache2-dev libapache2-mod-md 13 | 14 | COPY docker/debian-sid/bin/* /apache-httpd/bin/ 15 | COPY configure.ac Makefile.am NEWS README* AUTHORS ChangeLog COPYING LICENSE /apache-httpd/mod_h2/ 16 | COPY m4 /apache-httpd/mod_h2/m4 17 | COPY mod_http2 /apache-httpd/mod_h2/mod_http2 18 | COPY test test/Makefile.am /apache-httpd/mod_h2/test/ 19 | COPY test/pyhttpd /apache-httpd/mod_h2/test/pyhttpd 20 | COPY test/modules /apache-httpd/mod_h2/test/modules 21 | COPY test/unit /apache-httpd/mod_h2/test/unit 22 | 23 | CMD ["/bin/bash", "-c", "/apache-httpd/bin/mod_h2_test.sh"] -------------------------------------------------------------------------------- /docker/ubuntu-focal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt update; apt upgrade -y 5 | RUN apt install -yy apt-listchanges \ 6 | make openssl libssl-dev libcurl4 libcurl4-openssl-dev \ 7 | gcc subversion git cargo python3 iputils-ping \ 8 | libapr1-dev libaprutil1-dev libnghttp2-dev pip \ 9 | autoconf libtool libtool-bin libpcre3-dev libjansson-dev curl rsync nghttp2-client 10 | 11 | RUN pip install pytest tqdm pycurl cryptography requests 12 | 13 | RUN apt install -y apache2 apache2-dev libapache2-mod-md 14 | 15 | COPY docker/debian-sid/bin/* /apache-httpd/bin/ 16 | COPY configure.ac Makefile.am NEWS README* AUTHORS ChangeLog COPYING LICENSE /apache-httpd/mod_h2/ 17 | COPY m4 /apache-httpd/mod_h2/m4 18 | COPY mod_http2 /apache-httpd/mod_h2/mod_http2 19 | COPY test test/Makefile.am /apache-httpd/mod_h2/test/ 20 | COPY test/pyhttpd /apache-httpd/mod_h2/test/pyhttpd 21 | COPY test/modules /apache-httpd/mod_h2/test/modules 22 | COPY test/unit /apache-httpd/mod_h2/test/unit 23 | 24 | CMD ["/bin/bash", "-c", "/apache-httpd/bin/mod_h2_test.sh"] -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cgi, os 3 | import cgitb; cgitb.enable() 4 | 5 | status = '200 Ok' 6 | 7 | try: 8 | form = cgi.FieldStorage() 9 | input = form['name'] 10 | 11 | # Test if the file was uploaded 12 | if input.value is not None: 13 | val = os.environ[input.value] if input.value in os.environ else "" 14 | print("Status: 200") 15 | print("""\ 16 | Content-Type: text/plain\n""") 17 | print("{0}={1}".format(input.value, val)) 18 | 19 | else: 20 | print("Status: 400 Parameter Missing") 21 | print("""\ 22 | Content-Type: text/html\n 23 | 24 |

No name was specified: %s

25 | """ % (count.value)) 26 | 27 | except KeyError: 28 | print("Status: 200 Ok") 29 | print("""\ 30 | Content-Type: text/html\n 31 | 32 | Echo
33 | 34 |
35 | """) 36 | pass 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /mod_http2/h2_private.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef mod_h2_h2_private_h 18 | #define mod_h2_h2_private_h 19 | 20 | #include 21 | 22 | #include 23 | 24 | extern module AP_MODULE_DECLARE_DATA http2_module; 25 | 26 | APLOG_USE_MODULE(http2); 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /docker/archlinux/bin/mod_h2_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOP=/apache-httpd 4 | DATADIR=$TOP/data 5 | 6 | fail() { 7 | echo "$@" 8 | exit 1 9 | } 10 | 11 | needs_update() { 12 | local ref_file="$1" 13 | local check_dir="$2" 14 | if test ! -f "$ref_file"; then 15 | return 0 16 | fi 17 | find "$check_dir" -type f -a -newer "$ref_file" -o -type d -name .git -prune -a -false | 18 | while read fname; do 19 | return 0 20 | done 21 | return 1 22 | } 23 | 24 | PREFIX=$(apxs -q exec_prefix) 25 | if test ! -d $PREFIX; then 26 | fail "apache install prefix not found: $PREFIX" 27 | fi 28 | 29 | # remove some stuff that accumulates 30 | LOG_DIR=$(apxs -q logfiledir) 31 | rm -f $LOG_DIR/* 32 | 33 | cd "$TOP/mod_h2" ||fail 34 | if needs_update .installed .; then 35 | rm -f .installed 36 | if test ! -f configure -o configure.ac -nt configure; then 37 | autoreconf -i ||fail 38 | fi 39 | if test ! -d Makefile -o ./configure -nt Makefile; then 40 | ./configure || fail 41 | touch ./configure 42 | fi 43 | make clean||fail 44 | make ||fail 45 | touch .installed 46 | fi 47 | make install ||fail 48 | make test 49 | #make loadtest 50 | -------------------------------------------------------------------------------- /docker/debian-sid/bin/mod_h2_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOP=/apache-httpd 4 | DATADIR=$TOP/data 5 | 6 | fail() { 7 | echo "$@" 8 | exit 1 9 | } 10 | 11 | needs_update() { 12 | local ref_file="$1" 13 | local check_dir="$2" 14 | if test ! -f "$ref_file"; then 15 | return 0 16 | fi 17 | find "$check_dir" -type f -a -newer "$ref_file" -o -type d -name .git -prune -a -false | 18 | while read fname; do 19 | return 0 20 | done 21 | return 1 22 | } 23 | 24 | PREFIX=$(apxs -q exec_prefix) 25 | if test ! -d $PREFIX; then 26 | fail "apache install prefix not found: $PREFIX" 27 | fi 28 | 29 | # remove some stuff that accumulates 30 | LOG_DIR=$(apxs -q logfiledir) 31 | rm -f $LOG_DIR/* 32 | 33 | cd "$TOP/mod_h2" ||fail 34 | if needs_update .installed .; then 35 | rm -f .installed 36 | if test ! -f configure -o configure.ac -nt configure; then 37 | autoreconf -i ||fail 38 | fi 39 | if test ! -d Makefile -o ./configure -nt Makefile; then 40 | ./configure || fail 41 | touch ./configure 42 | fi 43 | make clean||fail 44 | make ||fail 45 | find . 46 | touch .installed 47 | fi 48 | make install ||fail 49 | make test 50 | #make loadtest 51 | -------------------------------------------------------------------------------- /docker/ubuntu-focal/bin/mod_h2_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOP=/apache-httpd 4 | DATADIR=$TOP/data 5 | 6 | fail() { 7 | echo "$@" 8 | exit 1 9 | } 10 | 11 | needs_update() { 12 | local ref_file="$1" 13 | local check_dir="$2" 14 | if test ! -f "$ref_file"; then 15 | return 0 16 | fi 17 | find "$check_dir" -type f -a -newer "$ref_file" -o -type d -name .git -prune -a -false | 18 | while read fname; do 19 | return 0 20 | done 21 | return 1 22 | } 23 | 24 | PREFIX=$(apxs -q exec_prefix) 25 | if test ! -d $PREFIX; then 26 | fail "apache install prefix not found: $PREFIX" 27 | fi 28 | 29 | # remove some stuff that accumulates 30 | LOG_DIR=$(apxs -q logfiledir) 31 | rm -f $LOG_DIR/* 32 | 33 | cd "$TOP/mod_h2" ||fail 34 | if needs_update .installed .; then 35 | rm -f .installed 36 | if test ! -f configure -o configure.ac -nt configure; then 37 | autoreconf -i ||fail 38 | fi 39 | if test ! -d Makefile -o ./configure -nt Makefile; then 40 | ./configure || fail 41 | touch ./configure 42 | fi 43 | make clean||fail 44 | make ||fail 45 | find . 46 | touch .installed 47 | fi 48 | make install ||fail 49 | make test 50 | #make loadtest 51 | -------------------------------------------------------------------------------- /mod_http2/h2_switch.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_switch__ 18 | #define __mod_h2__h2_switch__ 19 | 20 | /* 21 | * One time, post config initialization. 22 | */ 23 | apr_status_t h2_switch_init(apr_pool_t *pool, server_rec *s); 24 | 25 | /* Register apache hooks for protocol switching 26 | */ 27 | void h2_switch_register_hooks(void); 28 | 29 | 30 | #endif /* defined(__mod_h2__h2_switch__) */ 31 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/hecho.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cgi, os 3 | import cgitb; cgitb.enable() 4 | 5 | status = '200 Ok' 6 | 7 | try: 8 | form = cgi.FieldStorage() 9 | 10 | # A nested FieldStorage instance holds the file 11 | name = form['name'].value 12 | value = '' 13 | 14 | try: 15 | value = form['value'].value 16 | except KeyError: 17 | value = os.environ.get("HTTP_"+name, "unset") 18 | 19 | # Test if a value was given 20 | if name: 21 | print("Status: 200") 22 | print("%s: %s" % (name, value,)) 23 | print ("""\ 24 | Content-Type: text/plain\n""") 25 | 26 | else: 27 | print("Status: 400 Parameter Missing") 28 | print("""\ 29 | Content-Type: text/html\n 30 | 31 |

No name and value was specified: %s %s

32 | """ % (name, value)) 33 | 34 | except KeyError: 35 | print("Status: 200 Ok") 36 | print("""\ 37 | Content-Type: text/html\n 38 | 39 | Echo
40 | 41 | 42 |
43 | """) 44 | pass 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/modules/http2/test_501_proxy_serverheader.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestProxyServerHeader: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | conf = H2Conf(env, extras={ 11 | f'cgi.{env.http_tld}': [ 12 | "Header unset Server", 13 | "Header always set Server cgi", 14 | ] 15 | }) 16 | conf.add_vhost_cgi(proxy_self=True, h2proxy_self=False) 17 | conf.install() 18 | assert env.apache_restart() == 0 19 | 20 | def setup_method(self, method): 21 | print("setup_method: %s" % method.__name__) 22 | 23 | def teardown_method(self, method): 24 | print("teardown_method: %s" % method.__name__) 25 | 26 | def test_h2_501_01(self, env): 27 | url = env.mkurl("https", "cgi", "/proxy/hello.py") 28 | r = env.curl_get(url, 5) 29 | assert r.response["status"] == 200 30 | assert "HTTP/1.1" == r.response["json"]["protocol"] 31 | assert "" == r.response["json"]["https"] 32 | assert "" == r.response["json"]["ssl_protocol"] 33 | assert "" == r.response["json"]["h2"] 34 | assert "" == r.response["json"]["h2push"] 35 | assert "cgi" == r.response["header"]["server"] 36 | -------------------------------------------------------------------------------- /http2_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | 4 | function curl() { 5 | command curl "$@" --limit-rate 5M \ 6 | -o /dev/null -w "HTTP/%{http_version}" -m 5 \ 7 | 2>&1 | grep -v 'Operation timed out after' 8 | echo; echo 9 | } 10 | 11 | [ "$#" -eq 1 ] || { 12 | echo "Usage: $0 URL" >&2 13 | exit 1 14 | } 15 | 16 | SRV="$1"; shift 17 | 18 | ### 19 | 20 | UNIQ_ID="test_id=$(date +%s)" 21 | URI="$SRV?$UNIQ_ID" 22 | 23 | echo "### Performing test with ID $UNIQ_ID" 24 | echo "### grep $UNIQ_ID /var/log/access.mod_h2_issue_203" 25 | echo 26 | 27 | echo "# http2_whole_ssl" 28 | curl --http2 "https://$URI&_http2_whole_ssl___" 29 | 30 | echo "# http11_whole_ssl" 31 | curl --http1.1 "https://$URI&http11_whole_ssl___" 32 | 33 | echo "# http10_whole_ssl" 34 | curl --http1.0 "https://$URI&http10_whole_ssl___" 35 | 36 | ### 37 | 38 | echo "# http2_whole_nonssl" 39 | curl --http2 "http://$URI&_http2_whole_nonssl" 40 | 41 | echo "# http11_whole_nonssl" 42 | curl --http1.1 "http://$URI&http11_whole_nonssl" 43 | 44 | echo "# http10_whole_nonssl" 45 | curl --http1.0 "http://$URI&http10_whole_nonssl" 46 | 47 | ### 48 | 49 | echo "# http2_half_ssl" 50 | curl --http2 -H "Range: bytes=0-52428800" "https://$URI&_http2_half_ssl____" 51 | 52 | echo "# http11_half_ssl" 53 | curl --http1.1 -H "Range: bytes=0-52428800" "https://$URI&http11_half_ssl____" 54 | -------------------------------------------------------------------------------- /test/pyhttpd/conf/stop.conf.template: -------------------------------------------------------------------------------- 1 | # a config safe to use for stopping the server 2 | # this allows us to stop the server even when+ 3 | # the config in the file is borked (as test cases may try to do that) 4 | # 5 | ServerName localhost 6 | ServerRoot "${server_dir}" 7 | 8 | DefaultRuntimeDir logs 9 | PidFile httpd.pid 10 | 11 | Include "conf/modules.conf" 12 | 13 | DocumentRoot "${server_dir}/htdocs" 14 | 15 | 16 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %k" combined 17 | LogFormat "%h %l %u %t \"%r\" %>s %b" common 18 | CustomLog "logs/access_log" combined 19 | 20 | 21 | 22 | TypesConfig "${gen_dir}/apache/conf/mime.types" 23 | 24 | Listen ${http_port} 25 | Listen ${https_port} 26 | 27 | 28 | # provide some default 29 | SSLSessionCache "shmcb:ssl_gcache_data(32000)" 30 | 31 | 32 | 33 | ServerName ${http_tld} 34 | ServerAlias www.${http_tld} 35 | 36 | SSLEngine off 37 | 38 | DocumentRoot "${server_dir}/htdocs" 39 | 40 | 41 | 42 | Options Indexes FollowSymLinks 43 | AllowOverride None 44 | Require all granted 45 | 46 | AddHandler cgi-script .py 47 | AddHandler cgi-script .cgi 48 | Options +ExecCGI 49 | 50 | -------------------------------------------------------------------------------- /mod_http2/h2_bucket_eos.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef mod_http2_h2_bucket_stream_eos_h 18 | #define mod_http2_h2_bucket_stream_eos_h 19 | 20 | struct h2_stream; 21 | 22 | /** End Of HTTP/2 STREAM (H2EOS) bucket */ 23 | extern const apr_bucket_type_t h2_bucket_type_eos; 24 | 25 | #define H2_BUCKET_IS_H2EOS(e) (e->type == &h2_bucket_type_eos) 26 | 27 | apr_bucket *h2_bucket_eos_make(apr_bucket *b, struct h2_stream *stream); 28 | 29 | apr_bucket *h2_bucket_eos_create(apr_bucket_alloc_t *list, 30 | struct h2_stream *stream); 31 | 32 | #endif /* mod_http2_h2_bucket_stream_eos_h */ 33 | -------------------------------------------------------------------------------- /test/modules/http2/test_102_require.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestRequire: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | domain = f"ssl.{env.http_tld}" 11 | conf = H2Conf(env) 12 | conf.start_vhost(domains=[domain], port=env.https_port) 13 | conf.add(""" 14 | Protocols h2 http/1.1 15 | SSLOptions +StdEnvVars 16 | 17 | Require expr \"%{HTTP2} == 'on'\" 18 | 19 | 20 | Require expr \"%{HTTP2} == 'off'\" 21 | """) 22 | conf.end_vhost() 23 | conf.install() 24 | # the dir needs to exists for the configuration to have effect 25 | env.mkpath(f"{env.server_dir}/htdocs/ssl-client-verify") 26 | assert env.apache_restart() == 0 27 | 28 | def test_h2_102_01(self, env): 29 | url = env.mkurl("https", "ssl", "/h2only.html") 30 | r = env.curl_get(url) 31 | assert 0 == r.exit_code 32 | assert r.response 33 | assert 404 == r.response["status"] 34 | 35 | def test_h2_102_02(self, env): 36 | url = env.mkurl("https", "ssl", "/noh2.html") 37 | r = env.curl_get(url) 38 | assert 0 == r.exit_code 39 | assert r.response 40 | assert 403 == r.response["status"] 41 | -------------------------------------------------------------------------------- /test/unit/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | #include "test_common.h" 16 | 17 | #include 18 | 19 | static Suite *main_test_suite(void) 20 | { 21 | Suite *suite = suite_create("main"); 22 | 23 | suite_add_tcase(suite, h2_util_test_case()); 24 | 25 | return suite; 26 | } 27 | 28 | int main(int argc, const char * const argv[]) 29 | { 30 | SRunner *runner; 31 | int failed; 32 | 33 | /* Initialize APR and create our test runner. */ 34 | apr_app_initialize(&argc, &argv, NULL); 35 | runner = srunner_create(main_test_suite()); 36 | 37 | /* Log TAP to stdout. */ 38 | srunner_set_tap(runner, "-"); 39 | 40 | /* Run the tests and collect failures. */ 41 | srunner_run_all(runner, CK_SILENT /* output only TAP */); 42 | failed = srunner_ntests_failed(runner); 43 | 44 | /* Clean up. */ 45 | srunner_free(runner); 46 | apr_terminate(); 47 | 48 | return failed ? 1 : 0; 49 | } 50 | -------------------------------------------------------------------------------- /mod_http2/h2_version.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef mod_h2_h2_version_h 18 | #define mod_h2_h2_version_h 19 | 20 | #undef PACKAGE_VERSION 21 | #undef PACKAGE_TARNAME 22 | #undef PACKAGE_STRING 23 | #undef PACKAGE_NAME 24 | #undef PACKAGE_BUGREPORT 25 | 26 | /** 27 | * @macro 28 | * Version number of the http2 module as c string 29 | */ 30 | #define MOD_HTTP2_VERSION "2.0.2-git" 31 | 32 | /** 33 | * @macro 34 | * Numerical representation of the version number of the http2 module 35 | * release. This is a 24 bit number with 8 bits for major number, 8 bits 36 | * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. 37 | */ 38 | #define MOD_HTTP2_VERSION_NUM 0x020002 39 | 40 | 41 | #endif /* mod_h2_h2_version_h */ 42 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/necho.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cgi, os 3 | import time 4 | import cgitb; cgitb.enable() 5 | 6 | status = '200 Ok' 7 | 8 | try: 9 | form = cgi.FieldStorage() 10 | count = form['count'] 11 | text = form['text'] 12 | 13 | waitsec = float(form['wait1'].value) if 'wait1' in form else 0.0 14 | if waitsec > 0: 15 | time.sleep(waitsec) 16 | 17 | if int(count.value): 18 | print("Status: 200") 19 | print("""\ 20 | Content-Type: text/plain\n""") 21 | 22 | waitsec = float(form['wait2'].value) if 'wait2' in form else 0.0 23 | if waitsec > 0: 24 | time.sleep(waitsec) 25 | 26 | i = 0; 27 | for i in range(0, int(count.value)): 28 | print("%s" % (text.value)) 29 | 30 | waitsec = float(form['wait3'].value) if 'wait3' in form else 0.0 31 | if waitsec > 0: 32 | time.sleep(waitsec) 33 | 34 | else: 35 | print("Status: 400 Parameter Missing") 36 | print("""\ 37 | Content-Type: text/html\n 38 | 39 |

No count was specified: %s

40 | """ % (count.value)) 41 | 42 | except KeyError: 43 | print("Status: 200 Ok") 44 | print("""\ 45 | Content-Type: text/html\n 46 | 47 | Echo
48 | 49 | 50 |
51 | """) 52 | pass 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/modules/http2/test_300_interim.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestInterimResponses: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add_vhost_test1().add_vhost_cgi().install() 11 | assert env.apache_restart() == 0 12 | 13 | def setup_method(self, method): 14 | print("setup_method: %s" % method.__name__) 15 | 16 | def teardown_method(self, method): 17 | print("teardown_method: %s" % method.__name__) 18 | 19 | # check that we normally do not see an interim response 20 | def test_h2_300_01(self, env): 21 | url = env.mkurl("https", "test1", "/index.html") 22 | r = env.curl_post_data(url, 'XYZ') 23 | assert r.response["status"] == 200 24 | assert "previous" not in r.response 25 | 26 | # check that we see an interim response when we ask for it 27 | def test_h2_300_02(self, env): 28 | url = env.mkurl("https", "cgi", "/echo.py") 29 | r = env.curl_post_data(url, 'XYZ', options=["-H", "expect: 100-continue"]) 30 | assert r.response["status"] == 200 31 | assert "previous" in r.response 32 | assert 100 == r.response["previous"]["status"] 33 | 34 | # check proper answer on unexpected 35 | def test_h2_300_03(self, env): 36 | url = env.mkurl("https", "cgi", "/echo.py") 37 | r = env.curl_post_data(url, 'XYZ', options=["-H", "expect: the-unexpected"]) 38 | assert 417 == r.response["status"] 39 | assert "previous" not in r.response 40 | -------------------------------------------------------------------------------- /mod_http2/h2_version.h.in: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef mod_h2_h2_version_h 18 | #define mod_h2_h2_version_h 19 | 20 | #undef PACKAGE_VERSION 21 | #undef PACKAGE_TARNAME 22 | #undef PACKAGE_STRING 23 | #undef PACKAGE_NAME 24 | #undef PACKAGE_BUGREPORT 25 | 26 | /** 27 | * @macro 28 | * Version number of the http2 module as c string 29 | */ 30 | #define MOD_HTTP2_VERSION "@PACKAGE_VERSION@-git" 31 | 32 | /** 33 | * @macro 34 | * Numerical representation of the version number of the http2 module 35 | * release. This is a 24 bit number with 8 bits for major number, 8 bits 36 | * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. 37 | */ 38 | #define MOD_HTTP2_VERSION_NUM @PACKAGE_VERSION_NUM@ 39 | 40 | 41 | #endif /* mod_h2_h2_version_h */ 42 | -------------------------------------------------------------------------------- /test/unit/test_common.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /* 16 | * Common headers and declarations needed by most/all test source files. 17 | */ 18 | 19 | #include /* for pid_t on Windows, needed by Check */ 20 | #include 21 | 22 | /* 23 | * Compatibility ck_assert macros for Check < 0.11. 24 | */ 25 | #if (CHECK_MAJOR_VERSION == 0) && (CHECK_MINOR_VERSION < 11) 26 | # include /* fabs() */ 27 | # include /* memcmp() */ 28 | 29 | # define ck_assert_double_eq_tol(a, b, tol) \ 30 | ck_assert(fabs((a) - (b)) <= (tol)) 31 | # define ck_assert_mem_eq(a, b, len) \ 32 | ck_assert(!memcmp((a), (b), (len))) 33 | # define ck_assert_ptr_nonnull(p) \ 34 | ck_assert((p) != NULL) 35 | #endif 36 | 37 | /* 38 | * A list of Check test case declarations, usually one per source file. Add your 39 | * test case here when adding a new source file, then add it to the 40 | * main_test_suite() in main.c. 41 | */ 42 | 43 | TCase *h2_util_test_case(void); 44 | -------------------------------------------------------------------------------- /test/modules/http2/test_005_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from .env import H2Conf 5 | 6 | 7 | def mk_text_file(fpath: str, lines: int): 8 | t110 = "" 9 | for _ in range(11): 10 | t110 += "0123456789" 11 | with open(fpath, "w") as fd: 12 | for i in range(lines): 13 | fd.write("{0:015d}: ".format(i)) # total 128 bytes per line 14 | fd.write(t110) 15 | fd.write("\n") 16 | 17 | 18 | class TestFiles: 19 | 20 | URI_PATHS = [] 21 | 22 | @pytest.fixture(autouse=True, scope='class') 23 | def _class_scope(self, env): 24 | docs_a = os.path.join(env.server_docs_dir, "cgi/files") 25 | uris = [] 26 | file_count = 32 27 | file_sizes = [1, 10, 100, 10000] 28 | for i in range(file_count): 29 | fsize = file_sizes[i % len(file_sizes)] 30 | if fsize is None: 31 | raise Exception("file sizes?: {0} {1}".format(i, fsize)) 32 | fname = "{0}-{1}k.txt".format(i, fsize) 33 | mk_text_file(os.path.join(docs_a, fname), 8 * fsize) 34 | self.URI_PATHS.append(f"/files/{fname}") 35 | 36 | H2Conf(env).add_vhost_cgi( 37 | proxy_self=True, h2proxy_self=True 38 | ).add_vhost_test1( 39 | proxy_self=True, h2proxy_self=True 40 | ).install() 41 | assert env.apache_restart() == 0 42 | 43 | def test_h2_005_01(self, env): 44 | url = env.mkurl("https", "cgi", self.URI_PATHS[2]) 45 | r = env.curl_get(url) 46 | assert r.response, r.stderr + r.stdout 47 | assert r.response["status"] == 200 48 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | # 13 | SUBDIRS = @BUILD_SUBDIRS@ 14 | DIST_SUBDIRS = mod_http2 15 | 16 | ACLOCAL_AMFLAGS = -I m4 17 | 18 | EXTRA_DIST = \ 19 | test/unit \ 20 | test/Makefile.in \ 21 | test/Makefile.am \ 22 | test/modules \ 23 | test/pyhttpd 24 | 25 | 26 | dist_doc_DATA = README README.md LICENSE 27 | 28 | dist-hook: 29 | @rm -rf $(distdir)/test/unit/.deps \ 30 | $(distdir)/test/unit/.py* \ 31 | `find $(distdir) -name __pycache__` \ 32 | `find $(distdir) -name .deps` \ 33 | `find $(distdir) -name .libs` \ 34 | `find $(distdir) -name .dirstamp` \ 35 | `find $(distdir) -name .pytest_cache` \ 36 | `find $(distdir) -name "*.o"` 37 | 38 | .PHONY: test 39 | 40 | test: 41 | $(MAKE) -C test/ test 42 | 43 | loadtest: 44 | $(MAKE) -C test/ loadtest 45 | 46 | clean-local: 47 | $(MAKE) -C test/ clean 48 | 49 | docker-test: 50 | docker-compose build debian-sid 51 | docker-compose build archlinux 52 | docker-compose run debian-sid 53 | docker-compose run archlinux 54 | -------------------------------------------------------------------------------- /test/pyhttpd/conf/httpd.conf.template: -------------------------------------------------------------------------------- 1 | ServerName localhost 2 | ServerRoot "${server_dir}" 3 | 4 | DefaultRuntimeDir logs 5 | PidFile httpd.pid 6 | 7 | Include "conf/modules.conf" 8 | 9 | DocumentRoot "${server_dir}/htdocs" 10 | 11 | 12 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %k" combined 13 | LogFormat "%h %l %u %t \"%r\" %>s %b" common 14 | CustomLog "logs/access_log" combined 15 | 16 | 17 | 18 | TypesConfig "${gen_dir}/apache/conf/mime.types" 19 | 20 | Listen ${http_port} 21 | Listen ${https_port} 22 | 23 | 24 | # provide some default 25 | SSLSessionCache "shmcb:ssl_gcache_data(32000)" 26 | 27 | 28 | # Insert our test specific configuration before the first vhost, 29 | # so that its vhosts can be the default one. This is relevant in 30 | # certain behaviours, such as protocol selection during SSL ALPN 31 | # negotiation. 32 | # 33 | Include "conf/test.conf" 34 | 35 | RequestReadTimeout header=10 body=10 36 | 37 | 38 | AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css 39 | 40 | 41 | AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css 42 | 43 | 44 | 45 | ServerName ${http_tld} 46 | ServerAlias www.${http_tld} 47 | 48 | SSLEngine off 49 | 50 | DocumentRoot "${server_dir}/htdocs" 51 | 52 | 53 | 54 | Options Indexes FollowSymLinks 55 | AllowOverride None 56 | Require all granted 57 | 58 | AddHandler cgi-script .py 59 | AddHandler cgi-script .cgi 60 | Options +ExecCGI 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /test/scoreboard.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | 5 | from modules.http2.env import H2TestEnv, H2TestSetup, H2Conf 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class ScoreboardTest: 11 | 12 | def __init__(self): 13 | pass 14 | 15 | @classmethod 16 | def main(cls): 17 | parser = argparse.ArgumentParser(prog='load_h1', description=""" 18 | Spin up a server with server-status configured. 19 | """) 20 | parser.add_argument("-v", "--verbose", action='count', default=0, 21 | help="log more output on stderr") 22 | args = parser.parse_args() 23 | 24 | if args.verbose > 0: 25 | console = logging.StreamHandler() 26 | console.setLevel(logging.INFO) 27 | console.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) 28 | logging.getLogger('').addHandler(console) 29 | 30 | rv = 0 31 | try: 32 | env = H2TestEnv() 33 | setup = H2TestSetup(env=env) 34 | env.setup_httpd(setup=setup) 35 | 36 | conf = H2Conf(env=env, extras={ 37 | 'base': [ 38 | "ServerLimit 1", 39 | "LogLevel http2:debug", 40 | "", 41 | " SetHandler server-status", 42 | "", 43 | ] 44 | }) 45 | conf.add_vhost_test1() 46 | conf.add_vhost_cgi() 47 | conf.install() 48 | env.apache_restart() 49 | sys.stdin.read() 50 | 51 | except KeyboardInterrupt: 52 | log.warning("aborted") 53 | rv = 1 54 | 55 | env.apache_stop() 56 | sys.exit(rv) 57 | 58 | if __name__ == "__main__": 59 | ScoreboardTest.main() 60 | -------------------------------------------------------------------------------- /test/modules/http2/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import pytest 5 | import sys 6 | 7 | sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) 8 | 9 | from .env import H2TestEnv 10 | 11 | 12 | def pytest_report_header(config, startdir): 13 | env = H2TestEnv() 14 | return f"mod_h2 [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]" 15 | 16 | 17 | def pytest_addoption(parser): 18 | parser.addoption("--repeat", action="store", type=int, default=1, 19 | help='Number of times to repeat each test') 20 | parser.addoption("--all", action="store_true") 21 | 22 | 23 | def pytest_generate_tests(metafunc): 24 | if "repeat" in metafunc.fixturenames: 25 | count = int(metafunc.config.getoption("repeat")) 26 | metafunc.fixturenames.append('tmp_ct') 27 | metafunc.parametrize('repeat', range(count)) 28 | 29 | 30 | @pytest.fixture(scope="package") 31 | def env(pytestconfig) -> H2TestEnv: 32 | level = logging.INFO 33 | console = logging.StreamHandler() 34 | console.setLevel(level) 35 | console.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) 36 | logging.getLogger('').addHandler(console) 37 | logging.getLogger('').setLevel(level=level) 38 | env = H2TestEnv(pytestconfig=pytestconfig) 39 | env.setup_httpd() 40 | env.apache_access_log_clear() 41 | env.httpd_error_log.clear_log() 42 | return env 43 | 44 | 45 | @pytest.fixture(autouse=True, scope="package") 46 | def _session_scope(env): 47 | yield 48 | assert env.apache_stop() == 0 49 | errors, warnings = env.httpd_error_log.get_missed() 50 | assert (len(errors), len(warnings)) == (0, 0),\ 51 | f"apache logged {len(errors)} errors and {len(warnings)} warnings: \n"\ 52 | "{0}\n{1}\n".format("\n".join(errors), "\n".join(warnings)) 53 | 54 | -------------------------------------------------------------------------------- /test/modules/http2/test_401_early_hints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | # The push tests depend on "nghttp" 7 | class TestEarlyHints: 8 | 9 | @pytest.fixture(autouse=True, scope='class') 10 | def _class_scope(self, env): 11 | H2Conf(env).start_vhost(domains=[f"hints.{env.http_tld}"], 12 | port=env.https_port, doc_root="htdocs/test1" 13 | ).add(""" 14 | H2EarlyHints on 15 | RewriteEngine on 16 | RewriteRule ^/006-(.*)?\\.html$ /006.html 17 | 18 | H2PushResource "/006/006.css" critical 19 | 20 | 21 | Header add Link ";rel=preload" 22 | 23 | """).end_vhost( 24 | ).install() 25 | assert env.apache_restart() == 0 26 | 27 | # H2EarlyHints enabled in general, check that it works for H2PushResource 28 | def test_h2_401_31(self, env): 29 | url = env.mkurl("https", "hints", "/006-hints.html") 30 | r = env.nghttp().get(url) 31 | assert r.response["status"] == 200 32 | promises = r.results["streams"][r.response["id"]]["promises"] 33 | assert 1 == len(promises) 34 | early = r.response["previous"] 35 | assert early 36 | assert 103 == int(early["header"][":status"]) 37 | assert early["header"]["link"] 38 | 39 | # H2EarlyHints enabled in general, but does not trigger on added response headers 40 | def test_h2_401_32(self, env): 41 | url = env.mkurl("https", "hints", "/006-nohints.html") 42 | r = env.nghttp().get(url) 43 | assert r.response["status"] == 200 44 | promises = r.results["streams"][r.response["id"]]["promises"] 45 | assert 1 == len(promises) 46 | assert "previous" not in r.response 47 | -------------------------------------------------------------------------------- /mod_http2/h2_c2.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_c2__ 18 | #define __mod_h2__h2_c2__ 19 | 20 | #include 21 | 22 | typedef enum { 23 | H2_MPM_UNKNOWN, 24 | H2_MPM_WORKER, 25 | H2_MPM_EVENT, 26 | H2_MPM_PREFORK, 27 | H2_MPM_MOTORZ, 28 | H2_MPM_SIMPLE, 29 | H2_MPM_NETWARE, 30 | H2_MPM_WINNT, 31 | } h2_mpm_type_t; 32 | 33 | /* Returns the type of MPM module detected */ 34 | h2_mpm_type_t h2_conn_mpm_type(void); 35 | const char *h2_conn_mpm_name(void); 36 | int h2_mpm_supported(void); 37 | 38 | /* Initialize this child process for h2 secondary connection work, 39 | * to be called once during child init before multi processing 40 | * starts. 41 | */ 42 | apr_status_t h2_c2_child_init(apr_pool_t *pool, server_rec *s); 43 | 44 | conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent); 45 | void h2_c2_destroy(conn_rec *c2); 46 | 47 | /** 48 | * Process a secondary connection for a HTTP/2 stream request. 49 | */ 50 | apr_status_t h2_c2_process(conn_rec *c, apr_thread_t *thread, int worker_id); 51 | 52 | void h2_c2_register_hooks(void); 53 | 54 | #endif /* defined(__mod_h2__h2_c2__) */ 55 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/cgi/upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cgi, os 3 | import cgitb 4 | cgitb.enable() 5 | 6 | status = '200 Ok' 7 | 8 | try: # Windows needs stdio set for binary mode. 9 | import msvcrt 10 | msvcrt.setmode (0, os.O_BINARY) # stdin = 0 11 | msvcrt.setmode (1, os.O_BINARY) # stdout = 1 12 | except ImportError: 13 | pass 14 | 15 | form = cgi.FieldStorage() 16 | 17 | # Test if the file was uploaded 18 | if 'file' in form: 19 | fileitem = form['file'] 20 | # strip leading path from file name to avoid directory traversal attacks 21 | fn = os.path.basename(fileitem.filename) 22 | f = open(('%s/files/%s' % (os.environ["DOCUMENT_ROOT"], fn)), 'wb'); 23 | f.write(fileitem.file.read()) 24 | f.close() 25 | message = "The file %s was uploaded successfully" % (fn) 26 | print("Status: 201 Created") 27 | print("Content-Type: text/html") 28 | print("Location: %s://%s/files/%s" % (os.environ["REQUEST_SCHEME"], os.environ["HTTP_HOST"], fn)) 29 | print("") 30 | print("

%s

" % (message)) 31 | 32 | elif 'remove' in form: 33 | remove = form['remove'].value 34 | try: 35 | fn = os.path.basename(remove) 36 | os.remove('./files/' + fn) 37 | message = 'The file "' + fn + '" was removed successfully' 38 | except OSError as e: 39 | message = 'Error removing ' + fn + ': ' + e.strerror 40 | status = '404 File Not Found' 41 | print("Status: %s" % (status)) 42 | print(""" 43 | Content-Type: text/html 44 | 45 | 46 |

%s

47 | """ % (message)) 48 | 49 | else: 50 | message = '''\ 51 | Upload File
52 | 53 |
54 | ''' 55 | print("Status: %s" % (status)) 56 | print("""\ 57 | Content-Type: text/html 58 | 59 | 60 |

%s

61 | """ % (message)) 62 | 63 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | mod_h2 test site 4 | 5 | 6 |

mod_h2 test site

7 |

8 |

served directly

9 | 20 |

mod_proxyied

21 | 32 |

mod_rewritten

33 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /mod_http2/h2_c2_filter.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_c2_filter__ 18 | #define __mod_h2__h2_c2_filter__ 19 | 20 | /** 21 | * h2_from_h1 parses a HTTP/1.1 response into 22 | * - response status 23 | * - a list of header values 24 | * - a series of bytes that represent the response body alone, without 25 | * any meta data, such as inserted by chunked transfer encoding. 26 | * 27 | * All data is allocated from the stream memory pool. 28 | * 29 | * Again, see comments in h2_request: ideally we would take the headers 30 | * and status from the httpd structures instead of parsing them here, but 31 | * we need to have all handlers and filters involved in request/response 32 | * processing, so this seems to be the way for now. 33 | */ 34 | struct h2_headers; 35 | struct h2_response_parser; 36 | 37 | apr_status_t h2_c2_filter_catch_h1_out(ap_filter_t* f, apr_bucket_brigade* bb); 38 | 39 | apr_status_t h2_c2_filter_response_out(ap_filter_t *f, apr_bucket_brigade *bb); 40 | 41 | apr_status_t h2_c2_filter_request_in(ap_filter_t* f, 42 | apr_bucket_brigade* brigade, 43 | ap_input_mode_t mode, 44 | apr_read_type_e block, 45 | apr_off_t readbytes); 46 | 47 | apr_status_t h2_c2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb); 48 | 49 | #endif /* defined(__mod_h2__h2_c2_filter__) */ 50 | -------------------------------------------------------------------------------- /mod_http2/h2_protocol.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_protocol__ 18 | #define __mod_h2__h2_protocol__ 19 | 20 | /** 21 | * List of protocol identifiers that we support in cleartext 22 | * negotiations. NULL terminated. 23 | */ 24 | extern const char *h2_protocol_ids_clear[]; 25 | 26 | /** 27 | * List of protocol identifiers that we support in TLS encrypted 28 | * negotiations (ALPN). NULL terminated. 29 | */ 30 | extern const char *h2_protocol_ids_tls[]; 31 | 32 | /** 33 | * Provide a user readable description of the HTTP/2 error code- 34 | * @param h2_error http/2 error code, as in rfc 7540, ch. 7 35 | * @return textual description of code or that it is unknown. 36 | */ 37 | const char *h2_protocol_err_description(unsigned int h2_error); 38 | 39 | /* 40 | * One time, post config initialization. 41 | */ 42 | apr_status_t h2_protocol_init(apr_pool_t *pool, server_rec *s); 43 | 44 | /** 45 | * Check if the given primary connection fulfills the protocol 46 | * requirements for HTTP/2. 47 | * @param c the connection 48 | * @param require_all != 0 iff any missing connection properties make 49 | * the test fail. For example, a cipher might not have been selected while 50 | * the handshake is still ongoing. 51 | * @return != 0 iff protocol requirements are met 52 | */ 53 | int h2_protocol_is_acceptable_c1(conn_rec *c, request_rec *r, int require_all); 54 | 55 | 56 | #endif /* defined(__mod_h2__h2_protocol__) */ 57 | -------------------------------------------------------------------------------- /test/modules/http2/test_700_load_get.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestLoadGet: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add_vhost_cgi().add_vhost_test1().install() 11 | assert env.apache_restart() == 0 12 | 13 | def check_h2load_ok(self, env, r, n): 14 | assert 0 == r.exit_code 15 | r = env.h2load_status(r) 16 | assert n == r.results["h2load"]["requests"]["total"] 17 | assert n == r.results["h2load"]["requests"]["started"] 18 | assert n == r.results["h2load"]["requests"]["done"] 19 | assert n == r.results["h2load"]["requests"]["succeeded"] 20 | assert n == r.results["h2load"]["status"]["2xx"] 21 | assert 0 == r.results["h2load"]["status"]["3xx"] 22 | assert 0 == r.results["h2load"]["status"]["4xx"] 23 | assert 0 == r.results["h2load"]["status"]["5xx"] 24 | 25 | # test load on cgi script, single connection, different sizes 26 | @pytest.mark.parametrize("start", [ 27 | 1000, 80000 28 | ]) 29 | def test_h2_700_10(self, env, start): 30 | text = "X" 31 | chunk = 32 32 | for n in range(0, 5): 33 | args = [env.h2load, "-n", "%d" % chunk, "-c", "1", "-m", "10", 34 | f"--base-uri={env.https_base_url}"] 35 | for i in range(0, chunk): 36 | args.append(env.mkurl("https", "cgi", ("/mnot164.py?count=%d&text=%s" % (start+(n*chunk)+i, text)))) 37 | r = env.run(args) 38 | self.check_h2load_ok(env, r, chunk) 39 | 40 | # test load on cgi script, single connection 41 | @pytest.mark.parametrize("conns", [ 42 | 1, 2, 16, 32 43 | ]) 44 | def test_h2_700_11(self, env, conns): 45 | text = "X" 46 | start = 1200 47 | chunk = 64 48 | for n in range(0, 5): 49 | args = [env.h2load, "-n", "%d" % chunk, "-c", "%d" % conns, "-m", "10", 50 | f"--base-uri={env.https_base_url}"] 51 | for i in range(0, chunk): 52 | args.append(env.mkurl("https", "cgi", ("/mnot164.py?count=%d&text=%s" % (start+(n*chunk)+i, text)))) 53 | r = env.run(args) 54 | self.check_h2load_ok(env, r, chunk) 55 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | debian-sid: 4 | # build and run tests in a debian sid container 5 | image: ${DOCKER_REGISTRY}/mod-h2-debian-sid:0.0.1 6 | container_name: mod-h2-debian-sid 7 | build: 8 | context: . 9 | dockerfile: docker/debian-sid/Dockerfile 10 | labels: 11 | - "description=mod_h2 debian sid server" 12 | - "maintainer=stefan@eissing.org" 13 | expose: 14 | - "42001" 15 | - "42002" 16 | volumes: 17 | - mod-h2-debian-sid-data:/apache-httpd/data 18 | ports: 19 | - "42001" 20 | - "42002" 21 | 22 | archlinux: 23 | # build and run tests in a debian sid container 24 | image: ${DOCKER_REGISTRY}/mod-h2-archlinux:0.0.1 25 | container_name: mod-h2-archlinux 26 | build: 27 | context: . 28 | dockerfile: docker/archlinux/Dockerfile 29 | labels: 30 | - "description=mod_h2 archlinux server" 31 | - "maintainer=stefan@eissing.org" 32 | expose: 33 | - "42001" 34 | - "42002" 35 | volumes: 36 | - mod-h2-archlinux-data:/apache-httpd/data 37 | ports: 38 | - "42001" 39 | - "42002" 40 | 41 | ubuntu-focal: 42 | # build and run tests in a ubuntu-focal container 43 | image: ${DOCKER_REGISTRY}/mod-h2-ubuntu-focal:0.0.1 44 | container_name: mod-h2-ubuntu-focal 45 | build: 46 | context: . 47 | dockerfile: docker/ubuntu-focal/Dockerfile 48 | labels: 49 | - "description=mod_h2 ubuntu-focal server" 50 | - "maintainer=stefan@eissing.org" 51 | expose: 52 | - "42001" 53 | - "42002" 54 | volumes: 55 | - mod-h2-ubuntu-focal-data:/apache-httpd/data 56 | ports: 57 | - "42001" 58 | - "42002" 59 | 60 | volumes: 61 | mod-h2-debian-sid-data: 62 | name: mod-h2-debian-sid-data 63 | labels: 64 | - "description=debian sid data for mod_h2" 65 | - "maintainer=stefan@eissing.org" 66 | 67 | mod-h2-archlinux-data: 68 | name: mod-h2-archlinux-data 69 | labels: 70 | - "description=archlinux data for mod_h2" 71 | - "maintainer=stefan@eissing.org" 72 | 73 | mod-h2-ubuntu-focal-data: 74 | name: mod-h2-ubuntu-focal-data 75 | labels: 76 | - "description=ubuntu-focal data for mod_h2" 77 | - "maintainer=stefan@eissing.org" 78 | 79 | 80 | -------------------------------------------------------------------------------- /test/modules/http2/test_106_shutdown.py: -------------------------------------------------------------------------------- 1 | # 2 | # mod-h2 test suite 3 | # check HTTP/2 timeout behaviour 4 | # 5 | import time 6 | from threading import Thread 7 | 8 | import pytest 9 | 10 | from .env import H2Conf 11 | from pyhttpd.result import ExecResult 12 | 13 | 14 | class TestShutdown: 15 | 16 | @pytest.fixture(autouse=True, scope='class') 17 | def _class_scope(self, env): 18 | conf = H2Conf(env) 19 | conf.add_vhost_cgi() 20 | conf.install() 21 | assert env.apache_restart() == 0 22 | 23 | def test_h2_106_01(self, env): 24 | url = env.mkurl("https", "cgi", "/necho.py") 25 | lines = 100000 26 | text = "123456789" 27 | wait2 = 1.0 28 | self.r = None 29 | def long_request(): 30 | args = ["-vvv", 31 | "-F", f"count={lines}", 32 | "-F", f"text={text}", 33 | "-F", f"wait2={wait2}", 34 | ] 35 | self.r = env.curl_get(url, 5, options=args) 36 | 37 | t = Thread(target=long_request) 38 | t.start() 39 | time.sleep(0.5) 40 | assert env.apache_reload() == 0 41 | t.join() 42 | # noinspection PyTypeChecker 43 | time.sleep(1) 44 | r: ExecResult = self.r 45 | assert r.exit_code == 0 46 | assert r.response, f"no response via {r.args} in {r.stderr}\nstdout: {len(r.stdout)} bytes" 47 | assert r.response["status"] == 200, f"{r}" 48 | assert len(r.response["body"]) == (lines * (len(text)+1)), f"{r}" 49 | 50 | def test_h2_106_02(self, env): 51 | # PR65731: invalid GOAWAY frame at session start when 52 | # MaxRequestsPerChild is reached 53 | # Create a low limit and only 2 children, so we'll encounter this easily 54 | conf = H2Conf(env, extras={ 55 | 'base': [ 56 | "ServerLimit 2", 57 | "MaxRequestsPerChild 3" 58 | ] 59 | }) 60 | conf.add_vhost_test1() 61 | conf.install() 62 | assert env.apache_restart() == 0 63 | url = env.mkurl("https", "test1", "/index.html") 64 | for i in range(7): 65 | r = env.curl_get(url, options=['-vvv']) 66 | assert r.exit_code == 0, f"failed on {i}. request: {r.stdout} {r.stderr}" 67 | assert r.response["status"] == 200 68 | assert "HTTP/2" == r.response["protocol"] 69 | -------------------------------------------------------------------------------- /test/modules/http2/test_710_load_post_static.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | from .env import H2Conf 5 | 6 | 7 | class TestLoadPostStatic: 8 | 9 | @pytest.fixture(autouse=True, scope='class') 10 | def _class_scope(self, env): 11 | H2Conf(env).add_vhost_test1().install() 12 | assert env.apache_restart() == 0 13 | 14 | def check_h2load_ok(self, env, r, n): 15 | assert 0 == r.exit_code 16 | r = env.h2load_status(r) 17 | assert n == r.results["h2load"]["requests"]["total"] 18 | assert n == r.results["h2load"]["requests"]["started"] 19 | assert n == r.results["h2load"]["requests"]["done"] 20 | assert n == r.results["h2load"]["requests"]["succeeded"] 21 | assert n == r.results["h2load"]["status"]["2xx"] 22 | assert 0 == r.results["h2load"]["status"]["3xx"] 23 | assert 0 == r.results["h2load"]["status"]["4xx"] 24 | assert 0 == r.results["h2load"]["status"]["5xx"] 25 | 26 | # test POST on static file, slurped in by server 27 | def test_h2_710_00(self, env): 28 | url = env.mkurl("https", "test1", "/index.html") 29 | n = 10 30 | m = 1 31 | conn = 1 32 | fname = "data-10k" 33 | args = [env.h2load, "-n", f"{n}", "-c", f"{conn}", "-m", f"{m}", 34 | f"--base-uri={env.https_base_url}", 35 | "-d", os.path.join(env.gen_dir, fname), url] 36 | r = env.run(args) 37 | self.check_h2load_ok(env, r, n) 38 | 39 | def test_h2_710_01(self, env): 40 | url = env.mkurl("https", "test1", "/index.html") 41 | n = 1000 42 | m = 100 43 | conn = 1 44 | fname = "data-1k" 45 | args = [env.h2load, "-n", f"{n}", "-c", f"{conn}", "-m", f"{m}", 46 | f"--base-uri={env.https_base_url}", 47 | "-d", os.path.join(env.gen_dir, fname), url] 48 | r = env.run(args) 49 | self.check_h2load_ok(env, r, n) 50 | 51 | def test_h2_710_02(self, env): 52 | url = env.mkurl("https", "test1", "/index.html") 53 | n = 100 54 | m = 50 55 | conn = 1 56 | fname = "data-100k" 57 | args = [env.h2load, "-n", f"{n}", "-c", f"{conn}", "-m", f"{m}", 58 | f"--base-uri={env.https_base_url}", 59 | "-d", os.path.join(env.gen_dir, fname), url] 60 | r = env.run(args) 61 | self.check_h2load_ok(env, r, n) 62 | -------------------------------------------------------------------------------- /mod_http2/h2_request.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_request__ 18 | #define __mod_h2__h2_request__ 19 | 20 | #include "h2.h" 21 | 22 | h2_request *h2_request_create(int id, apr_pool_t *pool, const char *method, 23 | const char *scheme, const char *authority, 24 | const char *path, apr_table_t *header); 25 | 26 | apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, 27 | request_rec *r); 28 | 29 | apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, 30 | const char *name, size_t nlen, 31 | const char *value, size_t vlen, 32 | size_t max_field_len, int *pwas_added); 33 | 34 | apr_status_t h2_request_add_trailer(h2_request *req, apr_pool_t *pool, 35 | const char *name, size_t nlen, 36 | const char *value, size_t vlen); 37 | 38 | apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos, size_t raw_bytes); 39 | 40 | h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src); 41 | 42 | /** 43 | * Create a request_rec representing the h2_request to be 44 | * processed on the given connection. 45 | * 46 | * @param req the h2 request to process 47 | * @param conn the connection to process the request on 48 | * @return the request_rec representing the request 49 | */ 50 | request_rec *h2_create_request_rec(const h2_request *req, conn_rec *conn); 51 | 52 | 53 | #endif /* defined(__mod_h2__h2_request__) */ 54 | -------------------------------------------------------------------------------- /test/pyhttpd/result.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import timedelta 3 | from typing import Optional, Dict, List 4 | 5 | 6 | class ExecResult: 7 | 8 | def __init__(self, args: List[str], exit_code: int, 9 | stdout: bytes, stderr: bytes = None, duration: timedelta = None): 10 | self._args = args 11 | self._exit_code = exit_code 12 | self._raw = stdout if stdout else b'' 13 | self._stdout = stdout.decode() if stdout is not None else "" 14 | self._stderr = stderr.decode() if stderr is not None else "" 15 | self._duration = duration if duration is not None else timedelta() 16 | self._response = None 17 | self._results = {} 18 | self._assets = [] 19 | # noinspection PyBroadException 20 | try: 21 | self._json_out = json.loads(self._stdout) 22 | except: 23 | self._json_out = None 24 | 25 | def __repr__(self): 26 | return f"ExecResult[code={self.exit_code}, args={self._args}, stdout={self.stdout}, stderr={self.stderr}]" 27 | 28 | @property 29 | def exit_code(self) -> int: 30 | return self._exit_code 31 | 32 | @property 33 | def args(self) -> List[str]: 34 | return self._args 35 | 36 | @property 37 | def outraw(self) -> bytes: 38 | return self._raw 39 | 40 | @property 41 | def stdout(self) -> str: 42 | return self._stdout 43 | 44 | @property 45 | def json(self) -> Optional[Dict]: 46 | """Output as JSON dictionary or None if not parseable.""" 47 | return self._json_out 48 | 49 | @property 50 | def stderr(self) -> str: 51 | return self._stderr 52 | 53 | @property 54 | def duration(self) -> timedelta: 55 | return self._duration 56 | 57 | @property 58 | def response(self) -> Optional[Dict]: 59 | return self._response 60 | 61 | @property 62 | def results(self) -> Dict: 63 | return self._results 64 | 65 | @property 66 | def assets(self) -> List: 67 | return self._assets 68 | 69 | def add_response(self, resp: Dict): 70 | if self._response: 71 | resp['previous'] = self._response 72 | self._response = resp 73 | 74 | def add_results(self, results: Dict): 75 | self._results.update(results) 76 | if 'response' in results: 77 | self.add_response(results['response']) 78 | 79 | def add_assets(self, assets: List): 80 | self._assets.extend(assets) 81 | -------------------------------------------------------------------------------- /test/modules/http2/test_711_load_post_cgi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | from .env import H2Conf 5 | 6 | 7 | class TestLoadCgi: 8 | 9 | @pytest.fixture(autouse=True, scope='class') 10 | def _class_scope(self, env): 11 | H2Conf(env).add_vhost_cgi(proxy_self=True, h2proxy_self=True).install() 12 | assert env.apache_restart() == 0 13 | 14 | def check_h2load_ok(self, env, r, n): 15 | assert 0 == r.exit_code 16 | r = env.h2load_status(r) 17 | assert n == r.results["h2load"]["requests"]["total"] 18 | assert n == r.results["h2load"]["requests"]["started"] 19 | assert n == r.results["h2load"]["requests"]["done"] 20 | assert n == r.results["h2load"]["requests"]["succeeded"] 21 | assert n == r.results["h2load"]["status"]["2xx"] 22 | assert 0 == r.results["h2load"]["status"]["3xx"] 23 | assert 0 == r.results["h2load"]["status"]["4xx"] 24 | assert 0 == r.results["h2load"]["status"]["5xx"] 25 | 26 | # test POST on cgi, where input is read 27 | def test_h2_711_10(self, env): 28 | url = env.mkurl("https", "test1", "/echo.py") 29 | n = 100 30 | m = 5 31 | conn = 1 32 | fname = "data-100k" 33 | args = [ 34 | env.h2load, "-n", str(n), "-c", str(conn), "-m", str(m), 35 | f"--base-uri={env.https_base_url}", 36 | "-d", os.path.join(env.gen_dir, fname), url 37 | ] 38 | r = env.run(args) 39 | self.check_h2load_ok(env, r, n) 40 | 41 | # test POST on cgi via http/1.1 proxy, where input is read 42 | def test_h2_711_11(self, env): 43 | url = env.mkurl("https", "test1", "/proxy/echo.py") 44 | n = 100 45 | m = 5 46 | conn = 1 47 | fname = "data-100k" 48 | args = [ 49 | env.h2load, "-n", str(n), "-c", str(conn), "-m", str(m), 50 | f"--base-uri={env.https_base_url}", 51 | "-d", os.path.join(env.gen_dir, fname), url 52 | ] 53 | r = env.run(args) 54 | self.check_h2load_ok(env, r, n) 55 | 56 | # test POST on cgi via h2proxy, where input is read 57 | def test_h2_711_12(self, env): 58 | url = env.mkurl("https", "test1", "/h2proxy/echo.py") 59 | n = 100 60 | m = 5 61 | conn = 1 62 | fname = "data-100k" 63 | args = [ 64 | env.h2load, "-n", str(n), "-c", str(conn), "-m", str(m), 65 | f"--base-uri={env.https_base_url}", 66 | "-d", os.path.join(env.gen_dir, fname), url 67 | ] 68 | r = env.run(args) 69 | self.check_h2load_ok(env, r, n) 70 | -------------------------------------------------------------------------------- /test/modules/http2/test_712_buffering.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | import pytest 4 | 5 | from .env import H2Conf 6 | from pyhttpd.curl import CurlPiper 7 | 8 | 9 | class TestBuffering: 10 | 11 | @pytest.fixture(autouse=True, scope='class') 12 | def _class_scope(self, env): 13 | conf = H2Conf(env) 14 | conf.add_vhost_cgi(h2proxy_self=True).install() 15 | assert env.apache_restart() == 0 16 | 17 | @pytest.mark.skip(reason="this test shows unreliable jitter") 18 | def test_h2_712_01(self, env): 19 | # test gRPC like requests that do not end, but give answers, see #207 20 | # 21 | # this test works like this: 22 | # - use curl to POST data to the server /h2test/echo 23 | # - feed curl the data in chunks, wait a bit between chunks 24 | # - since some buffering on curl's stdout to Python is involved, 25 | # we will see the response data only at the end. 26 | # - therefore, we enable tracing with timestamps in curl on stderr 27 | # and see when the response chunks arrive 28 | # - if the server sends the incoming data chunks back right away, 29 | # as it should, we see receiving timestamps separated roughly by the 30 | # wait time between sends. 31 | # 32 | url = env.mkurl("https", "cgi", "/h2test/echo") 33 | base_chunk = "0123456789" 34 | chunks = ["chunk-{0:03d}-{1}\n".format(i, base_chunk) for i in range(5)] 35 | stutter = timedelta(seconds=0.2) # this is short, but works on my machine (tm) 36 | piper = CurlPiper(env=env, url=url) 37 | piper.stutter_check(chunks, stutter) 38 | 39 | def test_h2_712_02(self, env): 40 | # same as 712_01 but via mod_proxy_http2 41 | # 42 | url = env.mkurl("https", "cgi", "/h2proxy/h2test/echo") 43 | base_chunk = "0123456789" 44 | chunks = ["chunk-{0:03d}-{1}\n".format(i, base_chunk) for i in range(3)] 45 | stutter = timedelta(seconds=0.4) # need a bit more delay since we have the extra connection 46 | piper = CurlPiper(env=env, url=url) 47 | piper.stutter_check(chunks, stutter) 48 | 49 | def test_h2_712_03(self, env): 50 | # same as 712_02 but with smaller chunks 51 | # 52 | url = env.mkurl("https", "cgi", "/h2proxy/h2test/echo") 53 | base_chunk = "0" 54 | chunks = ["ck{0}-{1}\n".format(i, base_chunk) for i in range(3)] 55 | stutter = timedelta(seconds=0.4) # need a bit more delay since we have the extra connection 56 | piper = CurlPiper(env=env, url=url) 57 | piper.stutter_check(chunks, stutter) 58 | -------------------------------------------------------------------------------- /test/modules/http2/test_100_conn_reuse.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestConnReuse: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add_vhost_noh2().add_vhost_test1().add_vhost_cgi().install() 11 | assert env.apache_restart() == 0 12 | 13 | # make sure the protocol selection on the different hosts work as expected 14 | def test_h2_100_01(self, env): 15 | # this host defaults to h2, but we can request h1 16 | url = env.mkurl("https", "cgi", "/hello.py") 17 | assert "2" == env.curl_protocol_version( url ) 18 | assert "1.1" == env.curl_protocol_version( url, options=[ "--http1.1" ] ) 19 | 20 | # this host does not enable h2, it always falls back to h1 21 | url = env.mkurl("https", "noh2", "/hello.py") 22 | assert "1.1" == env.curl_protocol_version( url ) 23 | assert "1.1" == env.curl_protocol_version( url, options=[ "--http2" ] ) 24 | 25 | # access a ServerAlias, after using ServerName in SNI 26 | def test_h2_100_02(self, env): 27 | url = env.mkurl("https", "cgi", "/hello.py") 28 | hostname = ("cgi-alias.%s" % env.http_tld) 29 | r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) 30 | assert r.response["status"] == 200 31 | assert "HTTP/2" == r.response["protocol"] 32 | assert hostname == r.response["json"]["host"] 33 | 34 | # access another vhost, after using ServerName in SNI, that uses same SSL setup 35 | def test_h2_100_03(self, env): 36 | url = env.mkurl("https", "cgi", "/") 37 | hostname = ("test1.%s" % env.http_tld) 38 | r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) 39 | assert r.response["status"] == 200 40 | assert "HTTP/2" == r.response["protocol"] 41 | assert "text/html" == r.response["header"]["content-type"] 42 | 43 | # access another vhost, after using ServerName in SNI, 44 | # that has different SSL certificate. This triggers a 421 (misdirected request) response. 45 | def test_h2_100_04(self, env): 46 | url = env.mkurl("https", "cgi", "/hello.py") 47 | hostname = ("noh2.%s" % env.http_tld) 48 | r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) 49 | assert 421 == r.response["status"] 50 | 51 | # access an unknown vhost, after using ServerName in SNI 52 | def test_h2_100_05(self, env): 53 | url = env.mkurl("https", "cgi", "/hello.py") 54 | hostname = ("unknown.%s" % env.http_tld) 55 | r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) 56 | assert 421 == r.response["status"] 57 | -------------------------------------------------------------------------------- /test/pyhttpd/htdocs/test1/apache.org-files/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top:60px; 3 | background-color: #fff; 4 | } 5 | 6 | h1, h2, h3, h4, h5, h6 { 7 | text-transform: uppercase; 8 | } 9 | 10 | .navbar-default .navbar-toggle { 11 | border: none; 12 | } 13 | 14 | .navbar-toggle { 15 | margin-top: 0px; 16 | margin-bottom: 0px; 17 | } 18 | 19 | section { 20 | padding-bottom: 25px; 21 | } 22 | 23 | ul.white > li > a, 24 | a.white { 25 | color: #ddd; 26 | } 27 | 28 | ul.white > li > a:hover, 29 | a.white:hover { 30 | color: #fff; 31 | } 32 | 33 | .white { 34 | color: #fff; 35 | } 36 | 37 | .bg-gray { 38 | background: #eee; 39 | } 40 | 41 | .letter-header { 42 | display: block; 43 | background-color: #f3f3f3; 44 | color: #303284; 45 | text-align: center; 46 | } 47 | 48 | .list-unstyled { 49 | list-style: none; 50 | } 51 | 52 | .no-top-margin{ 53 | margin-top: 0px; 54 | } 55 | 56 | .no-btm-margin { 57 | margin-bottom: 0px; 58 | } 59 | 60 | 61 | /* Custom Left Tabs */ 62 | 63 | .tabs-left > .nav-tabs { 64 | border-bottom: 0; 65 | } 66 | 67 | .tabs-left > .nav-tabs > li, 68 | .tabs-right > .nav-tabs > li { 69 | float: none; 70 | } 71 | 72 | .tabs-left > .nav-tabs > li > a, 73 | .tabs-right > .nav-tabs > li > a { 74 | min-width: 74px; 75 | margin-right: 0; 76 | margin-bottom: 3px; 77 | border: none; 78 | } 79 | 80 | .tabs-left > .nav-tabs > li > a:hover:after { 81 | content: ""; 82 | position: absolute; 83 | right: -24px; 84 | top: 50%; 85 | width: 0; 86 | height: 0; 87 | border-top: 25px solid transparent; 88 | border-bottom: 25px solid transparent; 89 | border-left: 25px solid #eee; 90 | margin-top: -25px; 91 | } 92 | 93 | .tabs-left > .nav-tabs > .active > a:after, 94 | .tabs-left > .nav-tabs > .active > a:hover:after { 95 | content: ""; 96 | position: absolute; 97 | right: -24px; 98 | top: 50%; 99 | width: 0; 100 | height: 0; 101 | border-top: 25px solid transparent; 102 | border-bottom: 25px solid transparent; 103 | border-left: 25px solid #303284; 104 | margin-top: -25px; 105 | } 106 | 107 | 108 | .tabs-left > .nav-tabs > li > a { 109 | margin-right: -1px; 110 | width: 90%; 111 | } 112 | 113 | .tabs-left > .nav-tabs > li > a:hover, 114 | .tabs-left > .nav-tabs > li > a:focus { 115 | border: none; 116 | } 117 | 118 | /* Media Adjustments */ 119 | @media (max-width: 992px){ 120 | .navbar-nav > li > a { 121 | padding-left: 5px; 122 | } 123 | } 124 | 125 | /* Prevent anchor links from displaying under the navbar */ 126 | :target:before { 127 | content: ""; 128 | display: block; 129 | height: 30px; 130 | margin-top: -30px; 131 | } 132 | -------------------------------------------------------------------------------- /mod_http2/Makefile.am: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | # 13 | SUBDIRS = 14 | 15 | ACLOCAL_AMFLAGS = -I m4 16 | AUTOMAKE_OPTIONS = foreign 17 | 18 | lib_LTLIBRARIES = mod_http2.la mod_proxy_http2.la 19 | 20 | mod_http2_la_CPPFLAGS = -std=c99 -D_GNU_SOURCE -Werror @WERROR_CFLAGS@ 21 | mod_http2_la_LDFLAGS = -module -avoid-version -export-symbols-regex http2_module 22 | 23 | mod_proxy_http2_la_CPPFLAGS = -std=c99 -D_GNU_SOURCE -Werror @WERROR_CFLAGS@ 24 | mod_proxy_http2_la_LDFLAGS = -module -avoid-version -export-symbols-regex proxy_http2_module 25 | 26 | OBJECTS = \ 27 | h2_bucket_beam.c \ 28 | h2_bucket_eos.c \ 29 | h2_c1.c \ 30 | h2_c1_io.c \ 31 | h2_c2.c \ 32 | h2_c2_filter.c \ 33 | h2_config.c \ 34 | h2_conn_ctx.c \ 35 | h2_headers.c \ 36 | h2_mplx.c \ 37 | h2_protocol.c \ 38 | h2_push.c \ 39 | h2_request.c \ 40 | h2_session.c \ 41 | h2_stream.c \ 42 | h2_switch.c \ 43 | h2_util.c \ 44 | h2_workers.c \ 45 | mod_http2.c 46 | 47 | HFILES = \ 48 | h2.h \ 49 | h2_bucket_beam.h \ 50 | h2_bucket_eos.h \ 51 | h2_c1.h \ 52 | h2_c1_io.h \ 53 | h2_c2.h \ 54 | h2_c2_filter.h \ 55 | h2_config.h \ 56 | h2_conn_ctx.h \ 57 | h2_headers.h \ 58 | h2_mplx.h \ 59 | h2_private.h \ 60 | h2_protocol.h \ 61 | h2_push.h \ 62 | h2_request.h \ 63 | h2_session.h \ 64 | h2_stream.h \ 65 | h2_switch.h \ 66 | h2_util.h \ 67 | h2_version.h \ 68 | h2_workers.h \ 69 | mod_http2.h 70 | 71 | PROXY_HFILES = \ 72 | h2.h \ 73 | h2_proxy_session.h \ 74 | h2_proxy_util.h \ 75 | mod_proxy_http2.h 76 | 77 | PROXY_OBJECTS = \ 78 | h2_proxy_session.c \ 79 | h2_proxy_util.c \ 80 | mod_proxy_http2.c 81 | 82 | mod_http2_la_SOURCES = $(HFILES) $(OBJECTS) 83 | 84 | mod_proxy_http2_la_SOURCES = $(PROXY_HFILES) $(PROXY_OBJECTS) 85 | 86 | all: mod_http2.la \ 87 | mod_proxy_http2.la 88 | 89 | install-libLTLIBRARIES: 90 | @: # override 91 | 92 | install-exec-local: mod_http2.la mod_proxy_http2.la 93 | $(MKDIR_P) $(DESTDIR)/@LIBEXEC_DIR@ 94 | $(APXS) -i -S LIBEXECDIR=$(DESTDIR)/@LIBEXEC_DIR@ -n h2 mod_http2.la 95 | $(APXS) -i -S LIBEXECDIR=$(DESTDIR)/@LIBEXEC_DIR@ -n h2 mod_proxy_http2.la 96 | 97 | 98 | -------------------------------------------------------------------------------- /test/modules/http2/test_202_trailer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from .env import H2Conf 5 | 6 | 7 | def setup_data(env): 8 | s100 = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n" 9 | with open(os.path.join(env.gen_dir, "data-1k"), 'w') as f: 10 | for i in range(10): 11 | f.write(s100) 12 | 13 | 14 | # The trailer tests depend on "nghttp" as no other client seems to be able to send those 15 | # rare things. 16 | class TestTrailers: 17 | 18 | @pytest.fixture(autouse=True, scope='class') 19 | def _class_scope(self, env): 20 | setup_data(env) 21 | H2Conf(env).add_vhost_cgi(h2proxy_self=True).install() 22 | assert env.apache_restart() == 0 23 | 24 | # check if the server survives a trailer or two 25 | def test_h2_202_01(self, env): 26 | url = env.mkurl("https", "cgi", "/echo.py") 27 | fpath = os.path.join(env.gen_dir, "data-1k") 28 | r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 1"]) 29 | assert 300 > r.response["status"] 30 | assert 1000 == len(r.response["body"]) 31 | 32 | r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 1b", "--trailer", "XXX: test"]) 33 | assert 300 > r.response["status"] 34 | assert 1000 == len(r.response["body"]) 35 | 36 | # check if the server survives a trailer without content-length 37 | def test_h2_202_02(self, env): 38 | url = env.mkurl("https", "cgi", "/echo.py") 39 | fpath = os.path.join(env.gen_dir, "data-1k") 40 | r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 2", "--no-content-length"]) 41 | assert 300 > r.response["status"] 42 | assert 1000 == len(r.response["body"]) 43 | 44 | # check if echoing request headers in response from GET works 45 | def test_h2_202_03(self, env): 46 | url = env.mkurl("https", "cgi", "/echohd.py?name=X") 47 | r = env.nghttp().get(url, options=["--header", "X: 3"]) 48 | assert 300 > r.response["status"] 49 | assert b"X: 3\n" == r.response["body"] 50 | 51 | # check if echoing request headers in response from POST works 52 | def test_h2_202_03b(self, env): 53 | url = env.mkurl("https", "cgi", "/echohd.py?name=X") 54 | r = env.nghttp().post_name(url, "Y", options=["--header", "X: 3b"]) 55 | assert 300 > r.response["status"] 56 | assert b"X: 3b\n" == r.response["body"] 57 | 58 | # check if echoing request headers in response from POST works, but trailers are not seen 59 | # This is the way CGI invocation works. 60 | def test_h2_202_04(self, env): 61 | url = env.mkurl("https", "cgi", "/echohd.py?name=X") 62 | r = env.nghttp().post_name(url, "Y", options=["--header", "X: 4a", "--trailer", "X: 4b"]) 63 | assert 300 > r.response["status"] 64 | assert b"X: 4a\n" == r.response["body"] 65 | 66 | -------------------------------------------------------------------------------- /test/modules/http2/test_002_curl_basics.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestCurlBasics: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | conf = H2Conf(env) 11 | conf.add_vhost_test1() 12 | conf.add_vhost_test2() 13 | conf.install() 14 | assert env.apache_restart() == 0 15 | 16 | # check that we see the correct documents when using the test1 server name over http: 17 | def test_h2_002_01(self, env): 18 | url = env.mkurl("http", "test1", "/alive.json") 19 | r = env.curl_get(url, 5) 20 | assert r.response["status"] == 200 21 | assert "HTTP/1.1" == r.response["protocol"] 22 | assert r.response["json"]["alive"] is True 23 | assert r.response["json"]["host"] == "test1" 24 | 25 | # check that we see the correct documents when using the test1 server name over https: 26 | def test_h2_002_02(self, env): 27 | url = env.mkurl("https", "test1", "/alive.json") 28 | r = env.curl_get(url, 5) 29 | assert r.response["status"] == 200 30 | assert r.response["json"]["alive"] is True 31 | assert "test1" == r.response["json"]["host"] 32 | assert r.response["header"]["content-type"] == "application/json" 33 | 34 | # enforce HTTP/1.1 35 | def test_h2_002_03(self, env): 36 | url = env.mkurl("https", "test1", "/alive.json") 37 | r = env.curl_get(url, 5, options=["--http1.1"]) 38 | assert r.response["status"] == 200 39 | assert r.response["protocol"] == "HTTP/1.1" 40 | 41 | # enforce HTTP/2 42 | def test_h2_002_04(self, env): 43 | url = env.mkurl("https", "test1", "/alive.json") 44 | r = env.curl_get(url, 5, options=["--http2"]) 45 | assert r.response["status"] == 200 46 | assert r.response["protocol"] == "HTTP/2" 47 | 48 | # default is HTTP/2 on this host 49 | def test_h2_002_04b(self, env): 50 | url = env.mkurl("https", "test1", "/alive.json") 51 | r = env.curl_get(url, 5) 52 | assert r.response["status"] == 200 53 | assert r.response["protocol"] == "HTTP/2" 54 | assert r.response["json"]["host"] == "test1" 55 | 56 | # although, without ALPN, we cannot select it 57 | def test_h2_002_05(self, env): 58 | url = env.mkurl("https", "test1", "/alive.json") 59 | r = env.curl_get(url, 5, options=["--no-alpn"]) 60 | assert r.response["status"] == 200 61 | assert r.response["protocol"] == "HTTP/1.1" 62 | assert r.response["json"]["host"] == "test1" 63 | 64 | # default is HTTP/1.1 on the other 65 | def test_h2_002_06(self, env): 66 | url = env.mkurl("https", "test2", "/alive.json") 67 | r = env.curl_get(url, 5) 68 | assert r.response["status"] == 200 69 | assert r.response["protocol"] == "HTTP/1.1" 70 | assert r.response["json"]["host"] == "test2" 71 | -------------------------------------------------------------------------------- /mod_http2/h2_workers.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_workers__ 18 | #define __mod_h2__h2_workers__ 19 | 20 | /* Thread pool specific to executing secondary connections. 21 | * Has a minimum and maximum number of workers it creates. 22 | * Starts with minimum workers and adds some on load, 23 | * reduces the number again when idle. 24 | */ 25 | struct apr_thread_mutex_t; 26 | struct apr_thread_cond_t; 27 | struct h2_mplx; 28 | struct h2_request; 29 | struct h2_fifo; 30 | 31 | struct h2_slot; 32 | 33 | typedef struct h2_workers h2_workers; 34 | 35 | struct h2_workers { 36 | server_rec *s; 37 | apr_pool_t *pool; 38 | 39 | int next_worker_id; 40 | apr_uint32_t max_workers; 41 | volatile apr_uint32_t min_workers; /* is changed during graceful shutdown */ 42 | volatile apr_interval_time_t max_idle_duration; /* is changed during graceful shutdown */ 43 | 44 | volatile int aborted; 45 | volatile int shutdown; 46 | int dynamic; 47 | 48 | apr_threadattr_t *thread_attr; 49 | int nslots; 50 | struct h2_slot *slots; 51 | 52 | volatile apr_uint32_t worker_count; 53 | 54 | struct h2_slot *free; 55 | struct h2_slot *idle; 56 | struct h2_slot *zombies; 57 | 58 | struct h2_fifo *mplxs; 59 | 60 | struct apr_thread_mutex_t *lock; 61 | struct apr_thread_cond_t *all_done; 62 | }; 63 | 64 | 65 | /* Create a worker pool with the given minimum and maximum number of 66 | * threads. 67 | */ 68 | h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pool, 69 | int min_size, int max_size, int idle_secs); 70 | 71 | /** 72 | * Registers a h2_mplx for scheduling. If this h2_mplx runs 73 | * out of work, it will be automatically be unregistered. Should 74 | * new work arrive, it needs to be registered again. 75 | */ 76 | apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m); 77 | 78 | /** 79 | * Remove a h2_mplx from the worker registry. 80 | */ 81 | apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m); 82 | 83 | /** 84 | * Shut down processing gracefully by terminating all idle workers. 85 | */ 86 | void h2_workers_graceful_shutdown(h2_workers *workers); 87 | 88 | #endif /* defined(__mod_h2__h2_workers__) */ 89 | -------------------------------------------------------------------------------- /test/modules/http2/test_006_assets.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestAssets: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add_vhost_test1().install() 11 | assert env.apache_restart() == 0 12 | 13 | # single page without any assets 14 | def test_h2_006_01(self, env): 15 | url = env.mkurl("https", "test1", "/001.html") 16 | r = env.nghttp().assets(url, options=["-Haccept-encoding: none"]) 17 | assert 0 == r.exit_code 18 | assert 1 == len(r.assets) 19 | assert r.assets == [ 20 | {"status": 200, "size": "251", "path": "/001.html"} 21 | ] 22 | 23 | # single image without any assets 24 | def test_h2_006_02(self, env): 25 | url = env.mkurl("https", "test1", "/002.jpg") 26 | r = env.nghttp().assets(url, options=["-Haccept-encoding: none"]) 27 | assert 0 == r.exit_code 28 | assert 1 == len(r.assets) 29 | assert r.assets == [ 30 | {"status": 200, "size": "88K", "path": "/002.jpg"} 31 | ] 32 | 33 | # gophertiles, yea! 34 | def test_h2_006_03(self, env): 35 | # create the tiles files we originally had checked in 36 | exp_assets = [ 37 | {"status": 200, "size": "10K", "path": "/004.html"}, 38 | {"status": 200, "size": "742", "path": "/004/gophertiles.jpg"}, 39 | ] 40 | for i in range(2, 181): 41 | with open(f"{env.server_docs_dir}/test1/004/gophertiles_{i:03d}.jpg", "w") as fd: 42 | fd.write("0123456789\n") 43 | exp_assets.append( 44 | {"status": 200, "size": "11", "path": f"/004/gophertiles_{i:03d}.jpg"}, 45 | ) 46 | 47 | url = env.mkurl("https", "test1", "/004.html") 48 | r = env.nghttp().assets(url, options=["-Haccept-encoding: none"]) 49 | assert 0 == r.exit_code 50 | assert 181 == len(r.assets) 51 | assert r.assets == exp_assets 52 | 53 | # page with js and css 54 | def test_h2_006_04(self, env): 55 | url = env.mkurl("https", "test1", "/006.html") 56 | r = env.nghttp().assets(url, options=["-Haccept-encoding: none"]) 57 | assert 0 == r.exit_code 58 | assert 3 == len(r.assets) 59 | assert r.assets == [ 60 | {"status": 200, "size": "543", "path": "/006.html"}, 61 | {"status": 200, "size": "216", "path": "/006/006.css"}, 62 | {"status": 200, "size": "839", "path": "/006/006.js"} 63 | ] 64 | 65 | # page with image, try different window size 66 | def test_h2_006_05(self, env): 67 | url = env.mkurl("https", "test1", "/003.html") 68 | r = env.nghttp().assets(url, options=["--window-bits=24", "-Haccept-encoding: none"]) 69 | assert 0 == r.exit_code 70 | assert 2 == len(r.assets) 71 | assert r.assets == [ 72 | {"status": 200, "size": "316", "path": "/003.html"}, 73 | {"status": 200, "size": "88K", "path": "/003/003_img.jpg"} 74 | ] 75 | -------------------------------------------------------------------------------- /test/unit/test_h2_util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "test_common.h" 22 | #include "h2_util.h" 23 | 24 | /* 25 | * Helpers 26 | */ 27 | 28 | /* 29 | * Test Fixture -- runs once per test 30 | */ 31 | 32 | static apr_pool_t *g_pool; 33 | 34 | static void h2_util_setup(void) 35 | { 36 | if (apr_pool_create(&g_pool, NULL) != APR_SUCCESS) { 37 | exit(1); 38 | } 39 | } 40 | 41 | static void h2_util_teardown(void) 42 | { 43 | apr_pool_destroy(g_pool); 44 | } 45 | 46 | static void base64_roundtrip(const char *buf_in, size_t buf_len) 47 | { 48 | const char *buf_out, *buf64; 49 | apr_size_t out_len; 50 | 51 | buf64 = h2_util_base64url_encode(buf_in, buf_len, g_pool); 52 | ck_assert(buf64); 53 | 54 | out_len = h2_util_base64url_decode(&buf_out, buf64, g_pool); 55 | 56 | ck_assert_int_eq(buf_len, out_len); 57 | ck_assert_mem_eq(buf_in, buf_out, buf_len); 58 | } 59 | 60 | /* 61 | * Tests 62 | */ 63 | START_TEST(base64_h2_util_roundtrip) 64 | { 65 | base64_roundtrip("1", 1); 66 | base64_roundtrip("12", 2); 67 | base64_roundtrip("123", 3); 68 | base64_roundtrip("1234", 4); 69 | base64_roundtrip("12345", 5); 70 | base64_roundtrip("123456", 6); 71 | base64_roundtrip("1234567", 7); 72 | base64_roundtrip("12345678", 8); 73 | base64_roundtrip("123456789", 9); 74 | } 75 | END_TEST 76 | 77 | static void largetrip(int step) 78 | { 79 | char buffer[256]; 80 | int i, start; 81 | 82 | for (start = 0; start < 256; ++start) { 83 | for (i = 0; i < 256; ++i) { 84 | buffer[(start+(i*step)) % 256] = (char)i; 85 | } 86 | base64_roundtrip(buffer, 256); 87 | } 88 | } 89 | 90 | START_TEST(base64_h2_util_largetrip) 91 | { 92 | largetrip(1); 93 | largetrip(3); 94 | largetrip(5); 95 | largetrip(17); 96 | largetrip(31); 97 | largetrip(53); 98 | largetrip(101); 99 | largetrip(167); 100 | largetrip(223); 101 | } 102 | END_TEST 103 | 104 | TCase *h2_util_test_case(void) 105 | { 106 | TCase *testcase = tcase_create("h2_util"); 107 | 108 | tcase_add_checked_fixture(testcase, h2_util_setup, h2_util_teardown); 109 | 110 | tcase_add_test(testcase, base64_h2_util_roundtrip); 111 | tcase_add_test(testcase, base64_h2_util_largetrip); 112 | 113 | return testcase; 114 | } 115 | -------------------------------------------------------------------------------- /mod_http2/h2_c1.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_c1__ 18 | #define __mod_h2__h2_c1__ 19 | 20 | #include 21 | 22 | struct h2_conn_ctx_t; 23 | 24 | extern APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_in) *h2_c_logio_add_bytes_in; 25 | extern APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_out) *h2_c_logio_add_bytes_out; 26 | 27 | /* Initialize this child process for h2 primary connection work, 28 | * to be called once during child init before multi processing 29 | * starts. 30 | */ 31 | apr_status_t h2_c1_child_init(apr_pool_t *pool, server_rec *s); 32 | 33 | /** 34 | * Setup the primary connection and our context for HTTP/2 processing 35 | * 36 | * @param c the connection HTTP/2 is starting on 37 | * @param r the upgrade request that still awaits an answer, optional 38 | * @param s the server selected for this connection (can be != c->base_server) 39 | */ 40 | apr_status_t h2_c1_setup(conn_rec *c, request_rec *r, server_rec *s); 41 | 42 | /** 43 | * Run the HTTP/2 primary connection in synchronous fashion. 44 | * Return when the HTTP/2 session is done 45 | * and the connection will close or a fatal error occurred. 46 | * 47 | * @param c the http2 connection to run 48 | * @return APR_SUCCESS when session is done. 49 | */ 50 | apr_status_t h2_c1_run(conn_rec *c); 51 | 52 | /** 53 | * The primary connection is about to close. If we have not send a GOAWAY 54 | * yet, this is the last chance. 55 | */ 56 | apr_status_t h2_c1_pre_close(struct h2_conn_ctx_t *ctx, conn_rec *c); 57 | 58 | /** 59 | * Check if the connection allows a direct detection of HTTPP/2, 60 | * as configurable by the H2Direct directive. 61 | * @param c the connection to check on 62 | * @return != 0 if direct detection is enabled 63 | */ 64 | int h2_c1_allows_direct(conn_rec *c); 65 | 66 | /** 67 | * Check if the "Upgrade" HTTP/1.1 mode of protocol switching is enabled 68 | * for the given request. 69 | * @param r the request to check 70 | * @return != 0 iff Upgrade switching is enabled 71 | */ 72 | int h2_c1_can_upgrade(request_rec *r); 73 | 74 | /* Register hooks for h2 handling on primary connections. 75 | */ 76 | void h2_c1_register_hooks(void); 77 | 78 | /** 79 | * Child is about to be stopped, release unused resources 80 | */ 81 | void h2_c1_child_stopping(apr_pool_t *pool, int graceful); 82 | 83 | #endif /* defined(__mod_h2__h2_c1__) */ 84 | -------------------------------------------------------------------------------- /test/modules/http2/test_201_header_conditional.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestConditionalHeaders: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add( 11 | """ 12 | KeepAlive on 13 | MaxKeepAliveRequests 30 14 | KeepAliveTimeout 30""" 15 | ).add_vhost_test1().install() 16 | assert env.apache_restart() == 0 17 | 18 | # check handling of 'if-modified-since' header 19 | def test_h2_201_01(self, env): 20 | url = env.mkurl("https", "test1", "/006/006.css") 21 | r = env.curl_get(url) 22 | assert r.response["status"] == 200 23 | lm = r.response["header"]["last-modified"] 24 | assert lm 25 | r = env.curl_get(url, options=["-H", "if-modified-since: %s" % lm]) 26 | assert 304 == r.response["status"] 27 | r = env.curl_get(url, options=["-H", "if-modified-since: Tue, 04 Sep 2010 11:51:59 GMT"]) 28 | assert r.response["status"] == 200 29 | 30 | # check handling of 'if-none-match' header 31 | def test_h2_201_02(self, env): 32 | url = env.mkurl("https", "test1", "/006/006.css") 33 | r = env.curl_get(url) 34 | assert r.response["status"] == 200 35 | etag = r.response["header"]["etag"] 36 | assert etag 37 | r = env.curl_get(url, options=["-H", "if-none-match: %s" % etag]) 38 | assert 304 == r.response["status"] 39 | r = env.curl_get(url, options=["-H", "if-none-match: dummy"]) 40 | assert r.response["status"] == 200 41 | 42 | @pytest.mark.skipif(True, reason="304 misses the Vary header in trunk and 2.4.x") 43 | def test_h2_201_03(self, env): 44 | url = env.mkurl("https", "test1", "/006.html") 45 | r = env.curl_get(url, options=["-H", "Accept-Encoding: gzip"]) 46 | assert r.response["status"] == 200 47 | for h in r.response["header"]: 48 | print("%s: %s" % (h, r.response["header"][h])) 49 | lm = r.response["header"]["last-modified"] 50 | assert lm 51 | assert "gzip" == r.response["header"]["content-encoding"] 52 | assert "Accept-Encoding" in r.response["header"]["vary"] 53 | 54 | r = env.curl_get(url, options=["-H", "if-modified-since: %s" % lm, 55 | "-H", "Accept-Encoding: gzip"]) 56 | assert 304 == r.response["status"] 57 | for h in r.response["header"]: 58 | print("%s: %s" % (h, r.response["header"][h])) 59 | assert "vary" in r.response["header"] 60 | 61 | # Check if "Keep-Alive" response header is removed in HTTP/2. 62 | def test_h2_201_04(self, env): 63 | url = env.mkurl("https", "test1", "/006.html") 64 | r = env.curl_get(url, options=["--http1.1", "-H", "Connection: keep-alive"]) 65 | assert r.response["status"] == 200 66 | assert "timeout=30, max=30" == r.response["header"]["keep-alive"] 67 | r = env.curl_get(url, options=["-H", "Connection: keep-alive"]) 68 | assert r.response["status"] == 200 69 | assert "keep-alive" not in r.response["header"] 70 | -------------------------------------------------------------------------------- /mod_http2/h2_c1_io.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_c1_io__ 18 | #define __mod_h2__h2_c1_io__ 19 | 20 | struct h2_config; 21 | struct h2_session; 22 | 23 | /* h2_io is the basic handler of a httpd connection. It keeps two brigades, 24 | * one for input, one for output and works with the installed connection 25 | * filters. 26 | * The read is done via a callback function, so that input can be processed 27 | * directly without copying. 28 | */ 29 | typedef struct { 30 | struct h2_session *session; 31 | apr_bucket_brigade *output; 32 | 33 | int is_tls; 34 | int unflushed; 35 | apr_time_t cooldown_usecs; 36 | apr_int64_t warmup_size; 37 | 38 | apr_size_t write_size; 39 | apr_time_t last_write; 40 | apr_int64_t bytes_read; 41 | apr_int64_t bytes_written; 42 | 43 | int buffer_output; 44 | apr_off_t buffered_len; 45 | apr_off_t flush_threshold; 46 | unsigned int is_flushed : 1; 47 | 48 | char *scratch; 49 | apr_size_t ssize; 50 | apr_size_t slen; 51 | } h2_c1_io; 52 | 53 | apr_status_t h2_c1_io_init(h2_c1_io *io, struct h2_session *session); 54 | 55 | /** 56 | * Append data to the buffered output. 57 | * @param buf the data to append 58 | * @param length the length of the data to append 59 | */ 60 | apr_status_t h2_c1_io_add_data(h2_c1_io *io, 61 | const char *buf, 62 | size_t length); 63 | 64 | apr_status_t h2_c1_io_add(h2_c1_io *io, apr_bucket *b); 65 | 66 | apr_status_t h2_c1_io_append(h2_c1_io *io, apr_bucket_brigade *bb); 67 | 68 | /** 69 | * Pass any buffered data on to the connection output filters. 70 | * @param io the connection io 71 | */ 72 | apr_status_t h2_c1_io_pass(h2_c1_io *io); 73 | 74 | /** 75 | * if there is any data pendiong or was any data send 76 | * since the last FLUSH, send out a FLUSH now. 77 | */ 78 | apr_status_t h2_c1_io_assure_flushed(h2_c1_io *io); 79 | 80 | /** 81 | * Check if the buffered amount of data needs flushing. 82 | */ 83 | int h2_c1_io_needs_flush(h2_c1_io *io); 84 | 85 | /** 86 | * Check if we have output pending. 87 | */ 88 | int h2_c1_io_pending(h2_c1_io *io); 89 | 90 | struct h2_session; 91 | 92 | /** 93 | * Read c1 input and pass it on to nghttp2. 94 | * @param session the session 95 | * @param when_pending != 0 if only pending input (sitting in filters) 96 | * needs to be read 97 | */ 98 | apr_status_t h2_c1_read(struct h2_session *session); 99 | 100 | #endif /* defined(__mod_h2__h2_c1_io__) */ 101 | -------------------------------------------------------------------------------- /mod_http2/h2_config.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_config_h__ 18 | #define __mod_h2__h2_config_h__ 19 | 20 | #undef PACKAGE_VERSION 21 | #undef PACKAGE_TARNAME 22 | #undef PACKAGE_STRING 23 | #undef PACKAGE_NAME 24 | #undef PACKAGE_BUGREPORT 25 | 26 | typedef enum { 27 | H2_CONF_MAX_STREAMS, 28 | H2_CONF_WIN_SIZE, 29 | H2_CONF_MIN_WORKERS, 30 | H2_CONF_MAX_WORKERS, 31 | H2_CONF_MAX_WORKER_IDLE_SECS, 32 | H2_CONF_STREAM_MAX_MEM, 33 | H2_CONF_DIRECT, 34 | H2_CONF_MODERN_TLS_ONLY, 35 | H2_CONF_UPGRADE, 36 | H2_CONF_TLS_WARMUP_SIZE, 37 | H2_CONF_TLS_COOLDOWN_SECS, 38 | H2_CONF_PUSH, 39 | H2_CONF_PUSH_DIARY_SIZE, 40 | H2_CONF_COPY_FILES, 41 | H2_CONF_EARLY_HINTS, 42 | H2_CONF_PADDING_BITS, 43 | H2_CONF_PADDING_ALWAYS, 44 | H2_CONF_OUTPUT_BUFFER, 45 | H2_CONF_STREAM_TIMEOUT, 46 | } h2_config_var_t; 47 | 48 | struct apr_hash_t; 49 | struct h2_priority; 50 | struct h2_push_res; 51 | 52 | typedef struct h2_push_res { 53 | const char *uri_ref; 54 | int critical; 55 | } h2_push_res; 56 | 57 | 58 | void *h2_config_create_dir(apr_pool_t *pool, char *x); 59 | void *h2_config_merge_dir(apr_pool_t *pool, void *basev, void *addv); 60 | void *h2_config_create_svr(apr_pool_t *pool, server_rec *s); 61 | void *h2_config_merge_svr(apr_pool_t *pool, void *basev, void *addv); 62 | 63 | extern const command_rec h2_cmds[]; 64 | 65 | int h2_config_geti(request_rec *r, server_rec *s, h2_config_var_t var); 66 | apr_int64_t h2_config_geti64(request_rec *r, server_rec *s, h2_config_var_t var); 67 | 68 | /** 69 | * Get the configured value for variable at the given connection. 70 | */ 71 | int h2_config_cgeti(conn_rec *c, h2_config_var_t var); 72 | apr_int64_t h2_config_cgeti64(conn_rec *c, h2_config_var_t var); 73 | 74 | /** 75 | * Get the configured value for variable at the given server. 76 | */ 77 | int h2_config_sgeti(server_rec *s, h2_config_var_t var); 78 | apr_int64_t h2_config_sgeti64(server_rec *s, h2_config_var_t var); 79 | 80 | /** 81 | * Get the configured value for variable at the given request, 82 | * if configured for the request location. 83 | * Fallback to request server config otherwise. 84 | */ 85 | int h2_config_rgeti(request_rec *r, h2_config_var_t var); 86 | apr_int64_t h2_config_rgeti64(request_rec *r, h2_config_var_t var); 87 | 88 | apr_array_header_t *h2_config_push_list(request_rec *r); 89 | 90 | 91 | void h2_get_num_workers(server_rec *s, int *minw, int *maxw); 92 | void h2_config_init(apr_pool_t *pool); 93 | 94 | const struct h2_priority *h2_cconfig_get_priority(conn_rec *c, const char *content_type); 95 | 96 | #endif /* __mod_h2__h2_config_h__ */ 97 | 98 | -------------------------------------------------------------------------------- /mod_http2/h2_bucket_eos.c: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "h2_private.h" 26 | #include "h2.h" 27 | #include "h2_mplx.h" 28 | #include "h2_stream.h" 29 | #include "h2_bucket_eos.h" 30 | 31 | typedef struct { 32 | apr_bucket_refcount refcount; 33 | h2_stream *stream; 34 | } h2_bucket_eos; 35 | 36 | static apr_status_t bucket_cleanup(void *data) 37 | { 38 | h2_stream **pstream = data; 39 | 40 | if (*pstream) { 41 | /* If bucket_destroy is called after us, this prevents 42 | * bucket_destroy from trying to destroy the stream again. */ 43 | *pstream = NULL; 44 | } 45 | return APR_SUCCESS; 46 | } 47 | 48 | static apr_status_t bucket_read(apr_bucket *b, const char **str, 49 | apr_size_t *len, apr_read_type_e block) 50 | { 51 | (void)b; 52 | (void)block; 53 | *str = NULL; 54 | *len = 0; 55 | return APR_SUCCESS; 56 | } 57 | 58 | apr_bucket *h2_bucket_eos_make(apr_bucket *b, h2_stream *stream) 59 | { 60 | h2_bucket_eos *h; 61 | 62 | h = apr_bucket_alloc(sizeof(*h), b->list); 63 | h->stream = stream; 64 | 65 | b = apr_bucket_shared_make(b, h, 0, 0); 66 | b->type = &h2_bucket_type_eos; 67 | 68 | return b; 69 | } 70 | 71 | apr_bucket *h2_bucket_eos_create(apr_bucket_alloc_t *list, 72 | h2_stream *stream) 73 | { 74 | apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); 75 | 76 | APR_BUCKET_INIT(b); 77 | b->free = apr_bucket_free; 78 | b->list = list; 79 | b = h2_bucket_eos_make(b, stream); 80 | if (stream) { 81 | h2_bucket_eos *h = b->data; 82 | apr_pool_pre_cleanup_register(stream->pool, &h->stream, bucket_cleanup); 83 | } 84 | return b; 85 | } 86 | 87 | static void bucket_destroy(void *data) 88 | { 89 | h2_bucket_eos *h = data; 90 | 91 | if (apr_bucket_shared_destroy(h)) { 92 | h2_stream *stream = h->stream; 93 | if (stream && stream->pool) { 94 | apr_pool_cleanup_kill(stream->pool, &h->stream, bucket_cleanup); 95 | } 96 | apr_bucket_free(h); 97 | if (stream) { 98 | h2_stream_dispatch(stream, H2_SEV_EOS_SENT); 99 | } 100 | } 101 | } 102 | 103 | const apr_bucket_type_t h2_bucket_type_eos = { 104 | "H2EOS", 5, APR_BUCKET_METADATA, 105 | bucket_destroy, 106 | bucket_read, 107 | apr_bucket_setaside_noop, 108 | apr_bucket_split_notimpl, 109 | apr_bucket_shared_copy 110 | }; 111 | 112 | -------------------------------------------------------------------------------- /m4/ax_check_compile_flag.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # http://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Check whether the given FLAG works with the current language's compiler 12 | # or gives an error. (Warnings, however, are ignored) 13 | # 14 | # ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on 15 | # success/failure. 16 | # 17 | # If EXTRA-FLAGS is defined, it is added to the current language's default 18 | # flags (e.g. CFLAGS) when the check is done. The check is thus made with 19 | # the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to 20 | # force the compiler to issue an error when a bad flag is given. 21 | # 22 | # INPUT gives an alternative input source to AC_COMPILE_IFELSE. 23 | # 24 | # NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this 25 | # macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG. 26 | # 27 | # LICENSE 28 | # 29 | # Copyright (c) 2008 Guido U. Draheim 30 | # Copyright (c) 2011 Maarten Bosmans 31 | # 32 | # This program is free software: you can redistribute it and/or modify it 33 | # under the terms of the GNU General Public License as published by the 34 | # Free Software Foundation, either version 3 of the License, or (at your 35 | # option) any later version. 36 | # 37 | # This program is distributed in the hope that it will be useful, but 38 | # WITHOUT ANY WARRANTY; without even the implied warranty of 39 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 40 | # Public License for more details. 41 | # 42 | # You should have received a copy of the GNU General Public License along 43 | # with this program. If not, see . 44 | # 45 | # As a special exception, the respective Autoconf Macro's copyright owner 46 | # gives unlimited permission to copy, distribute and modify the configure 47 | # scripts that are the output of Autoconf when processing the Macro. You 48 | # need not follow the terms of the GNU General Public License when using 49 | # or distributing such scripts, even though portions of the text of the 50 | # Macro appear in them. The GNU General Public License (GPL) does govern 51 | # all other use of the material that constitutes the Autoconf Macro. 52 | # 53 | # This special exception to the GPL applies to versions of the Autoconf 54 | # Macro released by the Autoconf Archive. When you make and distribute a 55 | # modified version of the Autoconf Macro, you may extend this special 56 | # exception to the GPL to apply to your modified version as well. 57 | 58 | #serial 3 59 | 60 | AC_DEFUN([AX_CHECK_COMPILE_FLAG], 61 | [AC_PREREQ(2.59)dnl for _AC_LANG_PREFIX 62 | AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl 63 | AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [ 64 | ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS 65 | _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1" 66 | AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], 67 | [AS_VAR_SET(CACHEVAR,[yes])], 68 | [AS_VAR_SET(CACHEVAR,[no])]) 69 | _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags]) 70 | AS_IF([test x"AS_VAR_GET(CACHEVAR)" = xyes], 71 | [m4_default([$2], :)], 72 | [m4_default([$3], :)]) 73 | AS_VAR_POPDEF([CACHEVAR])dnl 74 | ])dnl AX_CHECK_COMPILE_FLAGS 75 | -------------------------------------------------------------------------------- /mod_http2/h2_headers.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_headers__ 18 | #define __mod_h2__h2_headers__ 19 | 20 | #include "h2.h" 21 | 22 | struct h2_bucket_beam; 23 | 24 | extern const apr_bucket_type_t h2_bucket_type_headers; 25 | 26 | #define H2_BUCKET_IS_HEADERS(e) (e->type == &h2_bucket_type_headers) 27 | 28 | apr_bucket * h2_bucket_headers_make(apr_bucket *b, h2_headers *r); 29 | 30 | apr_bucket * h2_bucket_headers_create(apr_bucket_alloc_t *list, 31 | h2_headers *r); 32 | 33 | h2_headers *h2_bucket_headers_get(apr_bucket *b); 34 | 35 | apr_bucket *h2_bucket_headers_beam(struct h2_bucket_beam *beam, 36 | apr_bucket_brigade *dest, 37 | const apr_bucket *src); 38 | 39 | /** 40 | * Create the headers from the given status and headers 41 | * @param status the headers status 42 | * @param header the headers of the headers 43 | * @param notes the notes carried by the headers 44 | * @param raw_bytes the raw network bytes (if known) used to transmit these 45 | * @param pool the memory pool to use 46 | */ 47 | h2_headers *h2_headers_create(int status, const apr_table_t *header, 48 | const apr_table_t *notes, apr_off_t raw_bytes, 49 | apr_pool_t *pool); 50 | 51 | /** 52 | * Create the headers from the given request_rec. 53 | * @param r the request record which was processed 54 | * @param status the headers status 55 | * @param header the headers of the headers 56 | * @param pool the memory pool to use 57 | */ 58 | h2_headers *h2_headers_rcreate(request_rec *r, int status, 59 | const apr_table_t *header, apr_pool_t *pool); 60 | 61 | /** 62 | * Copy the headers into another pool. This will not copy any 63 | * header strings. 64 | */ 65 | h2_headers *h2_headers_copy(apr_pool_t *pool, h2_headers *h); 66 | 67 | /** 68 | * Clone the headers into another pool. This will also clone any 69 | * header strings. 70 | */ 71 | h2_headers *h2_headers_clone(apr_pool_t *pool, h2_headers *h); 72 | 73 | /** 74 | * Create the headers for the given error. 75 | * @param type the error code 76 | * @param req the original h2_request 77 | * @param pool the memory pool to use 78 | */ 79 | h2_headers *h2_headers_die(apr_status_t type, 80 | const struct h2_request *req, apr_pool_t *pool); 81 | 82 | int h2_headers_are_final_response(h2_headers *headers); 83 | 84 | /** 85 | * Give the number of bytes of all contained header strings. 86 | */ 87 | apr_size_t h2_headers_length(h2_headers *headers); 88 | 89 | /** 90 | * For H2HEADER buckets, return the length of all contained header strings. 91 | * For all other buckets, return 0. 92 | */ 93 | apr_size_t h2_bucket_headers_headers_length(apr_bucket *b); 94 | 95 | #endif /* defined(__mod_h2__h2_headers__) */ 96 | -------------------------------------------------------------------------------- /test/modules/http2/data/nghttp-output-1k-1.txt: -------------------------------------------------------------------------------- 1 | stderr: [WARNING] Certificate verification failed: unable to verify the first certificate 2 | 3 | stdout: [ 0.002] Connected 4 | The negotiated protocol: h2 5 | [ 0.004] send SETTINGS frame 6 | (niv=2) 7 | [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] 8 | [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] 9 | [ 0.004] send PRIORITY frame 10 | (dep_stream_id=0, weight=201, exclusive=0) 11 | [ 0.004] send PRIORITY frame 12 | (dep_stream_id=0, weight=101, exclusive=0) 13 | [ 0.004] send PRIORITY frame 14 | (dep_stream_id=0, weight=1, exclusive=0) 15 | [ 0.004] send PRIORITY frame 16 | (dep_stream_id=7, weight=1, exclusive=0) 17 | [ 0.004] send PRIORITY frame 18 | (dep_stream_id=3, weight=1, exclusive=0) 19 | [ 0.004] send HEADERS frame 20 | ; END_HEADERS | PRIORITY 21 | (padlen=0, dep_stream_id=11, weight=16, exclusive=0) 22 | ; Open new stream 23 | :method: POST 24 | :path: //echo.py 25 | :scheme: https 26 | :authority: 127.0.0.1:42002 27 | accept: */* 28 | accept-encoding: gzip, deflate 29 | user-agent: nghttp2/1.33.0 30 | content-length: 1000 31 | host: cgi.tests.httpd.apache.org:42002 32 | [ 0.004] send DATA frame 33 | ; END_STREAM 34 | [ 0.005] recv SETTINGS frame 35 | (niv=1) 36 | [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] 37 | [ 0.005] recv SETTINGS frame 38 | ; ACK 39 | (niv=0) 40 | [ 0.005] recv WINDOW_UPDATE frame 41 | (window_size_increment=2147418112) 42 | [ 0.005] send SETTINGS frame 43 | ; ACK 44 | (niv=0) 45 | [ 0.048] recv (stream_id=13) :status: 200 46 | [ 0.048] recv (stream_id=13) date: Thu, 14 Feb 2019 11:48:18 GMT 47 | [ 0.048] recv (stream_id=13) server: Apache/2.4.39-dev (Unix) OpenSSL/1.1.1 48 | [ 0.048] recv (stream_id=13) content-type: application/data 49 | [ 0.048] recv HEADERS frame 50 | ; END_HEADERS 51 | (padlen=0) 52 | ; First response header 53 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 54 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 55 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 56 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 57 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 58 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 59 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 60 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 61 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 62 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 63 | [ 0.048] recv DATA frame 64 | [ 0.048] recv DATA frame 65 | ; END_STREAM 66 | [ 0.048] send GOAWAY frame 67 | (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) 68 | 69 | -------------------------------------------------------------------------------- /mod_http2/mod_http2.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __MOD_HTTP2_H__ 18 | #define __MOD_HTTP2_H__ 19 | 20 | /** The http2_var_lookup() optional function retrieves HTTP2 environment 21 | * variables. */ 22 | APR_DECLARE_OPTIONAL_FN(char *, 23 | http2_var_lookup, (apr_pool_t *, server_rec *, 24 | conn_rec *, request_rec *, char *)); 25 | 26 | /** An optional function which returns non-zero if the given connection 27 | * or its master connection is using HTTP/2. */ 28 | APR_DECLARE_OPTIONAL_FN(int, 29 | http2_is_h2, (conn_rec *)); 30 | 31 | 32 | /******************************************************************************* 33 | * START HTTP/2 request engines (DEPRECATED) 34 | ******************************************************************************/ 35 | 36 | /* The following functions were introduced for the experimental mod_proxy_http2 37 | * support, but have been abandoned since. 38 | * They are still declared here for backward compatibility, in case someone 39 | * tries to build an old mod_proxy_http2 against it, but will disappear 40 | * completely sometime in the future. 41 | */ 42 | 43 | struct apr_thread_cond_t; 44 | typedef struct h2_req_engine h2_req_engine; 45 | typedef void http2_output_consumed(void *ctx, conn_rec *c, apr_off_t consumed); 46 | 47 | typedef apr_status_t http2_req_engine_init(h2_req_engine *engine, 48 | const char *id, 49 | const char *type, 50 | apr_pool_t *pool, 51 | apr_size_t req_buffer_size, 52 | request_rec *r, 53 | http2_output_consumed **pconsumed, 54 | void **pbaton); 55 | 56 | APR_DECLARE_OPTIONAL_FN(apr_status_t, 57 | http2_req_engine_push, (const char *engine_type, 58 | request_rec *r, 59 | http2_req_engine_init *einit)); 60 | 61 | APR_DECLARE_OPTIONAL_FN(apr_status_t, 62 | http2_req_engine_pull, (h2_req_engine *engine, 63 | apr_read_type_e block, 64 | int capacity, 65 | request_rec **pr)); 66 | APR_DECLARE_OPTIONAL_FN(void, 67 | http2_req_engine_done, (h2_req_engine *engine, 68 | conn_rec *rconn, 69 | apr_status_t status)); 70 | 71 | APR_DECLARE_OPTIONAL_FN(void, 72 | http2_get_num_workers, (server_rec *s, 73 | int *minw, int *max)); 74 | 75 | /******************************************************************************* 76 | * END HTTP/2 request engines (DEPRECATED) 77 | ******************************************************************************/ 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /test/modules/http2/test_104_padding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | def frame_padding(payload, padbits): 7 | mask = (1 << padbits) - 1 8 | return ((payload + 9 + mask) & ~mask) - (payload + 9) 9 | 10 | 11 | class TestPadding: 12 | 13 | @pytest.fixture(autouse=True, scope='class') 14 | def _class_scope(self, env): 15 | conf = H2Conf(env) 16 | conf.start_vhost(domains=[f"ssl.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") 17 | conf.add("AddHandler cgi-script .py") 18 | conf.end_vhost() 19 | conf.start_vhost(domains=[f"pad0.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") 20 | conf.add("H2Padding 0") 21 | conf.add("AddHandler cgi-script .py") 22 | conf.end_vhost() 23 | conf.start_vhost(domains=[f"pad1.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") 24 | conf.add("H2Padding 1") 25 | conf.add("AddHandler cgi-script .py") 26 | conf.end_vhost() 27 | conf.start_vhost(domains=[f"pad2.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") 28 | conf.add("H2Padding 2") 29 | conf.add("AddHandler cgi-script .py") 30 | conf.end_vhost() 31 | conf.start_vhost(domains=[f"pad3.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") 32 | conf.add("H2Padding 3") 33 | conf.add("AddHandler cgi-script .py") 34 | conf.end_vhost() 35 | conf.start_vhost(domains=[f"pad8.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") 36 | conf.add("H2Padding 8") 37 | conf.add("AddHandler cgi-script .py") 38 | conf.end_vhost() 39 | conf.install() 40 | assert env.apache_restart() == 0 41 | 42 | # default paddings settings: 0 bits 43 | def test_h2_104_01(self, env): 44 | url = env.mkurl("https", "ssl", "/echo.py") 45 | # we get 2 frames back: one with data and an empty one with EOF 46 | # check the number of padding bytes is as expected 47 | for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]: 48 | r = env.nghttp().post_data(url, data, 5) 49 | assert r.response["status"] == 200 50 | assert r.results["paddings"] == [ 51 | frame_padding(len(data)+1, 0), 52 | frame_padding(0, 0) 53 | ] 54 | 55 | # 0 bits of padding 56 | def test_h2_104_02(self, env): 57 | url = env.mkurl("https", "pad0", "/echo.py") 58 | for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]: 59 | r = env.nghttp().post_data(url, data, 5) 60 | assert r.response["status"] == 200 61 | assert r.results["paddings"] == [0, 0] 62 | 63 | # 1 bit of padding 64 | def test_h2_104_03(self, env): 65 | url = env.mkurl("https", "pad1", "/echo.py") 66 | for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]: 67 | r = env.nghttp().post_data(url, data, 5) 68 | assert r.response["status"] == 200 69 | for i in r.results["paddings"]: 70 | assert i in range(0, 2) 71 | 72 | # 2 bits of padding 73 | def test_h2_104_04(self, env): 74 | url = env.mkurl("https", "pad2", "/echo.py") 75 | for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]: 76 | r = env.nghttp().post_data(url, data, 5) 77 | assert r.response["status"] == 200 78 | for i in r.results["paddings"]: 79 | assert i in range(0, 4) 80 | 81 | # 3 bits of padding 82 | def test_h2_104_05(self, env): 83 | url = env.mkurl("https", "pad3", "/echo.py") 84 | for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]: 85 | r = env.nghttp().post_data(url, data, 5) 86 | assert r.response["status"] == 200 87 | for i in r.results["paddings"]: 88 | assert i in range(0, 8) 89 | 90 | # 8 bits of padding 91 | def test_h2_104_06(self, env): 92 | url = env.mkurl("https", "pad8", "/echo.py") 93 | for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]: 94 | r = env.nghttp().post_data(url, data, 5) 95 | assert r.response["status"] == 200 96 | for i in r.results["paddings"]: 97 | assert i in range(0, 256) 98 | -------------------------------------------------------------------------------- /mod_http2/h2_conn_ctx.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_conn_ctx__ 18 | #define __mod_h2__h2_conn_ctx__ 19 | 20 | struct h2_session; 21 | struct h2_stream; 22 | struct h2_mplx; 23 | struct h2_bucket_beam; 24 | struct h2_response_parser; 25 | 26 | #define H2_PIPE_OUT 0 27 | #define H2_PIPE_IN 1 28 | 29 | /** 30 | * The h2 module context associated with a connection. 31 | * 32 | * It keeps track of the different types of connections: 33 | * - those from clients that use HTTP/2 protocol 34 | * - those from clients that do not use HTTP/2 35 | * - those created by ourself to perform work on HTTP/2 streams 36 | */ 37 | struct h2_conn_ctx_t { 38 | const char *id; /* c*: our identifier of this connection */ 39 | server_rec *server; /* c*: httpd server selected. */ 40 | const char *protocol; /* c1: the protocol negotiated */ 41 | struct h2_session *session; /* c1: the h2 session established */ 42 | struct h2_mplx *mplx; /* c2: the multiplexer */ 43 | 44 | int pre_conn_done; /* has pre_connection setup run? */ 45 | int stream_id; /* c1: 0, c2: stream id processed */ 46 | apr_pool_t *req_pool; /* c2: a c2 child pool for a request */ 47 | const struct h2_request *request; /* c2: the request to process */ 48 | struct h2_bucket_beam *beam_out; /* c2: data out, created from req_pool */ 49 | struct h2_bucket_beam *beam_in; /* c2: data in or NULL, borrowed from request stream */ 50 | 51 | apr_pool_t *mplx_pool; /* c2: an mplx child pool for safe use inside mplx lock */ 52 | apr_file_t *pipe_in_prod[2]; /* c2: input produced notification pipe */ 53 | apr_file_t *pipe_in_drain[2]; /* c2: input drained notification pipe */ 54 | apr_file_t *pipe_out_prod[2]; /* c2: output produced notification pipe */ 55 | 56 | apr_pollfd_t pfd_in_drain; /* c2: poll pipe_in_drain output */ 57 | apr_pollfd_t pfd_out_prod; /* c2: poll pipe_out_prod output */ 58 | 59 | int has_final_response; /* final HTTP response passed on out */ 60 | apr_status_t last_err; /* APR_SUCCES or last error encountered in filters */ 61 | struct h2_response_parser *parser; /* optional parser to catch H1 responses */ 62 | 63 | volatile int done; /* c2: processing has finished */ 64 | apr_time_t started_at; /* c2: when processing started */ 65 | apr_time_t done_at; /* c2: when processing was done */ 66 | }; 67 | typedef struct h2_conn_ctx_t h2_conn_ctx_t; 68 | 69 | /** 70 | * Get the h2 connection context. 71 | * @param c the connection to look at 72 | * @return h2 context of this connection 73 | */ 74 | #define h2_conn_ctx_get(c) \ 75 | ((c)? (h2_conn_ctx_t*)ap_get_module_config((c)->conn_config, &http2_module) : NULL) 76 | 77 | /** 78 | * Create the h2 connection context. 79 | * @param c the connection to create it at 80 | * @param s the server in use 81 | * @param protocol the procotol selected 82 | * @return created h2 context of this connection 83 | */ 84 | h2_conn_ctx_t *h2_conn_ctx_create_for_c1(conn_rec *c, server_rec *s, const char *protocol); 85 | 86 | apr_status_t h2_conn_ctx_init_for_c2(h2_conn_ctx_t **pctx, conn_rec *c, 87 | struct h2_mplx *mplx, struct h2_stream *stream); 88 | 89 | void h2_conn_ctx_clear_for_c2(conn_rec *c2); 90 | 91 | void h2_conn_ctx_detach(conn_rec *c); 92 | 93 | void h2_conn_ctx_destroy(conn_rec *c); 94 | 95 | void h2_conn_ctx_set_timeout(h2_conn_ctx_t *conn_ctx, apr_interval_time_t timeout); 96 | 97 | #endif /* defined(__mod_h2__h2_conn_ctx__) */ 98 | -------------------------------------------------------------------------------- /test/modules/http2/test_103_upgrade.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .env import H2Conf 4 | 5 | 6 | class TestUpgrade: 7 | 8 | @pytest.fixture(autouse=True, scope='class') 9 | def _class_scope(self, env): 10 | H2Conf(env).add_vhost_test1().add_vhost_test2().add_vhost_noh2( 11 | ).start_vhost(domains=[f"test3.{env.http_tld}"], port=env.https_port, doc_root="htdocs/test1" 12 | ).add( 13 | """ 14 | Protocols h2 http/1.1 15 | Header unset Upgrade""" 16 | ).end_vhost( 17 | ).start_vhost(domains=[f"test1b.{env.http_tld}"], port=env.http_port, doc_root="htdocs/test1" 18 | ).add( 19 | """ 20 | Protocols h2c http/1.1 21 | H2Upgrade off 22 | 23 | H2Upgrade on 24 | """ 25 | ).end_vhost( 26 | ).install() 27 | assert env.apache_restart() == 0 28 | 29 | # accessing http://test1, will not try h2 and advertise h2 in the response 30 | def test_h2_103_01(self, env): 31 | url = env.mkurl("http", "test1", "/index.html") 32 | r = env.curl_get(url) 33 | assert 0 == r.exit_code 34 | assert r.response 35 | assert "upgrade" in r.response["header"] 36 | assert "h2c" == r.response["header"]["upgrade"] 37 | 38 | # accessing http://noh2, will not advertise, because noh2 host does not have it enabled 39 | def test_h2_103_02(self, env): 40 | url = env.mkurl("http", "noh2", "/index.html") 41 | r = env.curl_get(url) 42 | assert 0 == r.exit_code 43 | assert r.response 44 | assert "upgrade" not in r.response["header"] 45 | 46 | # accessing http://test2, will not advertise, because h2 has less preference than http/1.1 47 | def test_h2_103_03(self, env): 48 | url = env.mkurl("http", "test2", "/index.html") 49 | r = env.curl_get(url) 50 | assert 0 == r.exit_code 51 | assert r.response 52 | assert "upgrade" not in r.response["header"] 53 | 54 | # accessing https://noh2, will not advertise, because noh2 host does not have it enabled 55 | def test_h2_103_04(self, env): 56 | url = env.mkurl("https", "noh2", "/index.html") 57 | r = env.curl_get(url) 58 | assert 0 == r.exit_code 59 | assert r.response 60 | assert "upgrade" not in r.response["header"] 61 | 62 | # accessing https://test2, will not advertise, because h2 has less preference than http/1.1 63 | def test_h2_103_05(self, env): 64 | url = env.mkurl("https", "test2", "/index.html") 65 | r = env.curl_get(url) 66 | assert 0 == r.exit_code 67 | assert r.response 68 | assert "upgrade" not in r.response["header"] 69 | 70 | # accessing https://test1, will advertise h2 in the response 71 | def test_h2_103_06(self, env): 72 | url = env.mkurl("https", "test1", "/index.html") 73 | r = env.curl_get(url, options=["--http1.1"]) 74 | assert 0 == r.exit_code 75 | assert r.response 76 | assert "upgrade" in r.response["header"] 77 | assert "h2" == r.response["header"]["upgrade"] 78 | 79 | # accessing https://test3, will not send Upgrade since it is suppressed 80 | def test_h2_103_07(self, env): 81 | url = env.mkurl("https", "test3", "/index.html") 82 | r = env.curl_get(url, options=["--http1.1"]) 83 | assert 0 == r.exit_code 84 | assert r.response 85 | assert "upgrade" not in r.response["header"] 86 | 87 | # upgrade to h2c for a request, where h2c is preferred 88 | def test_h2_103_20(self, env): 89 | url = env.mkurl("http", "test1", "/index.html") 90 | r = env.nghttp().get(url, options=["-u"]) 91 | assert r.response["status"] == 200 92 | 93 | # upgrade to h2c for a request where http/1.1 is preferred, but the clients upgrade 94 | # wish is honored nevertheless 95 | def test_h2_103_21(self, env): 96 | url = env.mkurl("http", "test2", "/index.html") 97 | r = env.nghttp().get(url, options=["-u"]) 98 | assert 404 == r.response["status"] 99 | 100 | # ugrade to h2c on a host where h2c is not enabled will fail 101 | def test_h2_103_22(self, env): 102 | url = env.mkurl("http", "noh2", "/index.html") 103 | r = env.nghttp().get(url, options=["-u"]) 104 | assert not r.response 105 | 106 | # ugrade to h2c on a host where h2c is preferred, but Upgrade is disabled 107 | def test_h2_103_23(self, env): 108 | url = env.mkurl("http", "test1b", "/index.html") 109 | r = env.nghttp().get(url, options=["-u"]) 110 | assert not r.response 111 | 112 | # ugrade to h2c on a host where h2c is preferred, but Upgrade is disabled on the server, 113 | # but allowed for a specific location 114 | def test_h2_103_24(self, env): 115 | url = env.mkurl("http", "test1b", "/006.html") 116 | r = env.nghttp().get(url, options=["-u"]) 117 | assert r.response["status"] == 200 118 | -------------------------------------------------------------------------------- /mod_http2/h2_conn_ctx.c: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "h2_private.h" 26 | #include "h2_session.h" 27 | #include "h2_bucket_beam.h" 28 | #include "h2_c2.h" 29 | #include "h2_mplx.h" 30 | #include "h2_stream.h" 31 | #include "h2_util.h" 32 | #include "h2_conn_ctx.h" 33 | 34 | 35 | void h2_conn_ctx_detach(conn_rec *c) 36 | { 37 | ap_set_module_config(c->conn_config, &http2_module, NULL); 38 | } 39 | 40 | static h2_conn_ctx_t *ctx_create(conn_rec *c, const char *id) 41 | { 42 | h2_conn_ctx_t *conn_ctx = apr_pcalloc(c->pool, sizeof(*conn_ctx)); 43 | conn_ctx->id = id; 44 | conn_ctx->server = c->base_server; 45 | conn_ctx->started_at = apr_time_now(); 46 | 47 | ap_set_module_config(c->conn_config, &http2_module, conn_ctx); 48 | return conn_ctx; 49 | } 50 | 51 | h2_conn_ctx_t *h2_conn_ctx_create_for_c1(conn_rec *c1, server_rec *s, const char *protocol) 52 | { 53 | h2_conn_ctx_t *ctx; 54 | 55 | ctx = ctx_create(c1, apr_psprintf(c1->pool, "%ld", c1->id)); 56 | ctx->server = s; 57 | ctx->protocol = apr_pstrdup(c1->pool, protocol); 58 | 59 | ctx->pfd_out_prod.desc_type = APR_POLL_SOCKET; 60 | ctx->pfd_out_prod.desc.s = ap_get_conn_socket(c1); 61 | apr_socket_opt_set(ctx->pfd_out_prod.desc.s, APR_SO_NONBLOCK, 1); 62 | ctx->pfd_out_prod.reqevents = APR_POLLIN | APR_POLLERR | APR_POLLHUP; 63 | ctx->pfd_out_prod.client_data = ctx; 64 | 65 | return ctx; 66 | } 67 | 68 | apr_status_t h2_conn_ctx_init_for_c2(h2_conn_ctx_t **pctx, conn_rec *c2, 69 | struct h2_mplx *mplx, struct h2_stream *stream) 70 | { 71 | h2_conn_ctx_t *conn_ctx; 72 | apr_status_t rv = APR_SUCCESS; 73 | 74 | ap_assert(c2->master); 75 | conn_ctx = h2_conn_ctx_get(c2); 76 | if (!conn_ctx) { 77 | h2_conn_ctx_t *c1_ctx; 78 | 79 | c1_ctx = h2_conn_ctx_get(c2->master); 80 | ap_assert(c1_ctx); 81 | ap_assert(c1_ctx->session); 82 | 83 | conn_ctx = ctx_create(c2, c1_ctx->id); 84 | conn_ctx->server = c2->master->base_server; 85 | } 86 | 87 | conn_ctx->mplx = mplx; 88 | conn_ctx->stream_id = stream->id; 89 | apr_pool_create(&conn_ctx->req_pool, c2->pool); 90 | apr_pool_tag(conn_ctx->req_pool, "H2_C2_REQ"); 91 | conn_ctx->request = stream->request; 92 | conn_ctx->started_at = apr_time_now(); 93 | conn_ctx->done = 0; 94 | conn_ctx->done_at = 0; 95 | 96 | *pctx = conn_ctx; 97 | return rv; 98 | } 99 | 100 | void h2_conn_ctx_clear_for_c2(conn_rec *c2) 101 | { 102 | h2_conn_ctx_t *conn_ctx; 103 | 104 | ap_assert(c2->master); 105 | conn_ctx = h2_conn_ctx_get(c2); 106 | conn_ctx->stream_id = -1; 107 | conn_ctx->request = NULL; 108 | 109 | if (conn_ctx->req_pool) { 110 | apr_pool_destroy(conn_ctx->req_pool); 111 | conn_ctx->req_pool = NULL; 112 | conn_ctx->beam_out = NULL; 113 | } 114 | memset(&conn_ctx->pfd_in_drain, 0, sizeof(conn_ctx->pfd_in_drain)); 115 | memset(&conn_ctx->pfd_out_prod, 0, sizeof(conn_ctx->pfd_out_prod)); 116 | conn_ctx->beam_in = NULL; 117 | } 118 | 119 | void h2_conn_ctx_destroy(conn_rec *c) 120 | { 121 | h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); 122 | 123 | if (conn_ctx) { 124 | if (conn_ctx->mplx_pool) { 125 | apr_pool_destroy(conn_ctx->mplx_pool); 126 | conn_ctx->mplx_pool = NULL; 127 | } 128 | ap_set_module_config(c->conn_config, &http2_module, NULL); 129 | } 130 | } 131 | 132 | void h2_conn_ctx_set_timeout(h2_conn_ctx_t *conn_ctx, apr_interval_time_t timeout) 133 | { 134 | if (conn_ctx->beam_out) { 135 | h2_beam_timeout_set(conn_ctx->beam_out, timeout); 136 | } 137 | if (conn_ctx->pipe_out_prod[H2_PIPE_OUT]) { 138 | apr_file_pipe_timeout_set(conn_ctx->pipe_out_prod[H2_PIPE_OUT], timeout); 139 | } 140 | 141 | if (conn_ctx->beam_in) { 142 | h2_beam_timeout_set(conn_ctx->beam_in, timeout); 143 | } 144 | if (conn_ctx->pipe_in_prod[H2_PIPE_OUT]) { 145 | apr_file_pipe_timeout_set(conn_ctx->pipe_in_prod[H2_PIPE_OUT], timeout); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # mod_h[ttp]2 - http/2 for Apache httpd 3 | 4 | This repository contains `mod_h[ttp]2` and `mod_proxy_h[ttp]2` from Apache httpd as a standalone build. It servers as early access to features and fixes before being shipped in the next Apache release. Both modules can be considered **production ready** and stable as shipped by the Apache project. 5 | 6 | ## Status 7 | 8 | **`mod_h[ttp]2` is an official Apache httpd module** since release 2.4.17. `mod_proxy_h[ttp]2` has been added in Apache in 2.4.23. The versions here at github are for more frequent releases than the Apache schedule provides for. 9 | 10 | ## Thanks 11 | 12 | The following beautiful people have directly contributed to this project via commits over the years: 13 | Julian Reschke, Lubos Uhliarik, Luca Toscano, MATSUMOTO Ryosuke, 14 | Michael Kaufmann, Michael Köller, Mike Frysinger, Nicholas Race, 15 | Nicolas Noble, Petri Koistinen, Sam Hurst, Tatsuhiro Tsujikawa. 16 | 17 | ## Install 18 | 19 | You need a built Apache httpd 2.4.34 or newer, including apxs and headers to compile and 20 | run this module. Additionally, you need an installed libnghttp2, at least in version 21 | 1.7.0. And additionally, you want an installed OpenSSL 1.0.2 or later. 22 | 23 | tl;dr 24 | 25 | **You need an installed recent Apache 2.4.x** 26 | 27 | ## Apache 2.4.x Packages 28 | 29 | * **Ubuntu**: [ppa by ondrej](https://launchpad.net/~ondrej/+archive/ubuntu/apache2) for Ubuntu 14.04 and others 30 | * **Fedora**: [shipped in Fedora 23 and later](https://bodhi.fedoraproject.org/updates/?packages=httpd) 31 | * **Debian** sid (unstable) [how to install debian sid](https://wiki.debian.org/InstallFAQ#Q._How_do_I_install_.22unstable.22_.28.22sid.22.29.3F) 32 | * **Gentoo**: [latest stable](https://packages.gentoo.org/packages/www-servers/apache) 33 | * **FreeBSD**: [Apache 2.4 port includes mod_http2](http://www.freshports.org/www/apache24/) / [mod_http2 port](http://www.freshports.org/www/mod_http2/) 34 | 35 | ## Changes 36 | 37 | See ```ChangeLog``` for details. 38 | 39 | ## Tests 40 | 41 | I decided to make the test suite part of this repository again. The existing suite resides 42 | in test Apache httpd test repository and is a set of shell scripts. It works, but I miss 43 | features that professional test frameworks bring. The tests included here use ```python3``` and ```pytest``` which I think is an excellent way to do tests. I use it also in my Let's Encrypt module ```mod_md```. 44 | 45 | You can build the module without having the test prerequisites. If you want to run them, however, you need ```pytest```, ```python3``` and a ```curl``` with http2 support. Then you can 46 | 47 | ``` 48 | > make 49 | > make test 50 | ``` 51 | 52 | 53 | ## `mod_proxy_http2` 54 | 55 | This module is part of the Apache httpd proxy architecture and functions similar to `mod_proxy_http` 56 | and friends. To configure it, you need to use ```h2:``` or ```h2c:``` in the proxy URL. Example: 57 | 58 | ``` 59 | 60 | BalancerMember "h2://test.example.org:SUBST_PORT_HTTPS_SUBST" 61 | 62 | 63 | BalancerMember "h2c://test.example.org:SUBST_PORT_HTTP_SUBST" 64 | 65 | 66 | 67 | ProxyPass "/h2proxy" "balancer://h2-local" 68 | ProxyPassReverse "/h2proxy" "balancer://h2-local" 69 | ProxyPass "/h2cproxy" "balancer://h2c-local" 70 | ProxyPassReverse "/h2cproxy" "balancer://h2c-local" 71 | 72 | ``` 73 | 74 | This will only work under the following conditions: 75 | * the backend speaks HTTP/2, the module will not fallback to HTTP/1.1 76 | * the backend supports HTTP/2 direct mode (see also ```H2Direct``` directive of ```mod_http2```) 77 | 78 | All other common httpd ```proxy``` directives also apply. 79 | 80 | 81 | ## Documentation 82 | 83 | The official [Apache documentation of the module](https://httpd.apache.org/docs/2.4/en/mod/mod_http2.html). 84 | 85 | I also compiled a [how to h2 in apache](https://icing.github.io/mod_h2/howto.html) document with advice on how to deploy, configure and verify your ```mod_h[ttp]2``` installation. 86 | 87 | ## Build from git 88 | 89 | Still not dissuaded? Ok, here are some hints to get you started. 90 | Building from git is easy, but please be sure that at least autoconf 2.68 is 91 | used: 92 | 93 | ``` 94 | > autoreconf -i 95 | > automake 96 | > autoconf 97 | > ./configure --with-apxs= 98 | > make 99 | ``` 100 | 101 | ## Licensing 102 | 103 | Please see the file called LICENSE. 104 | 105 | ## Credits 106 | 107 | This work has been funded by the GSM Association (http://gsma.com). The module 108 | itself was heavily influenced by mod_spdy, the Google implementation of their 109 | SPDY protocol. And without Tatsuhiro Tsujikawa excellent nghttp2 work, this 110 | would not have been possible. 111 | 112 | 113 | Münster, 04.11.2019, 114 | 115 | Stefan Eissing, greenbytes GmbH 116 | 117 | Copying and distribution of this file, with or without modification, 118 | are permitted in any medium without royalty provided the copyright 119 | notice and this notice are preserved. This file is offered as-is, 120 | without warranty of any kind. See LICENSE for details. 121 | 122 | 123 | -------------------------------------------------------------------------------- /test/pyhttpd/curl.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import subprocess 4 | import sys 5 | import time 6 | from threading import Thread 7 | 8 | from .env import HttpdTestEnv 9 | 10 | 11 | class CurlPiper: 12 | 13 | def __init__(self, env: HttpdTestEnv, url: str): 14 | self.env = env 15 | self.url = url 16 | self.proc = None 17 | self.args = None 18 | self.headerfile = None 19 | self._stderr = [] 20 | self._stdout = [] 21 | self.stdout_thread = None 22 | self.stderr_thread = None 23 | self._exitcode = -1 24 | self._r = None 25 | 26 | @property 27 | def exitcode(self): 28 | return self._exitcode 29 | 30 | @property 31 | def response(self): 32 | return self._r.response if self._r else None 33 | 34 | def start(self): 35 | self.args, self.headerfile = self.env.curl_complete_args(self.url, timeout=5, options=[ 36 | "-T", "-", "-X", "POST", "--trace-ascii", "%", "--trace-time"]) 37 | sys.stderr.write("starting: {0}\n".format(self.args)) 38 | self.proc = subprocess.Popen(self.args, stdin=subprocess.PIPE, 39 | stdout=subprocess.PIPE, 40 | stderr=subprocess.PIPE, 41 | bufsize=0) 42 | 43 | def read_output(fh, buffer): 44 | while True: 45 | chunk = fh.read() 46 | if not chunk: 47 | break 48 | buffer.append(chunk.decode()) 49 | 50 | # collect all stdout and stderr until we are done 51 | # use separate threads to not block ourself 52 | self._stderr = [] 53 | self._stdout = [] 54 | if self.proc.stderr: 55 | self.stderr_thread = Thread(target=read_output, args=(self.proc.stderr, self._stderr)) 56 | self.stderr_thread.start() 57 | if self.proc.stdout: 58 | self.stdout_thread = Thread(target=read_output, args=(self.proc.stdout, self._stdout)) 59 | self.stdout_thread.start() 60 | return self.proc 61 | 62 | def send(self, data: str): 63 | self.proc.stdin.write(data.encode()) 64 | self.proc.stdin.flush() 65 | 66 | def close(self) -> ([str], [str]): 67 | self.proc.stdin.close() 68 | self.stdout_thread.join() 69 | self.stderr_thread.join() 70 | self._end() 71 | return self._stdout, self._stderr 72 | 73 | def _end(self): 74 | if self.proc: 75 | # noinspection PyBroadException 76 | try: 77 | if self.proc.stdin: 78 | # noinspection PyBroadException 79 | try: 80 | self.proc.stdin.close() 81 | except Exception: 82 | pass 83 | if self.proc.stdout: 84 | self.proc.stdout.close() 85 | if self.proc.stderr: 86 | self.proc.stderr.close() 87 | except Exception: 88 | self.proc.terminate() 89 | finally: 90 | self.proc.wait() 91 | self.stdout_thread = None 92 | self.stderr_thread = None 93 | self._exitcode = self.proc.returncode 94 | self.proc = None 95 | self._r = self.env.curl_parse_headerfile(self.headerfile) 96 | 97 | def stutter_check(self, chunks: [str], stutter: datetime.timedelta): 98 | if not self.proc: 99 | self.start() 100 | for chunk in chunks: 101 | self.send(chunk) 102 | time.sleep(stutter.total_seconds()) 103 | recv_out, recv_err = self.close() 104 | # assert we got everything back 105 | assert "".join(chunks) == "".join(recv_out) 106 | # now the tricky part: check *when* we got everything back 107 | recv_times = [] 108 | for line in "".join(recv_err).split('\n'): 109 | m = re.match(r'^\s*(\d+:\d+:\d+(\.\d+)?) <= Recv data, (\d+) bytes.*', line) 110 | if m: 111 | recv_times.append(datetime.time.fromisoformat(m.group(1))) 112 | # received as many chunks as we sent 113 | assert len(chunks) == len(recv_times), "received response not in {0} chunks, but {1}".format( 114 | len(chunks), len(recv_times)) 115 | 116 | def microsecs(tdelta): 117 | return ((tdelta.hour * 60 + tdelta.minute) * 60 + tdelta.second) * 1000000 + tdelta.microsecond 118 | 119 | recv_deltas = [] 120 | last_mics = microsecs(recv_times[0]) 121 | for ts in recv_times[1:]: 122 | mics = microsecs(ts) 123 | delta_mics = mics - last_mics 124 | if delta_mics < 0: 125 | delta_mics += datetime.time(23, 59, 59, 999999) 126 | recv_deltas.append(datetime.timedelta(microseconds=delta_mics)) 127 | last_mics = mics 128 | stutter_td = datetime.timedelta(seconds=stutter.total_seconds() * 0.9) # 10% leeway 129 | # TODO: the first two chunks are often close together, it seems 130 | # there still is a little buffering delay going on 131 | for idx, td in enumerate(recv_deltas[1:]): 132 | assert stutter_td < td, \ 133 | f"chunk {idx} arrived too early \n{recv_deltas}\nafter {td}\n{recv_err}" 134 | -------------------------------------------------------------------------------- /test/modules/http2/test_105_timeout.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | 4 | import pytest 5 | 6 | from .env import H2Conf 7 | from pyhttpd.curl import CurlPiper 8 | 9 | 10 | class TestTimeout: 11 | 12 | # Check that base servers 'Timeout' setting is observed on SSL handshake 13 | def test_h2_105_01(self, env): 14 | conf = H2Conf(env) 15 | conf.add(""" 16 | AcceptFilter http none 17 | Timeout 1.5 18 | """) 19 | conf.add_vhost_cgi() 20 | conf.install() 21 | assert env.apache_restart() == 0 22 | host = 'localhost' 23 | # read with a longer timeout than the server 24 | sock = socket.create_connection((host, int(env.https_port))) 25 | try: 26 | # on some OS, the server does not see our connection until there is 27 | # something incoming 28 | sock.send(b'0') 29 | sock.settimeout(4) 30 | buff = sock.recv(1024) 31 | assert buff == b'' 32 | except Exception as ex: 33 | print(f"server did not close in time: {ex}") 34 | assert False 35 | sock.close() 36 | # read with a shorter timeout than the server 37 | sock = socket.create_connection((host, int(env.https_port))) 38 | try: 39 | sock.settimeout(0.5) 40 | sock.recv(1024) 41 | assert False 42 | except Exception as ex: 43 | print(f"as expected: {ex}") 44 | sock.close() 45 | 46 | # Check that mod_reqtimeout handshake setting takes effect 47 | def test_h2_105_02(self, env): 48 | conf = H2Conf(env) 49 | conf.add(""" 50 | AcceptFilter http none 51 | Timeout 10 52 | RequestReadTimeout handshake=1 header=5 body=10 53 | """) 54 | conf.add_vhost_cgi() 55 | conf.install() 56 | assert env.apache_restart() == 0 57 | host = 'localhost' 58 | # read with a longer timeout than the server 59 | sock = socket.create_connection((host, int(env.https_port))) 60 | try: 61 | # on some OS, the server does not see our connection until there is 62 | # something incoming 63 | sock.send(b'0') 64 | sock.settimeout(4) 65 | buff = sock.recv(1024) 66 | assert buff == b'' 67 | except Exception as ex: 68 | print(f"server did not close in time: {ex}") 69 | assert False 70 | sock.close() 71 | # read with a shorter timeout than the server 72 | sock = socket.create_connection((host, int(env.https_port))) 73 | try: 74 | sock.settimeout(0.5) 75 | sock.recv(1024) 76 | assert False 77 | except Exception as ex: 78 | print(f"as expected: {ex}") 79 | sock.close() 80 | 81 | # Check that mod_reqtimeout handshake setting do no longer apply to handshaked 82 | # connections. See . 83 | def test_h2_105_03(self, env): 84 | conf = H2Conf(env) 85 | conf.add(""" 86 | Timeout 10 87 | RequestReadTimeout handshake=1 header=5 body=10 88 | """) 89 | conf.add_vhost_cgi() 90 | conf.install() 91 | assert env.apache_restart() == 0 92 | url = env.mkurl("https", "cgi", "/necho.py") 93 | r = env.curl_get(url, 5, options=[ 94 | "-vvv", 95 | "-F", ("count=%d" % 100), 96 | "-F", ("text=%s" % "abcdefghijklmnopqrstuvwxyz"), 97 | "-F", ("wait1=%f" % 1.5), 98 | ]) 99 | assert r.response["status"] == 200 100 | 101 | def test_h2_105_10(self, env): 102 | # just a check without delays if all is fine 103 | conf = H2Conf(env) 104 | conf.add_vhost_cgi() 105 | conf.install() 106 | assert env.apache_restart() == 0 107 | url = env.mkurl("https", "cgi", "/h2test/delay") 108 | piper = CurlPiper(env=env, url=url) 109 | piper.start() 110 | stdout, stderr = piper.close() 111 | assert piper.exitcode == 0 112 | assert len("".join(stdout)) == 3 * 8192 113 | 114 | def test_h2_105_11(self, env): 115 | # short connection timeout, longer stream delay 116 | # receiving the first response chunk, then timeout 117 | conf = H2Conf(env) 118 | conf.add_vhost_cgi() 119 | conf.add("Timeout 1") 120 | conf.install() 121 | assert env.apache_restart() == 0 122 | url = env.mkurl("https", "cgi", "/h2test/delay?5") 123 | piper = CurlPiper(env=env, url=url) 124 | piper.start() 125 | stdout, stderr = piper.close() 126 | assert len("".join(stdout)) == 8192 127 | 128 | def test_h2_105_12(self, env): 129 | # long connection timeout, short stream timeout 130 | # sending a slow POST 131 | conf = H2Conf(env) 132 | conf.add_vhost_cgi() 133 | conf.add("Timeout 10") 134 | conf.add("H2StreamTimeout 1") 135 | conf.install() 136 | assert env.apache_restart() == 0 137 | url = env.mkurl("https", "cgi", "/h2test/delay?5") 138 | piper = CurlPiper(env=env, url=url) 139 | piper.start() 140 | for _ in range(3): 141 | time.sleep(2) 142 | try: 143 | piper.send("0123456789\n") 144 | except BrokenPipeError: 145 | break 146 | piper.close() 147 | assert piper.response 148 | assert piper.response['status'] == 408 149 | -------------------------------------------------------------------------------- /test/modules/http2/test_500_proxy.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | import re 4 | import pytest 5 | 6 | from .env import H2Conf 7 | 8 | 9 | class TestProxy: 10 | 11 | @pytest.fixture(autouse=True, scope='class') 12 | def _class_scope(self, env): 13 | TestProxy._local_dir = os.path.dirname(inspect.getfile(TestProxy)) 14 | H2Conf(env).add_vhost_cgi(proxy_self=True).install() 15 | assert env.apache_restart() == 0 16 | 17 | def local_src(self, fname): 18 | return os.path.join(TestProxy._local_dir, fname) 19 | 20 | def setup_method(self, method): 21 | print("setup_method: %s" % method.__name__) 22 | 23 | def teardown_method(self, method): 24 | print("teardown_method: %s" % method.__name__) 25 | 26 | def test_h2_500_01(self, env): 27 | url = env.mkurl("https", "cgi", "/proxy/hello.py") 28 | r = env.curl_get(url, 5) 29 | assert r.response["status"] == 200 30 | assert "HTTP/1.1" == r.response["json"]["protocol"] 31 | assert "" == r.response["json"]["https"] 32 | assert "" == r.response["json"]["ssl_protocol"] 33 | assert "" == r.response["json"]["h2"] 34 | assert "" == r.response["json"]["h2push"] 35 | 36 | # upload and GET again using curl, compare to original content 37 | def curl_upload_and_verify(self, env, fname, options=None): 38 | url = env.mkurl("https", "cgi", "/proxy/upload.py") 39 | fpath = os.path.join(env.gen_dir, fname) 40 | r = env.curl_upload(url, fpath, options=options) 41 | assert r.exit_code == 0 42 | assert 200 <= r.response["status"] < 300 43 | 44 | # why is the scheme wrong? 45 | r2 = env.curl_get(re.sub(r'http:', 'https:', r.response["header"]["location"])) 46 | assert r2.exit_code == 0 47 | assert r2.response["status"] == 200 48 | with open(self.local_src(fpath), mode='rb') as file: 49 | src = file.read() 50 | assert src == r2.response["body"] 51 | 52 | def test_h2_500_10(self, env): 53 | self.curl_upload_and_verify(env, "data-1k", ["--http2"]) 54 | self.curl_upload_and_verify(env, "data-10k", ["--http2"]) 55 | self.curl_upload_and_verify(env, "data-100k", ["--http2"]) 56 | self.curl_upload_and_verify(env, "data-1m", ["--http2"]) 57 | 58 | # POST some data using nghttp and see it echo'ed properly back 59 | def nghttp_post_and_verify(self, env, fname, options=None): 60 | url = env.mkurl("https", "cgi", "/proxy/echo.py") 61 | fpath = os.path.join(env.gen_dir, fname) 62 | r = env.nghttp().upload(url, fpath, options=options) 63 | assert r.exit_code == 0 64 | assert 200 <= r.response["status"] < 300 65 | with open(self.local_src(fpath), mode='rb') as file: 66 | src = file.read() 67 | assert src == r.response["body"] 68 | 69 | def test_h2_500_20(self, env): 70 | self.nghttp_post_and_verify(env, "data-1k", []) 71 | self.nghttp_post_and_verify(env, "data-10k", []) 72 | self.nghttp_post_and_verify(env, "data-100k", []) 73 | self.nghttp_post_and_verify(env, "data-1m", []) 74 | 75 | def test_h2_500_21(self, env): 76 | self.nghttp_post_and_verify(env, "data-1k", ["--no-content-length"]) 77 | self.nghttp_post_and_verify(env, "data-10k", ["--no-content-length"]) 78 | self.nghttp_post_and_verify(env, "data-100k", ["--no-content-length"]) 79 | self.nghttp_post_and_verify(env, "data-1m", ["--no-content-length"]) 80 | 81 | # upload and GET again using nghttp, compare to original content 82 | def nghttp_upload_and_verify(self, env, fname, options=None): 83 | url = env.mkurl("https", "cgi", "/proxy/upload.py") 84 | fpath = os.path.join(env.gen_dir, fname) 85 | 86 | r = env.nghttp().upload_file(url, fpath, options=options) 87 | assert r.exit_code == 0 88 | assert 200 <= r.response["status"] < 300 89 | assert r.response["header"]["location"] 90 | 91 | # why is the scheme wrong? 92 | r2 = env.nghttp().get(re.sub(r'http:', 'https:', r.response["header"]["location"])) 93 | assert r2.exit_code == 0 94 | assert r2.response["status"] == 200 95 | with open(self.local_src(fpath), mode='rb') as file: 96 | src = file.read() 97 | assert src == r2.response["body"] 98 | 99 | def test_h2_500_22(self, env): 100 | self.nghttp_upload_and_verify(env, "data-1k", []) 101 | self.nghttp_upload_and_verify(env, "data-10k", []) 102 | self.nghttp_upload_and_verify(env, "data-100k", []) 103 | self.nghttp_upload_and_verify(env, "data-1m", []) 104 | 105 | def test_h2_500_23(self, env): 106 | self.nghttp_upload_and_verify(env, "data-1k", ["--no-content-length"]) 107 | self.nghttp_upload_and_verify(env, "data-10k", ["--no-content-length"]) 108 | self.nghttp_upload_and_verify(env, "data-100k", ["--no-content-length"]) 109 | self.nghttp_upload_and_verify(env, "data-1m", ["--no-content-length"]) 110 | 111 | # upload using nghttp and check returned status 112 | def nghttp_upload_stat(self, env, fname, options=None): 113 | url = env.mkurl("https", "cgi", "/proxy/upload.py") 114 | fpath = os.path.join(env.gen_dir, fname) 115 | 116 | r = env.nghttp().upload_file(url, fpath, options=options) 117 | assert r.exit_code == 0 118 | assert 200 <= r.response["status"] < 300 119 | assert r.response["header"]["location"] 120 | 121 | def test_h2_500_24(self, env): 122 | for i in range(100): 123 | self.nghttp_upload_stat(env, "data-1k", ["--no-content-length"]) 124 | -------------------------------------------------------------------------------- /mod_http2/h2_proxy_session.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef h2_proxy_session_h 18 | #define h2_proxy_session_h 19 | 20 | #define H2_ALEN(a) (sizeof(a)/sizeof((a)[0])) 21 | 22 | #include 23 | 24 | struct h2_proxy_iqueue; 25 | struct h2_proxy_ihash_t; 26 | 27 | typedef enum { 28 | H2_STREAM_ST_IDLE, 29 | H2_STREAM_ST_OPEN, 30 | H2_STREAM_ST_RESV_LOCAL, 31 | H2_STREAM_ST_RESV_REMOTE, 32 | H2_STREAM_ST_CLOSED_INPUT, 33 | H2_STREAM_ST_CLOSED_OUTPUT, 34 | H2_STREAM_ST_CLOSED, 35 | } h2_proxy_stream_state_t; 36 | 37 | typedef enum { 38 | H2_PROXYS_ST_INIT, /* send initial SETTINGS, etc. */ 39 | H2_PROXYS_ST_DONE, /* finished, connection close */ 40 | H2_PROXYS_ST_IDLE, /* no streams to process */ 41 | H2_PROXYS_ST_BUSY, /* read/write without stop */ 42 | H2_PROXYS_ST_WAIT, /* waiting for tasks reporting back */ 43 | H2_PROXYS_ST_LOCAL_SHUTDOWN, /* we announced GOAWAY */ 44 | H2_PROXYS_ST_REMOTE_SHUTDOWN, /* client announced GOAWAY */ 45 | } h2_proxys_state; 46 | 47 | typedef enum { 48 | H2_PROXYS_EV_INIT, /* session was initialized */ 49 | H2_PROXYS_EV_LOCAL_GOAWAY, /* we send a GOAWAY */ 50 | H2_PROXYS_EV_REMOTE_GOAWAY, /* remote send us a GOAWAY */ 51 | H2_PROXYS_EV_CONN_ERROR, /* connection error */ 52 | H2_PROXYS_EV_PROTO_ERROR, /* protocol error */ 53 | H2_PROXYS_EV_CONN_TIMEOUT, /* connection timeout */ 54 | H2_PROXYS_EV_NO_IO, /* nothing has been read or written */ 55 | H2_PROXYS_EV_STREAM_SUBMITTED, /* stream has been submitted */ 56 | H2_PROXYS_EV_STREAM_DONE, /* stream has been finished */ 57 | H2_PROXYS_EV_STREAM_RESUMED, /* stream signalled availability of headers/data */ 58 | H2_PROXYS_EV_DATA_READ, /* connection data has been read */ 59 | H2_PROXYS_EV_NGH2_DONE, /* nghttp2 wants neither read nor write anything */ 60 | H2_PROXYS_EV_PRE_CLOSE, /* connection will close after this */ 61 | } h2_proxys_event_t; 62 | 63 | typedef enum { 64 | H2_PING_ST_NONE, /* normal connection mode, ProxyTimeout rules */ 65 | H2_PING_ST_AWAIT_ANY, /* waiting for any frame from backend */ 66 | H2_PING_ST_AWAIT_PING, /* waiting for PING frame from backend */ 67 | } h2_ping_state_t; 68 | 69 | typedef struct h2_proxy_session h2_proxy_session; 70 | typedef void h2_proxy_request_done(h2_proxy_session *s, request_rec *r, 71 | apr_status_t status, int touched); 72 | 73 | struct h2_proxy_session { 74 | const char *id; 75 | conn_rec *c; 76 | proxy_conn_rec *p_conn; 77 | proxy_server_conf *conf; 78 | apr_pool_t *pool; 79 | nghttp2_session *ngh2; /* the nghttp2 session itself */ 80 | 81 | unsigned int aborted : 1; 82 | unsigned int h2_front : 1; /* if front-end connection is HTTP/2 */ 83 | 84 | h2_proxy_request_done *done; 85 | void *user_data; 86 | 87 | unsigned char window_bits_stream; 88 | unsigned char window_bits_connection; 89 | 90 | h2_proxys_state state; 91 | apr_interval_time_t wait_timeout; 92 | 93 | struct h2_proxy_ihash_t *streams; 94 | struct h2_proxy_iqueue *suspended; 95 | apr_size_t remote_max_concurrent; 96 | int last_stream_id; /* last stream id processed by backend, or 0 */ 97 | apr_time_t last_frame_received; 98 | 99 | apr_bucket_brigade *input; 100 | apr_bucket_brigade *output; 101 | 102 | h2_ping_state_t ping_state; 103 | apr_time_t ping_timeout; 104 | apr_time_t save_timeout; 105 | }; 106 | 107 | h2_proxy_session *h2_proxy_session_setup(const char *id, proxy_conn_rec *p_conn, 108 | proxy_server_conf *conf, 109 | int h2_front, 110 | unsigned char window_bits_connection, 111 | unsigned char window_bits_stream, 112 | h2_proxy_request_done *done); 113 | 114 | apr_status_t h2_proxy_session_submit(h2_proxy_session *s, const char *url, 115 | request_rec *r, int standalone); 116 | 117 | /** 118 | * Perform a step in processing the proxy session. Will return aftert 119 | * one read/write cycle and indicate session status by status code. 120 | * @param s the session to process 121 | * @return APR_EAGAIN when processing needs to be invoked again 122 | * APR_SUCCESS when all streams have been processed, session still live 123 | * APR_EOF when the session has been terminated 124 | */ 125 | apr_status_t h2_proxy_session_process(h2_proxy_session *s); 126 | 127 | void h2_proxy_session_cancel_all(h2_proxy_session *s); 128 | 129 | void h2_proxy_session_cleanup(h2_proxy_session *s, h2_proxy_request_done *done); 130 | 131 | #define H2_PROXY_REQ_URL_NOTE "h2-proxy-req-url" 132 | 133 | #endif /* h2_proxy_session_h */ 134 | -------------------------------------------------------------------------------- /test/modules/http2/env.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | import os 4 | import re 5 | import subprocess 6 | from typing import Dict, Any 7 | 8 | from pyhttpd.certs import CertificateSpec 9 | from pyhttpd.conf import HttpdConf 10 | from pyhttpd.env import HttpdTestEnv, HttpdTestSetup 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class H2TestSetup(HttpdTestSetup): 16 | 17 | def __init__(self, env: 'HttpdTestEnv'): 18 | super().__init__(env=env) 19 | self.add_source_dir(os.path.dirname(inspect.getfile(H2TestSetup))) 20 | self.add_modules(["http2", "proxy_http2", "cgid", "autoindex"]) 21 | 22 | def make(self): 23 | super().make() 24 | self._add_h2test() 25 | self._setup_data_1k_1m() 26 | 27 | def _add_h2test(self): 28 | local_dir = os.path.dirname(inspect.getfile(H2TestSetup)) 29 | p = subprocess.run([self.env.apxs, '-c', 'mod_h2test.c'], 30 | capture_output=True, 31 | cwd=os.path.join(local_dir, 'mod_h2test')) 32 | rv = p.returncode 33 | if rv != 0: 34 | log.error(f"compiling md_h2test failed: {p.stderr}") 35 | raise Exception(f"compiling md_h2test failed: {p.stderr}") 36 | 37 | modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf') 38 | with open(modules_conf, 'a') as fd: 39 | # load our test module which is not installed 40 | fd.write(f"LoadModule h2test_module \"{local_dir}/mod_h2test/.libs/mod_h2test.so\"\n") 41 | 42 | def _setup_data_1k_1m(self): 43 | s90 = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n" 44 | with open(os.path.join(self.env.gen_dir, "data-1k"), 'w') as f: 45 | for i in range(10): 46 | f.write(f"{i:09d}-{s90}") 47 | with open(os.path.join(self.env.gen_dir, "data-10k"), 'w') as f: 48 | for i in range(100): 49 | f.write(f"{i:09d}-{s90}") 50 | with open(os.path.join(self.env.gen_dir, "data-100k"), 'w') as f: 51 | for i in range(1000): 52 | f.write(f"{i:09d}-{s90}") 53 | with open(os.path.join(self.env.gen_dir, "data-1m"), 'w') as f: 54 | for i in range(10000): 55 | f.write(f"{i:09d}-{s90}") 56 | 57 | 58 | class H2TestEnv(HttpdTestEnv): 59 | 60 | def __init__(self, pytestconfig=None): 61 | super().__init__(pytestconfig=pytestconfig) 62 | self.add_httpd_conf([ 63 | "H2MinWorkers 1", 64 | "H2MaxWorkers 64", 65 | "Protocols h2 http/1.1 h2c", 66 | ]) 67 | self.add_httpd_log_modules(["http2", "proxy_http2", "h2test"]) 68 | self.add_cert_specs([ 69 | CertificateSpec(domains=[ 70 | f"push.{self._http_tld}", 71 | f"hints.{self._http_tld}", 72 | f"ssl.{self._http_tld}", 73 | f"pad0.{self._http_tld}", 74 | f"pad1.{self._http_tld}", 75 | f"pad2.{self._http_tld}", 76 | f"pad3.{self._http_tld}", 77 | f"pad8.{self._http_tld}", 78 | ]), 79 | CertificateSpec(domains=[f"noh2.{self.http_tld}"], key_type='rsa2048'), 80 | ]) 81 | 82 | self.httpd_error_log.set_ignored_lognos([ 83 | 'AH02032', 84 | 'AH01276', 85 | 'AH01630', 86 | 'AH00135', 87 | 'AH02261', # Re-negotiation handshake failed (our test_101) 88 | 'AH03490', # scoreboard full, happens on limit tests 89 | ]) 90 | self.httpd_error_log.add_ignored_patterns([ 91 | re.compile(r'.*malformed header from script \'hecho.py\': Bad header: x.*'), 92 | re.compile(r'.*:tls_post_process_client_hello:.*'), 93 | re.compile(r'.*:tls_process_client_certificate:.*'), 94 | ]) 95 | 96 | def setup_httpd(self, setup: HttpdTestSetup = None): 97 | super().setup_httpd(setup=H2TestSetup(env=self)) 98 | 99 | 100 | class H2Conf(HttpdConf): 101 | 102 | def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None): 103 | super().__init__(env=env, extras=HttpdConf.merge_extras(extras, { 104 | f"cgi.{env.http_tld}": [ 105 | "SSLOptions +StdEnvVars", 106 | "AddHandler cgi-script .py", 107 | ] 108 | })) 109 | 110 | def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=None): 111 | super().start_vhost(domains=domains, port=port, doc_root=doc_root, with_ssl=with_ssl) 112 | if f"noh2.{self.env.http_tld}" in domains: 113 | protos = ["http/1.1"] 114 | elif port == self.env.https_port or with_ssl is True: 115 | protos = ["h2", "http/1.1"] 116 | else: 117 | protos = ["h2c", "http/1.1"] 118 | if f"test2.{self.env.http_tld}" in domains: 119 | protos = reversed(protos) 120 | self.add(f"Protocols {' '.join(protos)}") 121 | return self 122 | 123 | def add_vhost_noh2(self): 124 | domains = [f"noh2.{self.env.http_tld}", f"noh2-alias.{self.env.http_tld}"] 125 | self.start_vhost(domains=domains, port=self.env.https_port, doc_root="htdocs/noh2") 126 | self.add(["Protocols http/1.1", "SSLOptions +StdEnvVars"]) 127 | self.end_vhost() 128 | self.start_vhost(domains=domains, port=self.env.http_port, doc_root="htdocs/noh2") 129 | self.add(["Protocols http/1.1", "SSLOptions +StdEnvVars"]) 130 | self.end_vhost() 131 | return self 132 | 133 | def add_vhost_test1(self, proxy_self=False, h2proxy_self=False): 134 | return super().add_vhost_test1(proxy_self=proxy_self, h2proxy_self=h2proxy_self) 135 | 136 | def add_vhost_test2(self): 137 | return super().add_vhost_test2() 138 | -------------------------------------------------------------------------------- /test/modules/http2/test_101_ssl_reneg.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | 4 | from .env import H2Conf 5 | 6 | 7 | class TestSslRenegotiation: 8 | 9 | @pytest.fixture(autouse=True, scope='class') 10 | def _class_scope(self, env): 11 | domain = f"ssl.{env.http_tld}" 12 | conf = H2Conf(env, extras={ 13 | 'base': [ 14 | "SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384", 15 | f"", 16 | " Require all granted", 17 | " SSLVerifyClient require", 18 | " SSLVerifyDepth 0", 19 | "" 20 | ], 21 | domain: [ 22 | "Protocols h2 http/1.1", 23 | "", 24 | " SSLCipherSuite ECDHE-RSA-CHACHA20-POLY1305", 25 | "", 26 | "", 27 | " SSLCipherSuite ECDHE-RSA-CHACHA20-POLY1305", 28 | " ErrorDocument 403 /forbidden.html", 29 | "", 30 | "", 31 | " SSLVerifyClient require", 32 | "", 33 | f"", 34 | " SSLRequireSSL", 35 | "", 36 | f"", 37 | " Require ssl", 38 | "", 39 | ]}) 40 | conf.add_vhost(domains=[domain], port=env.https_port, 41 | doc_root=f"{env.server_dir}/htdocs") 42 | conf.install() 43 | # the dir needs to exists for the configuration to have effect 44 | env.mkpath("%s/htdocs/ssl-client-verify" % env.server_dir) 45 | env.mkpath("%s/htdocs/renegotiate/cipher" % env.server_dir) 46 | env.mkpath("%s/htdocs/sslrequire" % env.server_dir) 47 | env.mkpath("%s/htdocs/requiressl" % env.server_dir) 48 | assert env.apache_restart() == 0 49 | 50 | # access a resource with SSL renegotiation, using HTTP/1.1 51 | def test_h2_101_01(self, env): 52 | url = env.mkurl("https", "ssl", "/renegotiate/cipher/") 53 | r = env.curl_get(url, options=["-v", "--http1.1", "--tlsv1.2", "--tls-max", "1.2"]) 54 | assert 0 == r.exit_code, f"{r}" 55 | assert r.response 56 | assert 403 == r.response["status"] 57 | 58 | # try to renegotiate the cipher, should fail with correct code 59 | def test_h2_101_02(self, env): 60 | url = env.mkurl("https", "ssl", "/renegotiate/cipher/") 61 | r = env.curl_get(url, options=[ 62 | "-vvv", "--tlsv1.2", "--tls-max", "1.2", "--ciphers", "ECDHE-RSA-AES256-GCM-SHA384" 63 | ]) 64 | assert 0 != r.exit_code 65 | assert not r.response 66 | assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr) 67 | 68 | # try to renegotiate a client certificate from Location 69 | # needs to fail with correct code 70 | def test_h2_101_03(self, env): 71 | url = env.mkurl("https", "ssl", "/renegotiate/verify/") 72 | r = env.curl_get(url, options=["-vvv", "--tlsv1.2", "--tls-max", "1.2"]) 73 | assert 0 != r.exit_code 74 | assert not r.response 75 | assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr) 76 | 77 | # try to renegotiate a client certificate from Directory 78 | # needs to fail with correct code 79 | def test_h2_101_04(self, env): 80 | url = env.mkurl("https", "ssl", "/ssl-client-verify/index.html") 81 | r = env.curl_get(url, options=["-vvv", "--tlsv1.2", "--tls-max", "1.2"]) 82 | assert 0 != r.exit_code, f"{r}" 83 | assert not r.response 84 | assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr) 85 | 86 | # make 10 requests on the same connection, none should produce a status code 87 | # reported by erki@example.ee 88 | def test_h2_101_05(self, env): 89 | r = env.run([env.h2load, "-n", "10", "-c", "1", "-m", "1", "-vvvv", 90 | f"{env.https_base_url}/ssl-client-verify/index.html"]) 91 | assert 0 == r.exit_code 92 | r = env.h2load_status(r) 93 | assert 10 == r.results["h2load"]["requests"]["total"] 94 | assert 10 == r.results["h2load"]["requests"]["started"] 95 | assert 10 == r.results["h2load"]["requests"]["done"] 96 | assert 0 == r.results["h2load"]["requests"]["succeeded"] 97 | assert 0 == r.results["h2load"]["status"]["2xx"] 98 | assert 0 == r.results["h2load"]["status"]["3xx"] 99 | assert 0 == r.results["h2load"]["status"]["4xx"] 100 | assert 0 == r.results["h2load"]["status"]["5xx"] 101 | 102 | # Check that "SSLRequireSSL" works on h2 connections 103 | # See 104 | def test_h2_101_10a(self, env): 105 | url = env.mkurl("https", "ssl", "/sslrequire/index.html") 106 | r = env.curl_get(url) 107 | assert 0 == r.exit_code 108 | assert r.response 109 | assert 404 == r.response["status"] 110 | 111 | # Check that "require ssl" works on h2 connections 112 | # See 113 | def test_h2_101_10b(self, env): 114 | url = env.mkurl("https", "ssl", "/requiressl/index.html") 115 | r = env.curl_get(url) 116 | assert 0 == r.exit_code 117 | assert r.response 118 | assert 404 == r.response["status"] 119 | 120 | # Check that status works with ErrorDoc, see pull #174, fixes #172 121 | def test_h2_101_11(self, env): 122 | url = env.mkurl("https", "ssl", "/renegotiate/err-doc-cipher") 123 | r = env.curl_get(url, options=[ 124 | "-vvv", "--tlsv1.2", "--tls-max", "1.2", "--ciphers", "ECDHE-RSA-AES256-GCM-SHA384" 125 | ]) 126 | assert 0 != r.exit_code 127 | assert not r.response 128 | assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr) 129 | -------------------------------------------------------------------------------- /mod_http2/h2_push.h: -------------------------------------------------------------------------------- 1 | /* Licensed to the Apache Software Foundation (ASF) under one or more 2 | * contributor license agreements. See the NOTICE file distributed with 3 | * this work for additional information regarding copyright ownership. 4 | * The ASF licenses this file to You under the Apache License, Version 2.0 5 | * (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef __mod_h2__h2_push__ 18 | #define __mod_h2__h2_push__ 19 | 20 | #include "h2.h" 21 | 22 | struct h2_request; 23 | struct h2_headers; 24 | struct h2_ngheader; 25 | struct h2_session; 26 | struct h2_stream; 27 | 28 | typedef struct h2_push { 29 | const struct h2_request *req; 30 | h2_priority *priority; 31 | } h2_push; 32 | 33 | typedef enum { 34 | H2_PUSH_DIGEST_APR_HASH, 35 | H2_PUSH_DIGEST_SHA256 36 | } h2_push_digest_type; 37 | 38 | /******************************************************************************* 39 | * push diary 40 | * 41 | * - The push diary keeps track of resources already PUSHed via HTTP/2 on this 42 | * connection. It records a hash value from the absolute URL of the resource 43 | * pushed. 44 | * - Lacking openssl, 45 | * - with openssl, it uses SHA256 to calculate the hash value, otherwise it 46 | * falls back to apr_hashfunc_default() 47 | * - whatever the method to generate the hash, the diary keeps a maximum of 64 48 | * bits per hash, limiting the memory consumption to about 49 | * H2PushDiarySize * 8 50 | * bytes. Entries are sorted by most recently used and oldest entries are 51 | * forgotten first. 52 | * - While useful by itself to avoid duplicated PUSHes on the same connection, 53 | * the original idea was that clients provided a 'Cache-Digest' header with 54 | * the values of *their own* cached resources. This was described in 55 | * 56 | * and some subsequent revisions that tweaked values but kept the overall idea. 57 | * - The draft was abandoned by the IETF http-wg, as support from major clients, 58 | * e.g. browsers, was lacking for various reasons. 59 | * - For these reasons, mod_h2 abandoned its support for client supplied values 60 | * but keeps the diary. It seems to provide value for applications using PUSH, 61 | * is configurable in size and defaults to a very moderate amount of memory 62 | * used. 63 | * - The cache digest header is a Golomb Coded Set of hash values, but it may 64 | * limit the amount of bits per hash value even further. For a good description 65 | * of GCS, read here: 66 | * 67 | ******************************************************************************/ 68 | 69 | 70 | /* 71 | * The push diary is based on the abandoned draft 72 | * 73 | * that describes how to use golomb filters. 74 | */ 75 | 76 | typedef struct h2_push_diary h2_push_diary; 77 | 78 | typedef void h2_push_digest_calc(h2_push_diary *diary, apr_uint64_t *phash, h2_push *push); 79 | 80 | struct h2_push_diary { 81 | apr_array_header_t *entries; 82 | int NMax; /* Maximum for N, should size change be necessary */ 83 | int N; /* Current maximum number of entries, power of 2 */ 84 | apr_uint64_t mask; /* mask for relevant bits */ 85 | unsigned int mask_bits; /* number of relevant bits */ 86 | const char *authority; 87 | h2_push_digest_type dtype; 88 | h2_push_digest_calc *dcalc; 89 | }; 90 | 91 | /** 92 | * Determine the list of h2_push'es to send to the client on behalf of 93 | * the given request/response pair. 94 | * 95 | * @param p the pool to use 96 | * @param req the requst from the client 97 | * @param res the response from the server 98 | * @return array of h2_push addresses or NULL 99 | */ 100 | apr_array_header_t *h2_push_collect(apr_pool_t *p, 101 | const struct h2_request *req, 102 | apr_uint32_t push_policy, 103 | const struct h2_headers *res); 104 | 105 | /** 106 | * Create a new push diary for the given maximum number of entries. 107 | * 108 | * @param p the pool to use 109 | * @param N the max number of entries, rounded up to 2^x 110 | * @return the created diary, might be NULL of max_entries is 0 111 | */ 112 | h2_push_diary *h2_push_diary_create(apr_pool_t *p, int N); 113 | 114 | /** 115 | * Filters the given pushes against the diary and returns only those pushes 116 | * that were newly entered in the diary. 117 | */ 118 | apr_array_header_t *h2_push_diary_update(struct h2_session *session, apr_array_header_t *pushes); 119 | 120 | /** 121 | * Collect pushes for the given request/response pair, enter them into the 122 | * diary and return those pushes newly entered. 123 | */ 124 | apr_array_header_t *h2_push_collect_update(struct h2_stream *stream, 125 | const struct h2_request *req, 126 | const struct h2_headers *res); 127 | /** 128 | * Get a cache digest as described in 129 | * https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/ 130 | * from the contents of the push diary. 131 | * 132 | * @param diary the diary to calculdate the digest from 133 | * @param p the pool to use 134 | * @param authority the authority to get the data for, use NULL/"*" for all 135 | * @param pdata on successful return, the binary cache digest 136 | * @param plen on successful return, the length of the binary data 137 | */ 138 | apr_status_t h2_push_diary_digest_get(h2_push_diary *diary, apr_pool_t *p, 139 | int maxP, const char *authority, 140 | const char **pdata, apr_size_t *plen); 141 | 142 | #endif /* defined(__mod_h2__h2_push__) */ 143 | -------------------------------------------------------------------------------- /test/pyhttpd/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | from datetime import datetime, timedelta 5 | from io import SEEK_END 6 | from typing import List, Tuple, Any 7 | 8 | 9 | class HttpdErrorLog: 10 | """Checking the httpd error log for errors and warnings, including 11 | limiting checks from a last known position forward. 12 | """ 13 | 14 | RE_ERRLOG_ERROR = re.compile(r'.*\[(?P[^:]+):error].*') 15 | RE_ERRLOG_WARN = re.compile(r'.*\[(?P[^:]+):warn].*') 16 | RE_APLOGNO = re.compile(r'.*\[(?P[^:]+):(error|warn)].* (?PAH\d+): .+') 17 | RE_SSL_LIB_ERR = re.compile(r'.*\[ssl:error].* SSL Library Error: error:(?P\S+):.+') 18 | 19 | def __init__(self, path: str): 20 | self._path = path 21 | self._ignored_modules = [] 22 | self._ignored_lognos = set() 23 | self._ignored_patterns = [] 24 | # remember the file position we started with 25 | self._start_pos = 0 26 | if os.path.isfile(self._path): 27 | with open(self._path) as fd: 28 | self._start_pos = fd.seek(0, SEEK_END) 29 | self._last_pos = self._start_pos 30 | self._last_errors = [] 31 | self._last_warnings = [] 32 | self._observed_erros = set() 33 | self._observed_warnings = set() 34 | 35 | def __repr__(self): 36 | return f"HttpdErrorLog[{self._path}, errors: {' '.join(self._last_errors)}, " \ 37 | f"warnings: {' '.join(self._last_warnings)}]" 38 | 39 | @property 40 | def path(self) -> str: 41 | return self._path 42 | 43 | def clear_log(self): 44 | if os.path.isfile(self.path): 45 | os.remove(self.path) 46 | self._start_pos = 0 47 | self._last_pos = self._start_pos 48 | self._last_errors = [] 49 | self._last_warnings = [] 50 | self._observed_erros = set() 51 | self._observed_warnings = set() 52 | 53 | def set_ignored_modules(self, modules: List[str]): 54 | self._ignored_modules = modules.copy() if modules else [] 55 | 56 | def set_ignored_lognos(self, lognos: List[str]): 57 | if lognos: 58 | for l in lognos: 59 | self._ignored_lognos.add(l) 60 | 61 | def add_ignored_patterns(self, patterns: List[Any]): 62 | self._ignored_patterns.extend(patterns) 63 | 64 | def _is_ignored(self, line: str) -> bool: 65 | for p in self._ignored_patterns: 66 | if p.match(line): 67 | return True 68 | m = self.RE_APLOGNO.match(line) 69 | if m and m.group('aplogno') in self._ignored_lognos: 70 | return True 71 | return False 72 | 73 | def get_recent(self, advance=True) -> Tuple[List[str], List[str]]: 74 | """Collect error and warning from the log since the last remembered position 75 | :param advance: advance the position to the end of the log afterwards 76 | :return: list of error and list of warnings as tuple 77 | """ 78 | self._last_errors = [] 79 | self._last_warnings = [] 80 | if os.path.isfile(self._path): 81 | with open(self._path) as fd: 82 | fd.seek(self._last_pos, os.SEEK_SET) 83 | for line in fd: 84 | if self._is_ignored(line): 85 | continue 86 | m = self.RE_ERRLOG_ERROR.match(line) 87 | if m and m.group('module') not in self._ignored_modules: 88 | self._last_errors.append(line) 89 | continue 90 | m = self.RE_ERRLOG_WARN.match(line) 91 | if m: 92 | if m and m.group('module') not in self._ignored_modules: 93 | self._last_warnings.append(line) 94 | continue 95 | if advance: 96 | self._last_pos = fd.tell() 97 | self._observed_erros.update(set(self._last_errors)) 98 | self._observed_warnings.update(set(self._last_warnings)) 99 | return self._last_errors, self._last_warnings 100 | 101 | def get_recent_count(self, advance=True): 102 | errors, warnings = self.get_recent(advance=advance) 103 | return len(errors), len(warnings) 104 | 105 | def ignore_recent(self): 106 | """After a test case triggered errors/warnings on purpose, add 107 | those to our 'observed' list so the do not get reported as 'missed'. 108 | """ 109 | self._last_errors = [] 110 | self._last_warnings = [] 111 | if os.path.isfile(self._path): 112 | with open(self._path) as fd: 113 | fd.seek(self._last_pos, os.SEEK_SET) 114 | for line in fd: 115 | if self._is_ignored(line): 116 | continue 117 | m = self.RE_ERRLOG_ERROR.match(line) 118 | if m and m.group('module') not in self._ignored_modules: 119 | self._observed_erros.add(line) 120 | continue 121 | m = self.RE_ERRLOG_WARN.match(line) 122 | if m: 123 | if m and m.group('module') not in self._ignored_modules: 124 | self._observed_warnings.add(line) 125 | continue 126 | self._last_pos = fd.tell() 127 | 128 | def get_missed(self) -> Tuple[List[str], List[str]]: 129 | errors = [] 130 | warnings = [] 131 | if os.path.isfile(self._path): 132 | with open(self._path) as fd: 133 | fd.seek(self._start_pos, os.SEEK_SET) 134 | for line in fd: 135 | if self._is_ignored(line): 136 | continue 137 | m = self.RE_ERRLOG_ERROR.match(line) 138 | if m and m.group('module') not in self._ignored_modules \ 139 | and line not in self._observed_erros: 140 | errors.append(line) 141 | continue 142 | m = self.RE_ERRLOG_WARN.match(line) 143 | if m: 144 | if m and m.group('module') not in self._ignored_modules \ 145 | and line not in self._observed_warnings: 146 | warnings.append(line) 147 | continue 148 | return errors, warnings 149 | 150 | def scan_recent(self, pattern: re, timeout=10): 151 | if not os.path.isfile(self.path): 152 | return False 153 | with open(self.path) as fd: 154 | end = datetime.now() + timedelta(seconds=timeout) 155 | while True: 156 | fd.seek(self._last_pos, os.SEEK_SET) 157 | for line in fd: 158 | if pattern.match(line): 159 | return True 160 | if datetime.now() > end: 161 | raise TimeoutError(f"pattern not found in error log after {timeout} seconds") 162 | time.sleep(.1) 163 | return False 164 | --------------------------------------------------------------------------------