├── tests ├── html │ ├── br1 │ │ ├── test01.txt │ │ └── test02.html │ └── br2 │ │ ├── test03.txt │ │ ├── test04.html │ │ └── test05.htm ├── response │ ├── test05.out │ ├── test01.out │ ├── test02.out │ ├── test04.out │ └── test03.out ├── conf │ ├── mod.conf │ ├── test.2.4.conf │ └── test.2.2.conf └── test.sh ├── .gitmodules ├── .travis ├── install.sh ├── before_install.sh └── before_script.sh ├── .gitignore ├── .travis.yml ├── autogen.sh ├── Makefile.am ├── README.md ├── configure.ac ├── LICENSE └── mod_brotli.c /tests/html/br1/test01.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/br1/test02.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/br2/test03.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/br2/test04.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/html/br2/test05.htm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/response/test05.out: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/html 3 | -------------------------------------------------------------------------------- /tests/response/test01.out: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/plain 3 | -------------------------------------------------------------------------------- /tests/response/test02.out: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Encoding: br 3 | Content-Type: text/html 4 | -------------------------------------------------------------------------------- /tests/response/test04.out: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Encoding: br 3 | Content-Type: text/html 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "brotli"] 2 | path = brotli 3 | url = https://github.com/google/brotli.git 4 | -------------------------------------------------------------------------------- /tests/response/test03.out: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Encoding: br 3 | Content-Type: text/plain 4 | -------------------------------------------------------------------------------- /.travis/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo cp .libs/mod_brotli.so /usr/lib/apache2/modules/ 4 | sudo bash -c "cat ${TRAVIS_BUILD_DIR}/tests/conf/mod.conf | sed -e 's|modules/|/usr/lib/apache2/modules/|g' > /etc/apache2/mods-available/brotli.load" 5 | sudo a2enmod brotli 6 | -------------------------------------------------------------------------------- /tests/conf/mod.conf: -------------------------------------------------------------------------------- 1 | LoadModule brotli_module modules/mod_brotli.so 2 | 3 | # BrotliCompressionLevel 6 4 | # BrotliWindowSize 19 5 | # BrotliFilterNote Input brotli_in 6 | # BrotliFilterNote Output brotli_out 7 | # BrotliFilterNote Ratio brotli_ratio 8 | # LogFormat '"%r" %{brotli_out}n/%{brotli_in}n (%{brotli_ratio}n)' brotli 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .deps/ 2 | .libs/ 3 | Makefile 4 | Makefile.in 5 | aclocal.m4 6 | ar-lib 7 | autom4te.cache/ 8 | compile 9 | config.guess 10 | config.h* 11 | config.log 12 | config.status 13 | config.sub 14 | configure 15 | depcomp 16 | install-sh 17 | libtool 18 | ltmain.sh 19 | m4/ 20 | missing 21 | stamp-h1 22 | *.o 23 | *.lo 24 | *.la 25 | 26 | *.c_ 27 | *.cc_ 28 | mod_deflate.c 29 | -------------------------------------------------------------------------------- /tests/conf/test.2.4.conf: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot /var/www/html 3 | 4 | Options FollowSymLinks 5 | Require all granted 6 | 7 | 8 | 9 | AddOutputFilterByType BROTLI text/html 10 | 11 | 12 | 13 | SetOutputFilter BROTLI 14 | SetEnvIfNoCase Request_URI \.htm$ no-br 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | sudo: required 4 | dist: trusty 5 | 6 | matrix: 7 | include: 8 | - env: 9 | - APACHE_VERSION="2.2.x" 10 | - env: 11 | - APACHE_VERSION="2.4.x" 12 | 13 | before_install: 14 | - .travis/before_install.sh 15 | 16 | install: 17 | - ./autogen.sh 18 | - ./configure 19 | - make 20 | - .travis/install.sh 21 | 22 | before_script: 23 | - .travis/before_script.sh 24 | 25 | script: 26 | - ./tests/test.sh 27 | -------------------------------------------------------------------------------- /tests/conf/test.2.2.conf: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot /var/www/html 3 | 4 | Options FollowSymLinks 5 | AllowOverride None 6 | Order allow,deny 7 | allow from all 8 | 9 | 10 | 11 | AddOutputFilterByType BROTLI text/html 12 | 13 | 14 | 15 | SetOutputFilter BROTLI 16 | SetEnvIfNoCase Request_URI \.htm$ no-br 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.travis/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${APACHE_VERSION}" = "2.4.x" ]; then 4 | sudo apt-get update -qq 5 | sudo apt-get install -qq apache2 apache2-dev 6 | else 7 | sudo tee -a /etc/apt/sources.list < /dev/null 23 | rm -f *.o *.la *.lo *.slo Makefile.in Makefile 2> /dev/null 24 | 25 | if [ "$1" = "clean" ]; then 26 | exit 27 | fi 28 | 29 | echo "Running aclocal" 30 | ${ACLOCAL} 31 | 32 | echo "Running libtoolize" 33 | ${LIBTOOLIZE} -c 34 | 35 | echo "Running autoheader" 36 | ${AUTOHEADER} 37 | 38 | echo "Running automake" 39 | ${AUTOMAKE} -a 40 | 41 | echo "Running autoconf" 42 | ${AUTOCONF} 43 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | 2 | ACLOCAL_AMFLAGS = -I m4 3 | 4 | moddir = @APACHE_MODULEDIR@ 5 | mod_LTLIBRARIES = mod_brotli.la 6 | 7 | brotli_SOURCES = \ 8 | brotli/c/common/dictionary.c \ 9 | brotli/c/enc/backward_references.c \ 10 | brotli/c/enc/backward_references_hq.c \ 11 | brotli/c/enc/bit_cost.c \ 12 | brotli/c/enc/block_splitter.c \ 13 | brotli/c/enc/brotli_bit_stream.c \ 14 | brotli/c/enc/cluster.c \ 15 | brotli/c/enc/compress_fragment.c \ 16 | brotli/c/enc/compress_fragment_two_pass.c \ 17 | brotli/c/enc/dictionary_hash.c \ 18 | brotli/c/enc/encode.c \ 19 | brotli/c/enc/entropy_encode.c \ 20 | brotli/c/enc/histogram.c \ 21 | brotli/c/enc/literal_cost.c \ 22 | brotli/c/enc/memory.c \ 23 | brotli/c/enc/metablock.c \ 24 | brotli/c/enc/static_dict.c \ 25 | brotli/c/enc/utf8_util.c 26 | 27 | mod_brotli_la_SOURCES = mod_brotli.c $(brotli_SOURCES) 28 | 29 | mod_brotli_la_CFLAGS = @APACHE_CFLAGS@ @BROTLI_CFLAGS@ 30 | mod_brotli_la_CPPFLAGS = @APACHE_CPPFLAGS@ @BROTLI_CPPFLAGS@ 31 | mod_brotli_la_LDFLAGS = -avoid-version -module @APACHE_LDFLAGS@ @BROTLI_LDFLAGS@ -lm 32 | mod_brotli_la_LIBS = @APACHE_LIBS@ @BROTLI_LIBS@ 33 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$(cd $(dirname ${BASH_SOURCE:-$0}); pwd) 4 | 5 | failed= 6 | 7 | function check 8 | { 9 | [ -f "${DIR}/out" ] && rm -f "${DIR}/out" 10 | 11 | basename=$(basename "$1") 12 | 13 | curl -sI -H "Accept-Encoding:br" "${HOST:-localhost}$1" | egrep '^(HTTP|Content-Encoding|Content-Type)' | sed 's/; .*$//g' > "${DIR}/out" 14 | 15 | diff -q -Z --strip-trailing-cr "${DIR}/out" "${DIR}/response/${basename%.*}.out" > /dev/null 16 | if [ $? -eq 0 ]; then 17 | echo "SUCCEED: ${basename%.*}" 18 | else 19 | echo "FAILED : ${basename%.*}" 20 | echo "=======" 21 | curl -sI -H "Accept-Encoding:br" "${HOST:-localhost}$1" 22 | echo "=======" 23 | failed=1 24 | fi 25 | } 26 | 27 | : "generate contents" && { 28 | printf '.%.0s' {1..65537} >> ${DIR}/html/br1/test01.txt 29 | printf '.%.0s' {1..65537} >> ${DIR}/html/br1/test02.html 30 | printf '.%.0s' {1..65537} >> ${DIR}/html/br2/test03.txt 31 | printf '.%.0s' {1..65537} >> ${DIR}/html/br2/test04.html 32 | printf '.%.0s' {1..65537} >> ${DIR}/html/br2/test05.htm 33 | } 34 | 35 | : "/br1" && { 36 | check "/br1/test01.txt" 37 | check "/br1/test02.html" 38 | } 39 | 40 | : "/br2" && { 41 | check "/br2/test03.txt" 42 | check "/br2/test04.html" 43 | check "/br2/test05.htm" 44 | } 45 | 46 | [ -f "${DIR}/out" ] && rm -f "${DIR}/out" 47 | 48 | if [ -n "${failed}" ]; then 49 | exit 1 50 | fi 51 | 52 | exit 0 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mod_brotli 2 | 3 | > NOTE: Please use the official module since Apache 2.4.26 : [Apache Module mod_brotli](https://httpd.apache.org/docs/2.4/en/mod/mod_brotli.html) 4 | 5 | [![Build Status](https://travis-ci.org/kjdev/apache-mod-brotli.svg?branch=master)](https://travis-ci.org/kjdev/apache-mod-brotli) 6 | 7 | mod_brotli is a Brotli compression module for Apache HTTPD Server. 8 | 9 | ## Requires 10 | 11 | * [brotli](https://github.com/google/brotli) 12 | 13 | * Libtool 14 | 15 | * Automake 16 | 17 | ## Build 18 | 19 | ```shell 20 | git clone --depth=1 --recursive https://github.com/kjdev/apache-mod-brotli.git 21 | cd apache-mod-brotli 22 | ./autogen.sh 23 | ./configure 24 | make 25 | ``` 26 | 27 | ## Install 28 | 29 | ```shell 30 | install -p -m 755 -D .libs/mod_brotli.so /etc/httpd/modules/mod_brotli.so 31 | ``` 32 | 33 | ## Configuration 34 | 35 | `httpd.conf`: 36 | 37 | ```apache 38 | # Load module 39 | LoadModule brotli_module modules/mod_brotli.so 40 | 41 | 42 | # Output filter 43 | AddOutputFilterByType BROTLI text/html text/plain text/css text/xml 44 | 45 | # SetOutputFilter BROTLI 46 | # SetEnvIfNoCase Request_URI \.txt$ no-br 47 | 48 | # Compression 49 | ## BrotliCompressionLevel: 0-11 (default: 11) 50 | BrotliCompressionLevel 10 51 | 52 | ## BrotliWindowSize: 10-24 (default: 22) 53 | BrotliWindowSize 22 54 | 55 | # Specifies how to change the ETag header when the response is compressed 56 | ## BrotliAlterEtag: AddSuffix, NoChange, Remove (default: AddSuffix) 57 | BrotliAlterEtag AddSuffix 58 | 59 | # Filter note 60 | BrotliFilterNote Input brotli_in 61 | BrotliFilterNote Output brotli_out 62 | BrotliFilterNote Ratio brotli_ratio 63 | 64 | LogFormat '"%r" %{brotli_out}n/%{brotli_in}n (%{brotli_ratio}n)' brotli 65 | CustomLog logs/access_log brotli 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # -*- Autoconf -*- 2 | # Process this file with autoconf to produce a configure script. 3 | 4 | AC_INIT(mod_brotli, 0.5.0,[]) 5 | 6 | AC_CONFIG_SRCDIR([mod_brotli.c]) 7 | AC_CONFIG_HEADERS([config.h]) 8 | AC_CONFIG_MACRO_DIR([m4]) 9 | 10 | AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects]) 11 | 12 | # Checks for programs. 13 | AM_PROG_AR 14 | AC_PROG_LIBTOOL 15 | 16 | # Checks for apxs. 17 | AC_ARG_WITH([apxs], 18 | [AC_HELP_STRING([--with-apxs=PATH], [apxs path [default=yes]])], 19 | [APXS_PATH="$withval"], 20 | [APXS_PATH="/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"]) 21 | 22 | AC_PATH_PROG(APXS, apxs, no, ${APXS_PATH}) 23 | 24 | AS_IF([test "x${APXS}" = "xno" ], 25 | [AC_PATH_PROG(APXS2, apxs2, no, ${APXS_PATH}) 26 | AS_IF([test "x${APXS2}" = "xno" ], 27 | [AC_MSG_ERROR([apxs not found])], 28 | [test "x${APXS2}" = "x"], 29 | [AC_MSG_ERROR([apxs not found])]) 30 | APXS=${APXS2}], 31 | [test "x${APXS}" = "x"], 32 | [AC_MSG_ERROR([apxs not found])]) 33 | 34 | APXS_CFLAGS=`${APXS} -q CFLAGS 2> /dev/null` 35 | APXS_CPPFLAGS=`${APXS} -q CPPFLAGS 2> /dev/null` 36 | APXS_LDFLAGS=`${APXS} -q LDFLAGS 2> /dev/null` 37 | APXS_LIBS=`${APXS} -q LIBS 2> /dev/null` 38 | APXS_LIBEXECDIR=`${APXS} -q LIBEXECDIR 2> /dev/null` 39 | APXS_INCLUDEDIR=`${APXS} -q INCLUDEDIR 2> /dev/null` 40 | APXS_INCLUDES="-I${APXS_INCLUDEDIR}" 41 | 42 | # Checks for apr. 43 | AC_ARG_WITH([apr], 44 | [AC_HELP_STRING([--with-apr=PATH], 45 | [apr config path [default=yes]])], 46 | [APR_CONFIG="$withval"], 47 | [with_apr=yes]) 48 | 49 | AC_MSG_CHECKING([whether apr]) 50 | AS_IF([test "x$with_apr" != "xno"], 51 | [if test ! -x "${APR_CONFIG}" -o -z "${APR_CONFIG}"; then 52 | APR_PATH=`${APXS} -q APR_BINDIR 2> /dev/null` 53 | APR_CONFIG="${APR_PATH}/apr-1-config" 54 | if test ! -x "${APR_CONFIG}"; then 55 | APR_CONFIG="${APR_PATH}/apr-config" 56 | fi 57 | fi 58 | APR_INCLUDES=`${APR_CONFIG} --includes 2> /dev/null` 59 | APR_CFLAGS=`${APR_CONFIG} --cflags 2> /dev/null` 60 | APR_CPPFLAGS=`${APR_CONFIG} --cppflags 2> /dev/null` 61 | APR_LDFLAGS=`${APR_CONFIG} --ldflags 2> /dev/null` 62 | APR_LIBS=`${APR_CONFIG} --libs 2> /dev/null` 63 | AC_MSG_RESULT(yes)], 64 | [AC_MSG_ERROR(apr not found)]) 65 | 66 | # Apache libraries. 67 | APACHE_MODULEDIR="${APXS_LIBEXECDIR}" 68 | APACHE_INCLUDES="${APXS_INCLUDES} ${APR_INCLUDES}" 69 | APACHE_CFLAGS="${APXS_CFLAGS} ${APR_CFLAGS} ${APACHE_INCLUDES}" 70 | APACHE_CPPFLAGS="${APXS_CPPFLAGS} ${APR_CPPFLAGS} ${APACHE_INCLUDES}" 71 | APACHE_LDFLAGS="${APXS_LDFLAGS} ${APR_LDFLAGS}" 72 | APACHE_LIBS="${APXS_LIBS} ${APR_LIBS}" 73 | 74 | AC_SUBST(APACHE_MODULEDIR) 75 | AC_SUBST(APACHE_INCLUDES) 76 | AC_SUBST(APACHE_CFLAGS) 77 | AC_SUBST(APACHE_CPPFLAGS) 78 | AC_SUBST(APACHE_LDFLAGS) 79 | AC_SUBST(APACHE_LIBS) 80 | 81 | 82 | # Brotli 83 | BROTLI_CFLAGS= 84 | #BROTLI_CPPFLAGS="-I./brotli/include -std=c++11" 85 | BROTLI_CPPFLAGS="-I./brotli/c/include" 86 | BROTLI_LDFLAGS= 87 | BROTLI_LIBS= 88 | 89 | AC_SUBST(BROTLI_CFLAGS) 90 | AC_SUBST(BROTLI_CPPFLAGS) 91 | AC_SUBST(BROTLI_LDFLAGS) 92 | AC_SUBST(BROTLI_LIBS) 93 | 94 | 95 | AC_CONFIG_FILES([ 96 | Makefile 97 | ]) 98 | AC_OUTPUT 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /mod_brotli.c: -------------------------------------------------------------------------------- 1 | /* 2 | * mod_brotli.c: Apache Brotli module 3 | * 4 | * LoadModule brotli_module modules/mod_brotli.so 5 | * 6 | * # SetOutputFilter BROTLI 7 | * # SetEnvIfNoCase Request_URI \.txt$ no-br 8 | * 9 | * AddOutputFilterByType BROTLI text/html 10 | * 11 | * # BrotliAlterEtag AddSuffix 12 | * 13 | * # BrotliFilterNote 14 | * # BrotliFilterNote Input brotli_in 15 | * # BrotliFilterNote Output brotli_out 16 | * # BrotliFilterNote Ratio brotli_ratio 17 | * # LogFormat '"%r" %{brotli_out}n/%{brotli_in}n (%{brotli_ratio}n)' brotli 18 | * # CustomLog logs/access_log brotli 19 | * 20 | */ 21 | 22 | #ifndef HAVE_CONFIG_H 23 | # include "config.h" 24 | # undef PACKAGE_NAME 25 | # undef PACKAGE_STRING 26 | # undef PACKAGE_TARNAME 27 | # undef PACKAGE_VERSION 28 | #endif 29 | 30 | #include 31 | #include "httpd.h" 32 | #include "http_config.h" 33 | #include "http_log.h" 34 | #include "http_core.h" 35 | #include "apr_lib.h" 36 | #include "apr_strings.h" 37 | #include "apr_general.h" 38 | #include "util_filter.h" 39 | #include "apr_buckets.h" 40 | #include "http_request.h" 41 | #define APR_WANT_STRFUNC 42 | #include "apr_want.h" 43 | #include "mod_ssl.h" 44 | 45 | #include "brotli/encode.h" 46 | 47 | static const char brotliFilterName[] = "BROTLI"; 48 | module AP_MODULE_DECLARE_DATA brotli_module; 49 | 50 | #define AP_BROTLI_ETAG_NOCHANGE 0 51 | #define AP_BROTLI_ETAG_ADDSUFFIX 1 52 | #define AP_BROTLI_ETAG_REMOVE 2 53 | 54 | #ifdef APLOG_USE_MODULE 55 | APLOG_USE_MODULE(brotli); 56 | #endif 57 | 58 | #ifndef APLOG_TRACE1 59 | #define APLOG_TRACE1 APLOG_DEBUG 60 | #endif 61 | 62 | #ifndef APLOG_R_IS_LEVEL 63 | #define APLOG_R_IS_LEVEL(r,level) false 64 | #endif 65 | 66 | #ifndef APLOGNO 67 | #define APLOGNO(n) "AH" #n ": " 68 | #endif 69 | 70 | typedef struct brotli_filter_config_t 71 | { 72 | int compressionlevel; 73 | int windowSize; 74 | const char *note_ratio_name; 75 | const char *note_input_name; 76 | const char *note_output_name; 77 | int etag_opt; 78 | } brotli_filter_config; 79 | 80 | static APR_OPTIONAL_FN_TYPE(ssl_var_lookup) *mod_brotli_ssl_var = NULL; 81 | 82 | static void * 83 | create_brotli_server_config(apr_pool_t *p, server_rec *s) 84 | { 85 | brotli_filter_config *c = (brotli_filter_config *)apr_pcalloc(p, sizeof *c); 86 | 87 | c->compressionlevel = BROTLI_DEFAULT_QUALITY; 88 | c->windowSize = BROTLI_DEFAULT_WINDOW; 89 | c->etag_opt = AP_BROTLI_ETAG_ADDSUFFIX; 90 | 91 | return c; 92 | } 93 | 94 | static const char * 95 | brotli_set_compressionlevel(cmd_parms *cmd, void *dummy, const char *arg) 96 | { 97 | brotli_filter_config *c; 98 | c = (brotli_filter_config *)ap_get_module_config(cmd->server->module_config, 99 | &brotli_module); 100 | int i = atoi(arg); 101 | 102 | if (i < 0) { 103 | return "BrotliCompression Level should be positive"; 104 | } 105 | 106 | if (i < BROTLI_MIN_QUALITY || i > BROTLI_MAX_QUALITY) { 107 | return "BrotliCompression level is not a valid range"; 108 | } 109 | 110 | c->compressionlevel = i; 111 | 112 | return NULL; 113 | } 114 | 115 | static const char * 116 | brotli_set_window_size(cmd_parms *cmd, void *dummy, const char *arg) 117 | { 118 | brotli_filter_config *c; 119 | c = (brotli_filter_config *)ap_get_module_config(cmd->server->module_config, 120 | &brotli_module); 121 | int i = atoi(arg); 122 | 123 | if (i < BROTLI_MIN_WINDOW_BITS || i > BROTLI_MAX_WINDOW_BITS) { 124 | return "BrotliWindowSize is not a valid range"; 125 | } 126 | 127 | c->windowSize = i; 128 | 129 | return NULL; 130 | } 131 | 132 | static const char * 133 | brotli_set_etag(cmd_parms *cmd, void *dummy, const char *arg) 134 | { 135 | brotli_filter_config *c; 136 | c = (brotli_filter_config *)ap_get_module_config(cmd->server->module_config, 137 | &brotli_module); 138 | 139 | if (!strcasecmp(arg, "NoChange")) { 140 | c->etag_opt = AP_BROTLI_ETAG_NOCHANGE; 141 | } else if (!strcasecmp(arg, "AddSuffix")) { 142 | c->etag_opt = AP_BROTLI_ETAG_ADDSUFFIX; 143 | } else if (!strcasecmp(arg, "Remove")) { 144 | c->etag_opt = AP_BROTLI_ETAG_REMOVE; 145 | } else { 146 | return "BrotliAlterEtag accepts only 'NoChange', 'AddSuffix', and 'Remove'"; 147 | } 148 | 149 | return NULL; 150 | } 151 | 152 | static const char * 153 | brotli_set_note(cmd_parms *cmd, void *dummy, const char *arg1, const char *arg2) 154 | { 155 | brotli_filter_config *c; 156 | c = (brotli_filter_config *)ap_get_module_config(cmd->server->module_config, 157 | &brotli_module); 158 | 159 | if (arg2 == NULL) { 160 | c->note_ratio_name = arg1; 161 | } 162 | else if (!strcasecmp(arg1, "ratio")) { 163 | c->note_ratio_name = arg2; 164 | } 165 | else if (!strcasecmp(arg1, "input")) { 166 | c->note_input_name = arg2; 167 | } 168 | else if (!strcasecmp(arg1, "output")) { 169 | c->note_output_name = arg2; 170 | } 171 | else { 172 | return apr_psprintf(cmd->pool, "Unknown note type %s", arg1); 173 | } 174 | 175 | return NULL; 176 | } 177 | 178 | typedef struct brotli_ctx_t 179 | { 180 | BrotliEncoderState *state; 181 | apr_bucket_brigade *bb; 182 | unsigned int filter_init:1; 183 | size_t bytes_in; 184 | size_t bytes_out; 185 | } brotli_ctx; 186 | 187 | static apr_status_t 188 | brotli_ctx_cleanup(void *data) 189 | { 190 | brotli_ctx *ctx = (brotli_ctx *)data; 191 | 192 | if (ctx && ctx->state) { 193 | BrotliEncoderDestroyInstance(ctx->state); 194 | ctx->state = NULL; 195 | } 196 | 197 | return APR_SUCCESS; 198 | } 199 | 200 | /* 201 | * ETag must be unique among the possible representations, so a change 202 | * to content-encoding requires a corresponding change to the ETag. 203 | * This routine appends -transform (e.g., -br) to the entity-tag 204 | * value inside the double-quotes if an ETag has already been set 205 | * and its value already contains double-quotes. PR 39727 206 | */ 207 | static void 208 | brotli_check_etag(request_rec *r, const char *transform, int etag_opt) 209 | { 210 | const char *etag = apr_table_get(r->headers_out, "ETag"); 211 | apr_size_t etaglen; 212 | 213 | if (etag_opt == AP_BROTLI_ETAG_REMOVE) { 214 | apr_table_unset(r->headers_out, "ETag"); 215 | return; 216 | } 217 | 218 | if ((etag && ((etaglen = strlen(etag)) > 2))) { 219 | if (etag[etaglen - 1] == '"') { 220 | apr_size_t transformlen = strlen(transform); 221 | char *newtag = (char *)apr_palloc(r->pool, etaglen + transformlen + 2); 222 | char *d = newtag; 223 | char *e = d + etaglen - 1; 224 | const char *s = etag; 225 | 226 | for (; d < e; ++d, ++s) { 227 | *d = *s; /* copy etag to newtag up to last quote */ 228 | } 229 | *d++ = '-'; /* append dash to newtag */ 230 | s = transform; 231 | e = d + transformlen; 232 | for (; d < e; ++d, ++s) { 233 | *d = *s; /* copy transform to newtag */ 234 | } 235 | *d++ = '"'; /* append quote to newtag */ 236 | *d = '\0'; /* null terminate newtag */ 237 | 238 | apr_table_setn(r->headers_out, "ETag", newtag); 239 | } 240 | } 241 | } 242 | 243 | static int 244 | have_ssl_compression(request_rec *r) 245 | { 246 | if (mod_brotli_ssl_var == NULL) { 247 | return 0; 248 | } 249 | const char *comp = mod_brotli_ssl_var(r->pool, r->server, r->connection, r, 250 | (char *)"SSL_COMPRESS_METHOD"); 251 | if (comp == NULL || *comp == '\0' || strcmp(comp, "NULL") == 0) { 252 | return 0; 253 | } 254 | return 1; 255 | } 256 | 257 | static apr_status_t 258 | brotli_compress(unsigned int operation, 259 | size_t len, const char *data, 260 | brotli_ctx *ctx, apr_pool_t *pool, 261 | struct apr_bucket_alloc_t *bucket_alloc) 262 | { 263 | const uint8_t *next_in = (uint8_t *)data; 264 | size_t avail_in = len; 265 | 266 | while (avail_in >= 0) { 267 | uint8_t *next_out = NULL; 268 | size_t avail_out = 0; 269 | 270 | if (!BrotliEncoderCompressStream(ctx->state, operation, 271 | &avail_in, &next_in, 272 | &avail_out, &next_out, NULL)) { 273 | return APR_EGENERAL; 274 | } 275 | 276 | if (BrotliEncoderHasMoreOutput(ctx->state)) { 277 | size_t size = 0; 278 | char *buffer = (char *)BrotliEncoderTakeOutput(ctx->state, &size); 279 | ctx->bytes_out += size; 280 | 281 | apr_bucket *b = apr_bucket_heap_create(buffer, size, NULL, bucket_alloc); 282 | APR_BRIGADE_INSERT_TAIL(ctx->bb, b); 283 | } else if (avail_in == 0) { 284 | break; 285 | } 286 | } 287 | 288 | return APR_SUCCESS; 289 | } 290 | 291 | static apr_status_t 292 | brotli_out_filter(ap_filter_t *f, apr_bucket_brigade *bb) 293 | { 294 | apr_bucket *e; 295 | request_rec *r = f->r; 296 | brotli_ctx *ctx = (brotli_ctx *)f->ctx; 297 | apr_size_t len = 0, blen; 298 | const char *data; 299 | brotli_filter_config *c; 300 | 301 | /* Do nothing if asked to filter nothing. */ 302 | if (APR_BRIGADE_EMPTY(bb)) { 303 | return APR_SUCCESS; 304 | } 305 | 306 | c = (brotli_filter_config *)ap_get_module_config(r->server->module_config, 307 | &brotli_module); 308 | 309 | if (!ctx) { 310 | char *token; 311 | const char *encoding; 312 | 313 | if (have_ssl_compression(r)) { 314 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 315 | "Compression enabled at SSL level; not compressing " 316 | "at HTTP level."); 317 | ap_remove_output_filter(f); 318 | return ap_pass_brigade(f->next, bb); 319 | } 320 | 321 | /* We have checked above that bb is not empty */ 322 | e = APR_BRIGADE_LAST(bb); 323 | if (APR_BUCKET_IS_EOS(e)) { 324 | /* 325 | * If we already know the size of the response, we can skip 326 | * compression on responses smaller than the compression overhead. 327 | * Otherwise the headers will be sent to the client without 328 | * "Content-Encoding: br". 329 | */ 330 | e = APR_BRIGADE_FIRST(bb); 331 | while (1) { 332 | apr_status_t rc; 333 | if (APR_BUCKET_IS_EOS(e)) { 334 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 335 | "Not compressing very small response of %" 336 | APR_SIZE_T_FMT " bytes", len); 337 | ap_remove_output_filter(f); 338 | return ap_pass_brigade(f->next, bb); 339 | } 340 | if (APR_BUCKET_IS_METADATA(e)) { 341 | e = APR_BUCKET_NEXT(e); 342 | continue; 343 | } 344 | 345 | rc = apr_bucket_read(e, &data, &blen, APR_BLOCK_READ); 346 | if (rc != APR_SUCCESS) { 347 | return rc; 348 | } 349 | 350 | len += blen; 351 | 352 | /* 50 is for Content-Encoding and Vary headers and ETag suffix */ 353 | if (len > 50) { 354 | break; 355 | } 356 | 357 | e = APR_BUCKET_NEXT(e); 358 | } 359 | } 360 | 361 | f->ctx = (brotli_ctx *)apr_pcalloc(r->pool, sizeof(*ctx)); 362 | ctx = (brotli_ctx *)f->ctx; 363 | 364 | /* 365 | * Only work on main request, not subrequests, 366 | * that are not a 204 response with no content 367 | * and are not tagged with the no-br env variable 368 | * and not a partial response to a Range request. 369 | */ 370 | if ((r->main != NULL) || (r->status == HTTP_NO_CONTENT) 371 | || apr_table_get(r->subprocess_env, "no-br") 372 | || apr_table_get(r->headers_out, "Content-Range")) { 373 | if (APLOG_R_IS_LEVEL(r, APLOG_TRACE1)) { 374 | const char *reason = 375 | (r->main != NULL) ? "subrequest" : 376 | (r->status == HTTP_NO_CONTENT) ? "no content" : 377 | apr_table_get(r->subprocess_env, "no-br") ? "no-br" : 378 | "content-range"; 379 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 380 | "Not compressing (%s)", reason); 381 | } 382 | ap_remove_output_filter(f); 383 | return ap_pass_brigade(f->next, bb); 384 | } 385 | 386 | /* 387 | * Some browsers might have problems with content types 388 | * other than text/html, so set br-only-text/html 389 | * (with browsermatch) for them 390 | */ 391 | if (r->content_type == NULL 392 | || strncmp(r->content_type, "text/html", 9)) { 393 | const char *env_value = apr_table_get(r->subprocess_env, 394 | "br-only-text/html"); 395 | if (env_value && (strcmp(env_value,"1") == 0)) { 396 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 397 | "Not compressing, (br-only-text/html)"); 398 | ap_remove_output_filter(f); 399 | return ap_pass_brigade(f->next, bb); 400 | } 401 | } 402 | 403 | /* 404 | * Let's see what our current Content-Encoding is. 405 | * If it's already encoded, don't compress again. 406 | * (We could, but let's not.) 407 | */ 408 | encoding = apr_table_get(r->headers_out, "Content-Encoding"); 409 | if (encoding) { 410 | const char *err_enc; 411 | err_enc = apr_table_get(r->err_headers_out, "Content-Encoding"); 412 | if (err_enc) { 413 | encoding = apr_pstrcat(r->pool, encoding, ",", err_enc, NULL); 414 | } 415 | } else { 416 | encoding = apr_table_get(r->err_headers_out, "Content-Encoding"); 417 | } 418 | 419 | if (r->content_encoding) { 420 | encoding = encoding ? apr_pstrcat(r->pool, encoding, ",", 421 | r->content_encoding, NULL) 422 | : r->content_encoding; 423 | } 424 | 425 | if (encoding) { 426 | const char *tmp = encoding; 427 | 428 | token = ap_get_token(r->pool, &tmp, 0); 429 | while (token && *token) { 430 | /* stolen from mod_negotiation: */ 431 | if (strcmp(token, "identity") && strcmp(token, "7bit") && 432 | strcmp(token, "8bit") && strcmp(token, "binary")) { 433 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 434 | "Not compressing (content-encoding already " 435 | " set: %s)", token); 436 | ap_remove_output_filter(f); 437 | return ap_pass_brigade(f->next, bb); 438 | } 439 | 440 | /* Otherwise, skip token */ 441 | if (*tmp) { 442 | ++tmp; 443 | } 444 | token = (*tmp) ? ap_get_token(r->pool, &tmp, 0) : NULL; 445 | } 446 | } 447 | 448 | /* 449 | * Even if we don't accept this request based on it not having 450 | * the Accept-Encoding, we need to note that we were looking 451 | * for this header and downstream proxies should be aware of that. 452 | */ 453 | apr_table_mergen(r->headers_out, "Vary", "Accept-Encoding"); 454 | 455 | /* 456 | * force-br will just force it out regardless if the browser 457 | * can actually do anything with it. 458 | */ 459 | if (!apr_table_get(r->subprocess_env, "force-br")) { 460 | /* if they don't have the line, then they can't play */ 461 | const char *accepts = apr_table_get(r->headers_in, "Accept-Encoding"); 462 | if (accepts == NULL) { 463 | ap_remove_output_filter(f); 464 | return ap_pass_brigade(f->next, bb); 465 | } 466 | 467 | token = ap_get_token(r->pool, &accepts, 0); 468 | while (token && token[0] && strcasecmp(token, "br")) { 469 | /* skip parameters, XXX: ;q=foo evaluation? */ 470 | while (*accepts == ';') { 471 | ++accepts; 472 | ap_get_token(r->pool, &accepts, 1); 473 | } 474 | 475 | /* retrieve next token */ 476 | if (*accepts == ',') { 477 | ++accepts; 478 | } 479 | token = (*accepts) ? ap_get_token(r->pool, &accepts, 0) : NULL; 480 | } 481 | 482 | /* No acceptable token found. */ 483 | if (token == NULL || token[0] == '\0') { 484 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 485 | "Not compressing (no Accept-Encoding: br)"); 486 | ap_remove_output_filter(f); 487 | return ap_pass_brigade(f->next, bb); 488 | } 489 | } else { 490 | ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 491 | "Forcing compression (force-br set)"); 492 | } 493 | 494 | /* 495 | * At this point we have decided to filter the content. Let's try to 496 | * to initialize zlib (except for 304 responses, where we will only 497 | * send out the headers). 498 | */ 499 | 500 | if (r->status != HTTP_NOT_MODIFIED) { 501 | ctx->bb = apr_brigade_create(r->pool, f->c->bucket_alloc); 502 | 503 | if (ctx->state == NULL) { 504 | ctx->state = BrotliEncoderCreateInstance(0, 0, 0); 505 | if (ctx->state == NULL) { 506 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(0474) 507 | "unable to init BrotliEncoderCreateInstance: URL %s", 508 | r->uri); 509 | ap_remove_output_filter(f); 510 | return ap_pass_brigade(f->next, bb); 511 | } 512 | 513 | uint32_t quality = (uint32_t)c->compressionlevel; 514 | uint32_t lgwin = (uint32_t)c->windowSize; 515 | if (len > 0) { 516 | while (len < (1 << (lgwin - 1)) && lgwin > BROTLI_MIN_WINDOW_BITS) { 517 | lgwin--; 518 | } 519 | } 520 | 521 | BrotliEncoderSetParameter(ctx->state, BROTLI_PARAM_QUALITY, quality); 522 | BrotliEncoderSetParameter(ctx->state, BROTLI_PARAM_LGWIN, lgwin); 523 | 524 | ctx->bytes_in = 0; 525 | ctx->bytes_out = 0; 526 | 527 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(0485) 528 | "brotli encoder: quality: %d lgwin: %d", quality, lgwin); 529 | } 530 | 531 | /* 532 | * Register a cleanup function to ensure 533 | * that we cleanup the internal brotli resources. 534 | */ 535 | apr_pool_cleanup_register(r->pool, ctx, brotli_ctx_cleanup, 536 | apr_pool_cleanup_null); 537 | 538 | /* 539 | * Set the filter init flag so subsequent invocations know we are 540 | * active. 541 | */ 542 | ctx->filter_init = 1; 543 | } 544 | 545 | /* If the entire Content-Encoding is "identity", we can replace it. */ 546 | if (!encoding || !strcasecmp(encoding, "identity")) { 547 | apr_table_setn(r->headers_out, "Content-Encoding", "br"); 548 | } else { 549 | apr_table_mergen(r->headers_out, "Content-Encoding", "br"); 550 | } 551 | /* Fix r->content_encoding if it was set before */ 552 | if (r->content_encoding) { 553 | r->content_encoding = apr_table_get(r->headers_out, "Content-Encoding"); 554 | } 555 | apr_table_unset(r->headers_out, "Content-Length"); 556 | if (c->etag_opt != AP_BROTLI_ETAG_NOCHANGE) { 557 | brotli_check_etag(r, "br", c->etag_opt); 558 | } 559 | 560 | /* For a 304 response, only change the headers */ 561 | if (r->status == HTTP_NOT_MODIFIED) { 562 | ap_remove_output_filter(f); 563 | return ap_pass_brigade(f->next, bb); 564 | } 565 | 566 | } else if (!ctx->filter_init) { 567 | /* 568 | * Hmm. We've run through the filter init before as we have a ctx, 569 | * but we never initialized. We probably have a dangling ref. Bail. 570 | */ 571 | return ap_pass_brigade(f->next, bb); 572 | } 573 | 574 | while (!APR_BRIGADE_EMPTY(bb)) { 575 | /* 576 | * Optimization: If we are a HEAD request and bytes_sent is not zero 577 | * it means that we have passed the content-length filter once and 578 | * have more data to sent. This means that the content-length filter 579 | * could not determine our content-length for the response to the 580 | * HEAD request anyway (the associated GET request would deliver the 581 | * body in chunked encoding) and we can stop compressing. 582 | */ 583 | if (r->header_only && r->bytes_sent) { 584 | ap_remove_output_filter(f); 585 | return ap_pass_brigade(f->next, bb); 586 | } 587 | 588 | e = APR_BRIGADE_FIRST(bb); 589 | 590 | if (APR_BUCKET_IS_EOS(e)) { 591 | /* flush the remaining data from the brotli buffers */ 592 | apr_status_t rv = brotli_compress(BROTLI_OPERATION_FINISH, 0, NULL, 593 | ctx, r->pool, f->c->bucket_alloc); 594 | if (rv != APR_SUCCESS) { 595 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(0554) 596 | "Brotli compress error"); 597 | return rv; 598 | } 599 | 600 | /* leave notes for logging */ 601 | if (c->note_input_name) { 602 | apr_table_setn(r->notes, c->note_input_name, 603 | (ctx->bytes_in > 0) 604 | ? apr_off_t_toa(r->pool, ctx->bytes_in) 605 | : "-"); 606 | } 607 | 608 | if (c->note_output_name) { 609 | apr_table_setn(r->notes, c->note_output_name, 610 | (ctx->bytes_in > 0) 611 | ? apr_off_t_toa(r->pool, ctx->bytes_out) 612 | : "-"); 613 | } 614 | 615 | if (c->note_ratio_name) { 616 | apr_table_setn(r->notes, c->note_ratio_name, 617 | (ctx->bytes_in > 0) 618 | ? apr_itoa(r->pool, 619 | (int)(ctx->bytes_out * 100 / ctx->bytes_in)) 620 | : "-"); 621 | } 622 | 623 | if (ctx->state) { 624 | BrotliEncoderDestroyInstance(ctx->state); 625 | ctx->state = NULL; 626 | } 627 | 628 | /* No need for cleanup any longer */ 629 | apr_pool_cleanup_kill(r->pool, ctx, brotli_ctx_cleanup); 630 | 631 | /* Remove EOS from the old list, and insert into the new. */ 632 | APR_BUCKET_REMOVE(e); 633 | APR_BRIGADE_INSERT_TAIL(ctx->bb, e); 634 | 635 | /* 636 | * Okay, we've seen the EOS. 637 | * Time to pass it along down the chain. 638 | */ 639 | return ap_pass_brigade(f->next, ctx->bb); 640 | } 641 | 642 | if (APR_BUCKET_IS_FLUSH(e)) { 643 | /* Remove flush bucket from old brigade anf insert into the new. */ 644 | APR_BUCKET_REMOVE(e); 645 | APR_BRIGADE_INSERT_TAIL(ctx->bb, e); 646 | apr_status_t rv= ap_pass_brigade(f->next, ctx->bb); 647 | if (rv != APR_SUCCESS) { 648 | return rv; 649 | } 650 | continue; 651 | } 652 | 653 | if (APR_BUCKET_IS_METADATA(e)) { 654 | /* 655 | * Remove meta data bucket from old brigade and insert into the 656 | * new. 657 | */ 658 | APR_BUCKET_REMOVE(e); 659 | APR_BRIGADE_INSERT_TAIL(ctx->bb, e); 660 | continue; 661 | } 662 | 663 | /* read */ 664 | apr_bucket_read(e, &data, &len, APR_BLOCK_READ); 665 | if (!len) { 666 | apr_bucket_delete(e); 667 | continue; 668 | } 669 | if (len > APR_INT32_MAX) { 670 | apr_bucket_split(e, APR_INT32_MAX); 671 | apr_bucket_read(e, &data, &len, APR_BLOCK_READ); 672 | } 673 | 674 | /* write */ 675 | if (len != 0) { 676 | apr_status_t rv = brotli_compress(BROTLI_OPERATION_PROCESS, len, data, 677 | ctx, r->pool, f->c->bucket_alloc); 678 | if (rv != APR_SUCCESS) { 679 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(0657) 680 | "Brotli compress error"); 681 | return APR_EGENERAL; 682 | } 683 | } 684 | 685 | apr_bucket_delete(e); 686 | } 687 | 688 | apr_brigade_cleanup(bb); 689 | return APR_SUCCESS; 690 | } 691 | 692 | static int 693 | mod_brotli_post_config(apr_pool_t *pconf, apr_pool_t *plog, 694 | apr_pool_t *ptemp, server_rec *s) 695 | { 696 | mod_brotli_ssl_var = APR_RETRIEVE_OPTIONAL_FN(ssl_var_lookup); 697 | return OK; 698 | } 699 | 700 | #define PROTO_FLAGS AP_FILTER_PROTO_CHANGE|AP_FILTER_PROTO_CHANGE_LENGTH 701 | 702 | static void 703 | register_hooks(apr_pool_t *p) 704 | { 705 | ap_register_output_filter(brotliFilterName, brotli_out_filter, NULL, 706 | AP_FTYPE_CONTENT_SET); 707 | ap_hook_post_config(mod_brotli_post_config, NULL, NULL, APR_HOOK_MIDDLE); 708 | } 709 | 710 | static const command_rec brotli_filter_cmds[] = { 711 | AP_INIT_TAKE1("BrotliCompressionLevel", 712 | brotli_set_compressionlevel, NULL, RSRC_CONF, 713 | "Set the Brotli Compression Level"), 714 | AP_INIT_TAKE1("BrotliWindowSize", 715 | brotli_set_window_size, NULL, RSRC_CONF, 716 | "Set the Brotli window size"), 717 | AP_INIT_TAKE1("BrotliAlterEtag", 718 | brotli_set_etag, NULL, RSRC_CONF, 719 | "Set how mod_brotli should modify ETAG response headers: 'AddSuffix' (default), 'NoChange' (2.2.x behavior), 'Remove'"), 720 | /* 721 | AP_INIT_TAKE1("BrotliBufferSize", 722 | brotli_set_buffer_size, NULL, RSRC_CONF, 723 | "Set the Brotli Buffer Size"), 724 | */ 725 | AP_INIT_TAKE12("BrotliFilterNote", 726 | brotli_set_note, NULL, RSRC_CONF, 727 | "Set a note to report on compression ratio"), 728 | {NULL} 729 | }; 730 | 731 | module AP_MODULE_DECLARE_DATA brotli_module = { 732 | STANDARD20_MODULE_STUFF, 733 | NULL, /* dir config creater */ 734 | NULL, /* dir merger --- default is to override */ 735 | create_brotli_server_config, /* server config */ 736 | NULL, /* merge server config */ 737 | brotli_filter_cmds, /* command table */ 738 | register_hooks /* register hooks */ 739 | }; 740 | --------------------------------------------------------------------------------