├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RHEL8-Testing.md ├── config ├── src └── ngx_http_dynamic_etag_module.c └── t ├── config.t └── etag.t /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | community_bridge: null 2 | github: dvershinin 3 | issuehunt: null 4 | ko_fi: null 5 | liberapay: null 6 | open_collective: null 7 | otechie: null 8 | tidelift: null 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Test Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 2 11 | matrix: 12 | nginx-branch: [stable, mainline] 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install build and test dependencies 17 | run: | 18 | sudo apt-get --yes update 19 | sudo apt-get install --yes libpcre3-dev libssl-dev perl cpanminus wget 20 | 21 | - name: Create NGINX download directory 22 | run: | 23 | mkdir nginx 24 | 25 | - name: Download ${{ matrix.nginx-branch }} NGINX 26 | uses: dvershinin/lastversion-action@main 27 | with: 28 | repository: 'nginx' 29 | action: 'unzip' 30 | branch: ${{ matrix.nginx-branch }} 31 | working_directory: ./nginx 32 | 33 | - name: Configure NGINX to compile with the module statically 34 | run: | 35 | cd nginx && ./configure --with-debug --add-module=.. 36 | 37 | - name: Make NGINX 38 | run: | 39 | cd nginx && make -j$(nproc) 40 | 41 | - name: Ensure Test::Nginx installed 42 | run: | 43 | cpanm --notest --local-lib=$HOME/perl5 Test::Nginx 44 | 45 | - name: Test the module 46 | run: | 47 | PATH=$(pwd)/nginx/objs:$PATH PERL5LIB=$HOME/perl5/lib/perl5 TEST_NGINX_VERBOSE=true prove -v 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject 2 | /.idea 3 | /t/servroot -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | compiler: 3 | - clang 4 | - gcc 5 | env: 6 | global: 7 | # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created 8 | # via the "travis encrypt" command using the project repo's public key 9 | - secure: "ZQymYx2v/NXyAnFKvgcfIYAjvCD5X/CWcW8zhfVWmz5udpyATTvZ6AHwGelboU4nbPA86OpS/A5iHKqDmYP6LOqsxLbCein1pgeh4Zbz6nOASiZZqN6X+rIdv/FXueELg/rxEXc0OPIPgQWtSLsA726o/RMCiBx9trOi4pf9R916fQ5ylfQHyZlCzR9Sg7Ga3f4Sx9KTPcGuS2dLXAKHExnYIT5cC+QkXcAs8yVSDSN31+Qx/BeZC0HOE02deyVdDLQQPu/f9B8WI7x/kvBw4PyMgAgRS3QTIFlyDDhgOTGUF7LGAfaMIrWT/0r2HGrEam+0xR7JZnzE6RYLifNi3KlDKdMgcuHlDJcxVOvQCmnznHTr6RcC0kO8VYFnxUJBkOuUbZ9jhvIEvgEaHLsiuhwEVmFTh33cB/u4NH8O70O1Zg/nQPRhL9G+x2sUL3De8xEvqfGOJ/5eUo4YzVv0Nqx4t0x141RVOSmiw2htOmkQjhgTU5g5BuKihLy4PtvH1H3MuhfA9uexedSAqeluzytMPDxOsQnDDtKZYu4m4lCNj94nn6Q/n+3Z1G9u2hgw86G3VO2zlAk0aVq1pGBRIQfTE2rmEhQbmKBQpq3mjr+hto42kq040sQ6/O/8AnE2RSGxnEnr2glJVbPRTVJdSBsK/y+13Plgt6CfgoiT6O4=" 10 | matrix: 11 | - NGINX_VERSION=1.16.0 12 | - NGINX_VERSION=1.17.2 - 13 | addons: 14 | apt: 15 | packages: 16 | - libpcre3-dev 17 | - libssl-dev 18 | - perl 19 | - cpanminus 20 | coverity_scan: 21 | project: 22 | name: "dvershinin/ngx_dynamic_etag" 23 | description: "NGINX module for adding ETag to dynamic content" 24 | build_command_prepend: "wget -O - http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -xzf - && cd nginx-${NGINX_VERSION} && ./configure --with-debug --add-module=.." 25 | build_command: "make -j2" 26 | branch_pattern: coverity_scan 27 | cache: 28 | ccache: true 29 | directories: 30 | - $HOME/perl5 31 | before_install: 32 | - test $TRAVIS_BRANCH != coverity_scan -o ${TRAVIS_JOB_NUMBER##*.} = 1 || exit 0 33 | - cpanm --notest --local-lib=$HOME/perl5 Test::Nginx 34 | install: 35 | - test $TRAVIS_BRANCH != coverity_scan || exit 0 36 | - wget -O - http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -xzf - 37 | - cd nginx-${NGINX_VERSION} 38 | - ./configure --with-debug --add-module=.. 39 | - make -j2 40 | - export PATH=$(pwd)/objs:$PATH 41 | - cd .. 42 | script: 43 | - test $TRAVIS_BRANCH != coverity_scan || exit 0 44 | - PERL5LIB=$HOME/perl5/lib/perl5 TEST_NGINX_VERBOSE=true prove -v 45 | after_failure: 46 | - cat t/servroot/conf/nginx.conf 47 | - cat t/servroot/access.log 48 | - cat t/servroot/error.log -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ciapnz@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Danila Vershinin 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx_dynamic_etag 2 | 3 | [![Build Status](https://travis-ci.org/dvershinin/ngx_dynamic_etag.svg?branch=master)](https://travis-ci.org/dvershinin/ngx_dynamic_etag) 4 | [![Coverity Scan](https://img.shields.io/coverity/scan/dvershinin-ngx_dynamic_etag)](https://scan.coverity.com/projects/dvershinin-ngx_dynamic_etag) 5 | [![Buy Me a Coffee](https://img.shields.io/badge/dynamic/json?color=blue&label=Buy%20me%20a%20Coffee&prefix=%23&query=next_time_total&url=https%3A%2F%2Fwww.getpagespeed.com%2Fbuymeacoffee.json&logo=buymeacoffee)](https://www.buymeacoffee.com/dvershinin) 6 | 7 | This NGINX module empowers your dynamic content with automatic [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) 8 | header. It allows client browsers to issue conditional `GET` requests to 9 | dynamic pages. And thus saves bandwidth and ensures better performance! 10 | 11 | ## Caveats first! 12 | 13 | This module is a real hack: it calls a header filter from a body filter, etc. 14 | 15 | The original author abandoned it, [having to say](https://github.com/kali/nginx-dynamic-etags/issues/2): 16 | 17 | > It never really worked. 18 | 19 | I largely rewrote it to deal with existing obvious faults, but the key part with buffers, 20 | which, myself being old, I probably wil l never understand, is untouched. 21 | 22 | To be reliable, the module has to read entire response and take a hash of it. 23 | Reading entire response is against NGINX lightweight design. 24 | I am not sure whether the buffer part waits for the entire response. 25 | 26 | Having said that, the tests which I added showcase that this whole stuff works! 27 | 28 | Note that the `HEAD` requests will not have any `ETag` returned, because we have no data to play with, 29 | since NGINX rightfully discards body for this request method. 30 | 31 | Consider this as a feature or a bug :-) If we remove this, then all `HEAD` requests end up having same `ETag` (hash on emptiness), 32 | which is definitely worse. 33 | 34 | Thus, be sure you check headers like this: 35 | 36 | ```bash 37 | curl -IL -X GET https://www.example.com/ 38 | ``` 39 | 40 | And not like this: 41 | 42 | ```bash 43 | curl -IL https://www.example.com/ 44 | ``` 45 | 46 | Another worthy thing to mention is that it makes little to no sense applying dynamic `ETag` on a page that changes on 47 | each reload. E.g. I found I wasn't using the dynamic `ETag` with benefits, because of ``, 48 | in my WordPress theme's `header.php`, since in this function: 49 | 50 | > the selection is random and changes each time the function is called 51 | 52 | To quickly check if your page is changing on reload, use: 53 | 54 | ```bash 55 | diff <(curl http://www.example.com") <(curl http://www.example.com") 56 | ``` 57 | 58 | Now that we're done with the "now you know" yada-yada, you can proceed with trying out this stuff :) 59 | 60 | 61 | ## Synopsis 62 | 63 | ```nginx 64 | http { 65 | server { 66 | location ~ \.php$ { 67 | dynamic_etag on; 68 | fastcgi_pass ...; 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | ## Configuration directives 75 | 76 | ### `dynamic_etag` 77 | 78 | - **syntax**: `dynamic_etag on|off|$var` 79 | - **default**: `off` 80 | - **context**: `http`, `server`, `location`, `if` 81 | 82 | Enables or disables applying ETag automatically. 83 | 84 | ### `dynamic_etag_types` 85 | 86 | - **syntax**: `dynamic_etag_types [..]` 87 | - **default**: `text/html` 88 | - **context**: `http`, `server`, `location` 89 | 90 | Enables applying ETag automatically for the specified MIME types 91 | in addition to `text/html`. The special value `*` matches any MIME type. 92 | Responses with the `text/html` MIME type are always included. 93 | 94 | ## Installation for stable NGINX 95 | 96 | Pre-compiled module packages are available for virtually any RHEL-based distro like Rocky Linux, AlmaLinux, etc. 97 | 98 | ### Any distro with `yum` 99 | 100 | ``` 101 | sudo yum -y install https://extras.getpagespeed.com/release-latest.rpm 102 | sudo yum install nginx-module-dynamic-etag 103 | ``` 104 | 105 | ### Any distro with `dnf` 106 | 107 | ```bash 108 | sudo dnf -y install https://extras.getpagespeed.com/release-latest.rpm 109 | sudo dnf install nginx-module-dynamic-etag 110 | ``` 111 | 112 | Follow the installation prompt to import GPG public key that is used for verifying packages. 113 | 114 | Then add the following at the top of your `/etc/nginx/nginx.conf`: 115 | 116 | ```nginx 117 | load_module modules/ngx_http_dynamic_etag_module.so; 118 | ``` 119 | 120 | ## Tips 121 | 122 | You can use `map` directive for conditionally enabling dynamic `ETag` based on URLs, e.g.: 123 | 124 | ```nginx 125 | map $request_uri $dyn_etag { 126 | default "off"; 127 | /foo "on"; 128 | /bar "on"; 129 | } 130 | server { 131 | ... 132 | location / { 133 | dynamic_etag $dyn_etag; 134 | fastcgi_pass ... 135 | } 136 | } 137 | ``` 138 | 139 | ## Original author's README 140 | 141 | Attempt at handling ETag / If-None-Match on proxied content. 142 | 143 | I plan on using this to front a Varnish server using a lot of ESI. 144 | 145 | It does kind of work, but... be aware, this is my first attempt at developing 146 | a nginx plugin, and dealing with headers after having read the body was not 147 | exactly in the how-to. 148 | 149 | Any comment and/or improvement and/or fork is welcome. 150 | 151 | Thanks to http://github.com/kkung/nginx-static-etags/ for... inspiration. 152 | -------------------------------------------------------------------------------- /RHEL8-Testing.md: -------------------------------------------------------------------------------- 1 | ## Getting tests suite to work in RHEL 8 2 | 3 | Files layout: 4 | 5 | * `~/Projects/nginx-stable` - NGINX core sources 6 | * `~/Projects/nginx-stable/ngx_dynamic_etag` - the module sources 7 | * `~/nginx-stable` - compiled NGINX files installed here for testing 8 | 9 | Setup test framework: 10 | 11 | sudo dnf install perl-App-cpanminus perl-local-lib 12 | # now CPAN modules can be installed as non-root: 13 | cpanm Test::Nginx::Socket 14 | 15 | Then read what you need to add to `~/.bashrc` by running `perl -Mlocal::lib`, and add it, e.g.: 16 | 17 | ``` 18 | PATH="/home/danila/perl5/bin${PATH:+:${PATH}}"; export PATH; 19 | PERL5LIB="/home/danila/perl5/lib/perl5${PERL5LIB:+:${PERL5LIB}}"; export PERL5LIB; 20 | PERL_LOCAL_LIB_ROOT="/home/danila/perl5${PERL_LOCAL_LIB_ROOT:+:${PERL_LOCAL_LIB_ROOT}}"; export PERL_LOCAL_LIB_ROOT; 21 | PERL_MB_OPT="--install_base \"/home/danila/perl5\""; export PERL_MB_OPT; 22 | PERL_MM_OPT="INSTALL_BASE=/home/danila/perl5"; export PERL_MM_OPT; 23 | ``` 24 | 25 | For testing, the module needs to be compiled in statically: 26 | 27 | ./configure --prefix=${HOME}/nginx-stable --add-module=../ngx_dynamic_etag --with-http_ssl_module 28 | 29 | ... and nginx available to your `PATH`. Considering that `.local/bin` is in your `PATH` (typical Python user), 30 | you can symlink compiled `nginx` binary in that directory: 31 | 32 | ```bash 33 | cd ~/.local/bin 34 | ln -fs ${HOME}/Projects/nginx-stable/objs/nginx ./nginx 35 | ``` 36 | 37 | * [Running Tests](https://openresty.gitbooks.io/programming-openresty/content/testing/running-tests.html) 38 | * [ngx-releng utility](https://github.com/openresty/openresty-devel-utils/blob/master/ngx-releng) 39 | * [tests reindex](https://github.com/openresty/openresty-devel-utils/blob/master/reindex) -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_dynamic_etag_module 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP_INIT_FILTER 5 | ngx_module_name=ngx_http_dynamic_etag_module 6 | ngx_module_srcs="$ngx_addon_dir/src/ngx_http_dynamic_etag_module.c" 7 | 8 | . auto/module 9 | else 10 | HTTP_MODULES="$HTTP_MODULES ngx_http_dynamic_etag_module" 11 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/src/ngx_http_dynamic_etag_module.c" 12 | fi -------------------------------------------------------------------------------- /src/ngx_http_dynamic_etag_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Danila Vershinin ( https://www.getpagespeed.com/ ) 3 | * Copyright (c) 2009 Mathieu Poumeyrol ( http://github.com/kali ) 4 | * 5 | * All rights reserved. 6 | * All original code was written by Mike West ( http://mikewest.org/ ) and 7 | Adrian Jung ( http://me2day.net/kkung, kkungkkung@gmail.com ). 8 | * 9 | * Copyright 2008 Mike West ( http://mikewest.org/ ) 10 | * Copyright 2009 Adrian Jung ( http://me2day.net/kkung, kkungkkung@gmail.com ). 11 | * 12 | * The following is released under the Creative Commons BSD license, 13 | * available for your perusal at `http://creativecommons.org/licenses/BSD/` 14 | */ 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | typedef struct { 21 | ngx_uint_t enable; 22 | ngx_http_complex_value_t enable_value; 23 | ngx_hash_t types; 24 | ngx_array_t *types_keys; 25 | } ngx_http_dynamic_etag_loc_conf_t; 26 | 27 | typedef struct { 28 | ngx_flag_t done; 29 | } ngx_http_dynamic_etag_module_ctx_t; 30 | 31 | static ngx_http_output_header_filter_pt ngx_http_next_header_filter; 32 | static ngx_http_output_body_filter_pt ngx_http_next_body_filter; 33 | 34 | static void *ngx_http_dynamic_etag_create_loc_conf(ngx_conf_t *cf); 35 | static char *ngx_http_dynamic_etag_merge_loc_conf(ngx_conf_t *cf, void *parent, 36 | void *child); 37 | static ngx_int_t ngx_http_dynamic_etag_init(ngx_conf_t *cf); 38 | static ngx_int_t ngx_http_dynamic_etag_header_filter(ngx_http_request_t *r); 39 | static ngx_int_t ngx_http_dynamic_etag_body_filter(ngx_http_request_t *r, 40 | ngx_chain_t *in); 41 | static char *ngx_http_dynamic_etag_enable(ngx_conf_t *cf, ngx_command_t *cmd, 42 | void *conf); 43 | 44 | static ngx_command_t ngx_http_dynamic_etag_commands[] = { 45 | 46 | { ngx_string( "dynamic_etag" ), 47 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 48 | ngx_http_dynamic_etag_enable, 49 | NGX_HTTP_LOC_CONF_OFFSET, 50 | 0, 51 | NULL }, 52 | 53 | { ngx_string("dynamic_etag_types"), 54 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, 55 | ngx_http_types_slot, 56 | NGX_HTTP_LOC_CONF_OFFSET, 57 | offsetof(ngx_http_dynamic_etag_loc_conf_t, types_keys), 58 | &ngx_http_html_default_types[0] }, 59 | 60 | ngx_null_command 61 | }; 62 | 63 | 64 | 65 | static ngx_http_module_t ngx_http_dynamic_etag_module_ctx = { 66 | NULL, /* preconfiguration */ 67 | ngx_http_dynamic_etag_init, /* postconfiguration */ 68 | 69 | NULL, /* create main configuration */ 70 | NULL, /* init main configuration */ 71 | 72 | NULL, /* create server configuration */ 73 | NULL, /* merge server configuration */ 74 | 75 | ngx_http_dynamic_etag_create_loc_conf, /* create location configuration */ 76 | ngx_http_dynamic_etag_merge_loc_conf, /* merge location configuration */ 77 | }; 78 | 79 | ngx_module_t ngx_http_dynamic_etag_module = { 80 | NGX_MODULE_V1, 81 | &ngx_http_dynamic_etag_module_ctx, /* module context */ 82 | ngx_http_dynamic_etag_commands, /* module directives */ 83 | NGX_HTTP_MODULE, /* module type */ 84 | NULL, /* init master */ 85 | NULL, /* init module */ 86 | NULL, /* init process */ 87 | NULL, /* init thread */ 88 | NULL, /* exit thread */ 89 | NULL, /* exit process */ 90 | NULL, /* exit master */ 91 | NGX_MODULE_V1_PADDING 92 | }; 93 | 94 | static void * 95 | ngx_http_dynamic_etag_create_loc_conf(ngx_conf_t *cf) 96 | { 97 | ngx_http_dynamic_etag_loc_conf_t *conf; 98 | 99 | conf = ngx_pcalloc( cf->pool, sizeof( ngx_http_dynamic_etag_loc_conf_t ) ); 100 | if (conf == NULL) { 101 | return NULL; 102 | } 103 | /* 104 | * set by ngx_pcalloc(): 105 | * 106 | * conf->types = { NULL }; 107 | * conf->types_keys = NULL; 108 | */ 109 | conf->enable = NGX_CONF_UNSET_UINT; 110 | return conf; 111 | } 112 | 113 | static char * 114 | ngx_http_dynamic_etag_merge_loc_conf(ngx_conf_t *cf, void *parent, 115 | void *child) 116 | { 117 | ngx_http_dynamic_etag_loc_conf_t *prev = parent; 118 | ngx_http_dynamic_etag_loc_conf_t *conf = child; 119 | 120 | if (conf->enable == NGX_CONF_UNSET_UINT) { 121 | ngx_conf_merge_uint_value(conf->enable, prev->enable, 0); 122 | conf->enable_value = prev->enable_value; 123 | } 124 | 125 | if (ngx_http_merge_types(cf, &conf->types_keys, &conf->types, 126 | &prev->types_keys, &prev->types, 127 | ngx_http_html_default_types) 128 | != NGX_OK) 129 | { 130 | return NGX_CONF_ERROR; 131 | } 132 | 133 | return NGX_CONF_OK; 134 | } 135 | 136 | static ngx_int_t 137 | ngx_http_dynamic_etag_init(ngx_conf_t *cf) 138 | { 139 | ngx_http_next_header_filter = ngx_http_top_header_filter; 140 | ngx_http_top_header_filter = ngx_http_dynamic_etag_header_filter; 141 | 142 | ngx_http_next_body_filter = ngx_http_top_body_filter; 143 | ngx_http_top_body_filter = ngx_http_dynamic_etag_body_filter; 144 | 145 | return NGX_OK; 146 | } 147 | 148 | static ngx_int_t 149 | ngx_http_dynamic_etag_header_filter(ngx_http_request_t *r) 150 | { 151 | 152 | ngx_http_dynamic_etag_module_ctx_t *ctx; 153 | ngx_http_dynamic_etag_loc_conf_t *conf; 154 | ngx_str_t enable; 155 | 156 | conf = ngx_http_get_module_loc_conf(r, ngx_http_dynamic_etag_module); 157 | 158 | // Skip processing if not fetching from upstream (e.g., FastCGI) 159 | if (r->upstream == NULL) { 160 | return ngx_http_next_header_filter(r); 161 | } 162 | 163 | if (conf->enable == 0 || r->method & NGX_HTTP_HEAD) { 164 | return ngx_http_next_header_filter(r); 165 | } else if (conf->enable == 2) { 166 | if (ngx_http_complex_value(r, &conf->enable_value, &enable) != NGX_OK) { 167 | return NGX_ERROR; 168 | } 169 | if (enable.len == 3 170 | && ngx_strncmp(enable.data, "off", 3) == 0) 171 | { 172 | return ngx_http_next_header_filter(r); 173 | } 174 | } 175 | 176 | if (r->headers_out.status != NGX_HTTP_OK 177 | || ngx_http_test_content_type(r, &conf->types) == NULL 178 | || r != r->main) 179 | { 180 | return ngx_http_next_header_filter(r); 181 | } 182 | 183 | ctx = ngx_http_get_module_ctx(r, ngx_http_dynamic_etag_module); 184 | 185 | if (ctx) { 186 | return ngx_http_next_header_filter(r); 187 | } 188 | 189 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_dynamic_etag_module_ctx_t)); 190 | if (ctx == NULL) { 191 | return NGX_ERROR; 192 | } 193 | 194 | ngx_http_set_ctx(r, ctx, ngx_http_dynamic_etag_module); 195 | 196 | r->main_filter_need_in_memory = 1; 197 | r->filter_need_in_memory = 1; 198 | 199 | /* Make sure that ngx_http_not_modified_filter_module does its stuff */ 200 | r->disable_not_modified = 0; 201 | 202 | return NGX_OK; 203 | } 204 | 205 | static u_char hex[] = "0123456789abcdef"; 206 | 207 | static ngx_int_t 208 | ngx_http_dynamic_etag_body_filter(ngx_http_request_t *r, ngx_chain_t *in) 209 | { 210 | ngx_chain_t *chain_link; 211 | ngx_http_dynamic_etag_module_ctx_t *ctx; 212 | 213 | ngx_int_t rc; 214 | ngx_md5_t md5; 215 | unsigned char digest[16]; 216 | ngx_uint_t i; 217 | 218 | // If the response is not from the upstream, skip processing 219 | if (r->upstream == NULL) { 220 | return ngx_http_next_body_filter(r, in); 221 | } 222 | 223 | ctx = ngx_http_get_module_ctx(r, ngx_http_dynamic_etag_module); 224 | if (ctx == NULL) { 225 | return ngx_http_next_body_filter(r, in); 226 | } 227 | 228 | ngx_http_dynamic_etag_loc_conf_t *conf; 229 | ngx_str_t enable; 230 | 231 | conf = ngx_http_get_module_loc_conf(r, ngx_http_dynamic_etag_module); 232 | 233 | 234 | if (conf->enable == 2) { 235 | if (ngx_http_complex_value(r, &conf->enable_value, &enable) 236 | != NGX_OK) 237 | { 238 | return NGX_ERROR; 239 | } 240 | } 241 | 242 | 243 | if (1 == conf->enable 244 | || (conf->enable == 2 && enable.len == 2 245 | && ngx_strncmp(enable.data, "on", 2) == 0)) 246 | { 247 | if (!r->headers_out.etag) { 248 | ngx_md5_init(&md5); 249 | for (chain_link = in; chain_link; chain_link = chain_link->next) { 250 | ngx_md5_update(&md5, 251 | chain_link->buf->pos, 252 | chain_link->buf->last - chain_link->buf->pos); 253 | } 254 | ngx_md5_final(digest, &md5); 255 | 256 | unsigned char * etag = ngx_pcalloc(r->pool, 34); 257 | if (etag == NULL) { 258 | return NGX_ERROR; 259 | } 260 | etag[0] = etag[33] = '"'; 261 | for ( i = 0 ; i < 16; i++ ) { 262 | etag[2 * i + 1] = hex[digest[i] >> 4]; 263 | etag[2 * i + 2] = hex[digest[i] & 0xf]; 264 | } 265 | 266 | r->headers_out.etag = ngx_list_push(&r->headers_out.headers); 267 | if (r->headers_out.etag == NULL) { 268 | return NGX_ERROR; 269 | } 270 | r->headers_out.etag->hash = 1; 271 | r->headers_out.etag->key.len = sizeof("ETag") - 1; 272 | r->headers_out.etag->key.data = (u_char *) "ETag"; 273 | r->headers_out.etag->value.len = 34; 274 | r->headers_out.etag->value.data = etag; 275 | } 276 | } 277 | 278 | 279 | rc = ngx_http_next_header_filter(r); 280 | if (rc == NGX_ERROR || rc > NGX_OK) { 281 | return NGX_ERROR; 282 | } 283 | 284 | ngx_http_set_ctx(r, NULL, ngx_http_dynamic_etag_module); 285 | 286 | return ngx_http_next_body_filter(r, in); 287 | } 288 | 289 | static char * 290 | ngx_http_dynamic_etag_enable(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 291 | { 292 | ngx_http_dynamic_etag_loc_conf_t *clcf = conf; 293 | 294 | ngx_str_t *value; 295 | ngx_http_compile_complex_value_t ccv; 296 | 297 | if (clcf->enable != NGX_CONF_UNSET_UINT) { 298 | return "is duplicate"; 299 | } 300 | 301 | value = cf->args->elts; 302 | 303 | if (ngx_strcmp(value[1].data, "on") == 0) { 304 | clcf->enable = 1; 305 | return NGX_CONF_OK; 306 | } else if (ngx_strcmp(value[1].data, "off") == 0) { 307 | clcf->enable = 0; 308 | return NGX_CONF_OK; 309 | } else if (ngx_strncmp(value[1].data, "$", 1) != 0) { 310 | return "should be either on, off, or a $variable"; 311 | } 312 | 313 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 314 | 315 | ccv.cf = cf; 316 | ccv.value = &value[1]; 317 | ccv.complex_value = &clcf->enable_value; 318 | 319 | if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { 320 | return NGX_CONF_ERROR; 321 | } 322 | 323 | /* variable */ 324 | clcf->enable = 2; 325 | 326 | return NGX_CONF_OK; 327 | } -------------------------------------------------------------------------------- /t/config.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | 3 | run_tests(); 4 | 5 | __DATA__ 6 | 7 | === TEST 1: dying on bad config 8 | --- http_config 9 | dynamic_etag bad; 10 | --- config 11 | --- must_die 12 | --- error_log 13 | directive should be either on, off, or a $variable -------------------------------------------------------------------------------- /t/etag.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | 3 | run_tests(); 4 | 5 | __DATA__ 6 | 7 | === TEST 1: etag with proxy_pass 8 | --- config 9 | location = /hello { 10 | return 200 "hello world\n"; 11 | } 12 | location = /hello-proxy { 13 | dynamic_etag on; 14 | dynamic_etag_types text/plain; 15 | proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello; 16 | } 17 | --- request 18 | GET /hello-proxy 19 | --- response_body 20 | hello world 21 | --- response_headers 22 | ETag: "6f5902ac237024bdd0c176cb93063dc4" 23 | 24 | 25 | 26 | === TEST 2: etag with proxy_pass differs 27 | --- config 28 | location = /hello { 29 | return 200 "hello earth\n"; 30 | } 31 | location = /hello-proxy { 32 | dynamic_etag on; 33 | dynamic_etag_types text/plain; 34 | proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello; 35 | } 36 | --- request 37 | GET /hello-proxy 38 | --- response_body 39 | hello earth 40 | --- response_headers 41 | ETag: "e5e0da9cf469b4842019c15e3ca531d1" 42 | 43 | 44 | 45 | === TEST 3: conditional get 46 | --- config 47 | location = /hello { 48 | return 200 "hello world\n"; 49 | } 50 | location = /hello-proxy { 51 | dynamic_etag on; 52 | dynamic_etag_types text/plain; 53 | proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello; 54 | } 55 | --- error_code: 304 56 | --- request 57 | GET /hello-proxy 58 | --- more_headers 59 | If-None-Match: "6f5902ac237024bdd0c176cb93063dc4" 60 | --- response_body 61 | 62 | 63 | 64 | === TEST 4: etag with proxy_pass + proxy_buffering 65 | --- config 66 | location = /hello { 67 | return 200 "hello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earth\n"; 68 | } 69 | location = /hello-proxy { 70 | dynamic_etag on; 71 | dynamic_etag_types text/plain; 72 | proxy_buffering off; 73 | proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello; 74 | } 75 | --- request 76 | GET /hello-proxy 77 | --- response_headers 78 | ETag: "0ada7fc2e9c81a3699a0ab65bea60f54" 79 | 80 | 81 | 82 | === TEST 5: etag with head absent as we have no data to play with 83 | --- config 84 | location = /hello { 85 | return 200 "hello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earthhello earth2\n"; 86 | } 87 | location = /hello-proxy { 88 | dynamic_etag on; 89 | dynamic_etag_types text/plain; 90 | proxy_buffering off; 91 | proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello; 92 | } 93 | --- request 94 | HEAD /hello-proxy 95 | --- response_headers 96 | !ETag 97 | --------------------------------------------------------------------------------