├── 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 | [](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 |
--------------------------------------------------------------------------------