├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ci ├── Dockerfile ├── port-4777-app.conf ├── port-4778-app.conf ├── port-4779-app.conf ├── port-4780-app.conf └── port-4781-app.conf ├── launch-test-nginx.sh ├── run-tests.sh ├── src ├── lua-ssi-content.lua └── lua-ssi-header.lua └── tests ├── big_file.txt ├── big_file └── index.json ├── broken_json_include.txt ├── broken_json_include ├── broken_sub_resource.json └── index.json ├── broken_json_include_with_percent.txt ├── broken_json_include_with_percent ├── index.json └── sub_resource.json ├── broken_json_include_without_inline.sh ├── broken_json_include_without_inline.txt ├── content_type_of_json_error.sh ├── content_type_of_json_error.txt ├── count_root_request_without_cache_control.sh ├── count_root_request_without_cache_control.txt ├── count_subrequests_without_cache_control.sh ├── count_subrequests_without_cache_control.txt ├── debug_no_include.sh ├── debug_no_include.txt ├── debug_ssi_expires_stale.sh ├── debug_ssi_expires_stale.txt ├── dont_minimize_max_age.sh ├── dont_minimize_max_age.txt ├── echo.sh ├── echo.txt ├── echo_custom_header.sh ├── echo_custom_header.txt ├── echo_method.sh ├── echo_method.txt ├── etag_check.sh ├── etag_check.txt ├── excluded_content_type.txt ├── excluded_content_type └── index.csv ├── gzip.txt ├── gzip ├── drooter.html ├── footer.html ├── index.html └── zwooter.html ├── image.txt ├── image └── cc-public-domain-mark.png ├── json.txt ├── json └── index.json ├── json_include.txt ├── json_include ├── index.json ├── sub_resource.json └── sub_sub_resource.json ├── json_include_bad_gateway.txt ├── json_include_bad_gateway └── index.json ├── json_include_bad_gateway_without_inline.sh ├── json_include_bad_gateway_without_inline.txt ├── json_include_with_percent.txt ├── json_include_with_percent ├── index.json └── sub_resource.json ├── json_virtual_include.txt ├── json_virtual_include ├── index.json ├── sub_resource.json └── sub_sub_resource.json ├── max-age ├── include-age-5-and-cache-control-10-and-15-expires-in-30.json ├── include-broken-max-age-value-and-expires-in-30.json ├── include-stale-expires-in-120.json ├── include-without-cache-control-expires-in-30.json ├── includes-30-max-age-35-age-40-swr-expires-in-120.json ├── no-cache-control.json └── ‪includes-35-max-age-30-age-40-swr-expires-in-120.json‬ ├── no_not_modified_if_not_200.sh ├── no_not_modified_if_not_200.txt ├── no_not_modified_if_not_200 ├── index.json └── sub_resource.json ├── not_modified_check_on_json_include.sh ├── not_modified_check_on_json_include.txt ├── one.txt ├── one ├── drooter.html ├── footer.html ├── index.html └── zwooter.html ├── parse_cache_control.lua ├── parse_cache_control.txt ├── recursion_cap.txt ├── recursion_cap ├── index.json ├── sub_resource.json └── sub_sub_resource.json ├── recursion_cap_depth.txt ├── recursion_cap_depth ├── index.json ├── sub_resource.json └── sub_sub_resource.json ├── relative_ssi_path.txt ├── relative_ssi_path └── index.json ├── response_of_status_409.sh ├── response_of_status_409.txt ├── status_401.sh ├── status_401.txt ├── status_409.sh ├── status_409.txt ├── status_500.sh ├── status_500.txt ├── use_max_age_0_if_max_age_is_broken_on_root.sh ├── use_max_age_0_if_max_age_is_broken_on_root.txt ├── use_max_age_0_if_max_age_is_broken_on_sub.sh ├── use_max_age_0_if_max_age_is_broken_on_sub.txt ├── use_minimized_max_age_and_swr.sh ├── use_minimized_max_age_and_swr.txt ├── use_minimized_max_age_and_swr_do_not_override_swr.sh ├── use_minimized_max_age_and_swr_do_not_override_swr.txt ├── use_minimum_max_age.sh └── use_minimum_max_age.txt /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests on Ubuntu 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | 8 | build: 9 | 10 | runs-on: ubuntu-20.04 11 | 12 | strategy: 13 | matrix: 14 | include: 15 | - nginxVersion: 1.13.3-1 16 | - nginxVersion: 1.12.0-1 17 | - nginxVersion: 1.11.9-0 18 | - nginxVersion: 1.11.5-0 19 | - nginxVersion: 1.10.1-3 20 | - nginxVersion: 1.10.1-0 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Launch Nginx 25 | env: 26 | NGINX_VERSION: ${{ matrix.nginxVersion }} 27 | run: ./launch-test-nginx.sh daemon $NGINX_VERSION 28 | - name: Run Tests 29 | env: 30 | NGINX_VERSION: ${{ matrix.nginxVersion }} 31 | run: bash run-tests.sh 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Dockerfile 2 | /tests/*.result 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # lua-native-ssi-nginx CHANGELOG 2 | 3 | ## 1.7.0 4 | 5 | * Added Header `X-Ssi-Missing-CC-Count` to count the amount of missing cache control headers 6 | * Added log if cache-control was missing but cache minimizing is activated 7 | 8 | ## 1.6.0 9 | 10 | - added `X-Ssi-Debug: true` request header to debug max age minimize behaviour 11 | - fixed `nocache` to `no-cache` in `cache-control` header 12 | 13 | ## 1.5.0 14 | 15 | - added config to override stale-while-revalidate for minimized cache 16 | - handle stale-while-revalidate when minimizing max-cache 17 | - log subrequest url in debug log 18 | 19 | ## 1.4.2 20 | 21 | - set age to 0 if cache control max age is minizimed 22 | 23 | ## 1.4.1 24 | 25 | - allow spaces around cache control directives 26 | 27 | ## 1.4.0 28 | 29 | - added possibility to minimize `max-age` of the response by `age` and `max-age` of all sub requests 30 | 31 | ## 1.3.0 32 | 33 | - render ssi error, if relative path is in ssi include 34 | 35 | ## 1.2.0 36 | 37 | - fixed internal server error on percent in url or message 38 | 39 | ## 1.1.0 40 | 41 | - added explanation for `proxy_max_temp_file_size 0` vs `proxy_buffering on` 42 | - removed `always_forward_body` because it does not always send all data to the first request 43 | 44 | ## 1.0.3 45 | 46 | - fixed url in inline json validation response 47 | 48 | ## 1.0.2 49 | 50 | - added recursion handling (depth and max includes) 51 | 52 | ## 1.0.1 53 | 54 | - handle header-only invocations (don't crash on empty `ngx.ctx.res`) 55 | - removed necessity for `lua_need_request_body on` in nginx 56 | - forward request body in a native way now (with `always_forward_body`) 57 | 58 | ## 1.0.0 59 | 60 | - initial release 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | lua-native-ssi-nginx is licensed under the terms of MIT License. 2 | 3 | Copyright (c) 2016 by DracoBlue (JanS@DracoBlue.de) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-native-ssi-nginx 2 | 3 | * Latest Release: [![GitHub version](https://badge.fury.io/gh/DracoBlue%2Flua-native-ssi-nginx.png)](https://github.com/DracoBlue/lua-native-ssi-nginx/releases) 4 | * Build Status: [![Build Status](https://github.com/dracoblue/lua-native-ssi-nginx/actions/workflows/run-tests.yml/badge.svg?branch=master)](https://github.com/DracoBlue/lua-native-ssi-nginx/actions/workflows/run-tests.yml) 5 | 6 | This is an effort to replace nginx's c ssi implementation with a flexible native lua based version, since nginx ssi does 7 | [not](https://github.com/openresty/lua-nginx-module#mixing-with-ssi-not-supported) work with the lua module. 8 | 9 | This solution has some advantages over the c ssi version: 10 | 11 | * it allows regexp for ssi types (because there are [no wildcards](http://stackoverflow.com/questions/34392175/using-gzip-types-ssi-types-in-nginx-with-wildcard-media-types) in c ssi_types) 12 | * it works with lua module 13 | * for `200 OK` responses it generates and handles etags based on md5 *after* all ssi includes have been performed 14 | * it handles and sanitizes invalid json in subrequests (inline or as summary) 15 | * it handles **only**: `` and `` and no other ssi features 16 | * it minimizes `max-age` of `Cache-Control` to the lowest value 17 | 18 | ## Usage 19 | 20 | If you started with a location like this: 21 | 22 | ``` txt 23 | location / { 24 | proxy_pass http://127.0.0.1:4777; 25 | # add your proxy_* parameters and so on here 26 | } 27 | ``` 28 | 29 | you have to replace it with something like this: 30 | 31 | ``` txt 32 | location /ssi-api-gateway/ { 33 | internal; 34 | rewrite ^/ssi-api-gateway/(.*)$ /$1 break; 35 | proxy_pass http://127.0.0.1:4777; 36 | # add your proxy_* parameters and so on here 37 | } 38 | 39 | location / { 40 | set $ssi_api_gateway_prefix "/ssi-api-gateway"; 41 | set $ssi_validate_json_types "application/json application/.*json"; 42 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 43 | content_by_lua_file "/etc/nginx/lua-ssi-content.lua"; 44 | header_filter_by_lua_file "/etc/nginx/lua-ssi-header.lua"; 45 | } 46 | ``` 47 | 48 | The `ssi-api-gateway` location is necessary to use e.g. nginx's caching layer and such things. 49 | 50 | ## Activate SSI only for specific content types 51 | 52 | If you want to enable ssi only for specific content types, use the following nginx configuration variable in the nginx 53 | location: 54 | 55 | ``` txt 56 | set $ssi_types "text/.*html application/.*json"; 57 | ``` 58 | 59 | The default is: 60 | 61 | ``` txt 62 | set $ssi_types ".*"; 63 | ``` 64 | 65 | 66 | ## Activate JSON Summary Validation 67 | 68 | **Prerequisites**: Install cjson (e.g. `apt-get install lua-cjson` to activate this feature. Otherwise you get the following message: 69 | `Even though ssi_validate_json is true, the cjson library is not installed! Skip validation!`. 70 | 71 | If you want to ensure, that subrequested json is always valid, you can activate this in the nginx location: 72 | 73 | ``` txt 74 | set $ssi_validate_json_types "application/json application/.*json"; 75 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 76 | ``` 77 | 78 | If you setup the configuration like this, the following ssi: 79 | 80 | ``` txt 81 | GET /broken_json_include/ 82 | {"thisIsThe": "index", "sub_resources": [] } 83 | 84 | GET /broken_json_include/broken_sub_resource.json 85 | {"thisIsA": "subResource", "with invalud json} 86 | ``` 87 | 88 | 89 | will result in the following valid json response: 90 | 91 | ``` json 92 | { 93 | "error": "invalid json", 94 | "brokenSsiRequests": [ 95 | { 96 | "url": "\/broken_json_include\/broken_sub_resource.json", 97 | "message": "Expected object key string but found unexpected end of string at character 47" 98 | } 99 | ], 100 | "message": "Expected object key string but found unexpected end of string at character 91","url":"\/broken_json_include\/" 101 | } 102 | ``` 103 | 104 | ## Activate JSON Inline Validation 105 | 106 | If you don't want to replace the entire SSI response with an error summary (like in the previous section), you can add: 107 | 108 | ``` 109 | set $ssi_validate_json_inline on; 110 | ``` 111 | 112 | and only the broken SSI will be replaced with the `$ssi_invalid_json_fallback`. 113 | 114 | **Important**: Please don't forget to define `$ssi_validate_json_types` and `$ssi_invalid_json_fallback` like described 115 | in the previous section!. 116 | 117 | So: 118 | 119 | ``` txt 120 | GET /broken_json_include/ 121 | {"thisIsThe": "index", "sub_resources": [] } 122 | 123 | GET /broken_json_include/broken_sub_resource.json 124 | {"thisIsA": "subResource", "with invalud json} 125 | ``` 126 | 127 | will result in the following valid json response: 128 | 129 | ``` json 130 | { 131 | "thisIsThe": "index", 132 | "sub_resources": [ 133 | { 134 | "error": "invalid json", 135 | "url": "/broken_json_include/", 136 | "message": "Expected object key string but found unexpected end of string at character 47" 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | ## Limit recursion depth 143 | 144 | The default values for the maximum depth (1024) and the maximum amount of includes (65535) can be changed with the following 145 | configuration parameters: 146 | 147 | ``` 148 | set $ssi_max_includes 512; 149 | set $ssi_max_ssi_depth 16; 150 | ``` 151 | 152 | If the limit is exceeded, the ssi will be replaced with: 153 | 154 | ``` json 155 | { 156 | "error": "invalid json", 157 | "url": "\/recursion_cap_depth\/sub_resource.json", 158 | "message": "max recursion depth exceeded 16(was 17)" 159 | } 160 | ``` 161 | 162 | or 163 | 164 | ``` json 165 | { 166 | "error": "invalid json", 167 | "url": "\/recursion_cap\/sub_resource.json", 168 | "message": "max ssi includes exceeded 512(was 728)" 169 | } 170 | ``` 171 | 172 | 173 | You can change the response with: 174 | 175 | ``` json 176 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 177 | ``` 178 | 179 | ## Minimize `max-age` in `Cache-Control` 180 | 181 | You can calculate the lowest `max-age` of the root document and all sub resources and return the lowest value. Additionally 182 | it takes the `age` response header of the sub resources into account and decreases the `max-age` by this value. This 183 | feature is opt-in only and you can activate it like this: 184 | 185 | ``` txt 186 | set $ssi_minimize_max_age on; 187 | ``` 188 | 189 | The default is: 190 | 191 | ``` txt 192 | set $ssi_minimize_max_age off; 193 | ``` 194 | 195 | An example: 196 | 197 | /users (max-age=60, age=0 -> ttl=60), includes: 198 | -> /users/1 (max-age=10, age=7 -> ttl=3) 199 | -> /users/2 (max-age=5, age=0 -> ttl=5) 200 | 201 | will return in `max-age=3` since 3 is the lowest ttl and thus the `max-age` value for the entire request. 202 | 203 | **Important**: If you activate this feature, all other Cache-Control directives will be removed and only `Cache-Control: max-age=300` 204 | (if the minimum max-age was 300) or `Cache-Control: max-age=0, no-cache` (if the minimum was negative) will be served. 205 | Additional Cache-Control features like `stale-while-revalidate` or `stale-if-error` will be removed. 206 | 207 | Invalid max-age values will be replaced with `Cache-Control: no-cache, max-age=0`. 208 | 209 | Additonally you may use: 210 | 211 | ``` 212 | set $ssi_minimize_override_stale_while_revalidate 5; 213 | ``` 214 | 215 | to append `stale-while-revalidate=5` to each `Cache-Control` header with `max-age` greater than 0. 216 | 217 | Starting with Version 1.7.0: If no `Cache-Control` header is send by subrequest, it will be handled like `max-age=0`. There is a header called `X-Ssi-Missing-CC-Count: 2`, 218 | which makes the amount of subrequests missing a cache control header in this request. It will also appear in the log like this: 219 | 220 | ``` 221 | 2022/01/30 09:17:05 [error] 7#7: *487 [lua] lua-ssi-content.lua:301: missing cache control on sub request url: /ssi-api-gateway/max-age/no-cache-control.json while sending to client, client: 172.17.0.1, server: , request: "GET /max-age/include-without-cache-control-expires-in-30.json HTTP/1.1", host: "localhost:4778" 222 | ``` 223 | 224 | and on root it looks like this: 225 | 226 | ``` 227 | 2022/01/30 09:17:05 [error] 7#7: *487 [lua] lua-ssi-content.lua:301: missing cache control on root request url: /ssi-api-gateway/max-age/no-cache-control.json while sending to client, client: 172.17.0.1, server: , request: "GET /max-age/no-cache-control.json HTTP/1.1", host: "localhost:4778" 228 | ``` 229 | 230 | 231 | ## Debug Output 232 | 233 | If you want to debug the lua ssi output or calculations, you should enable the debug log in nginx. 234 | 235 | If you need to debug one request and don't want to enable debug log for the entire server, you can send a special request 236 | header called `X-Ssi-Debug: true`. 237 | 238 | $ curl -v -sS -H 'X-Ssi-Debug: true' "http://localhost:4778/max-age/include-stale-expires-in-120.json" 2>&1 239 | < X-Ssi-Minimize-MaxAge-Url: /max-age/35-seconds/30-age/40-swr 240 | < X-Ssi-Minimize-MaxAge-Age: 45 241 | < X-Ssi-Minimize-MaxAge-Cache-Control: max-age=35, stale-while-revalidate=40 242 | 243 | If you apply the `X-Ssi-Debug: true` request header, you will receive extra `X-Ssi-Minimize`-prefixed response headers. 244 | In those headers you can see, which of the ssi requests was the one, which resulted in the final max-age of the 245 | `Cache-Control` response header. 246 | 247 | ## Development 248 | 249 | To run the tests locally launch: 250 | 251 | ``` console 252 | $ ./launch-test-nginx.sh 253 | ... 254 | Successfully built 72a844684987 255 | 2016/09/11 11:34:02 [alert] 1#0: lua_code_cache is off; this will hurt performance in /etc/nginx/sites-enabled/port-4778-app.conf:12 256 | nginx: [alert] lua_code_cache is off; this will hurt performance in /etc/nginx/sites-enabled/port-4778-app.conf:12 257 | ``` 258 | 259 | Now the nginx processes are running with docker. 260 | 261 | Now you can run the tests like this: 262 | 263 | ``` console 264 | $ ./run-tests.sh 265 | [OK] echo 266 | [OK] echo_custom_header 267 | [OK] echo_method 268 | [OK] gzip 269 | [OK] image 270 | [OK] json 271 | [OK] json_include 272 | [OK] one 273 | ``` 274 | 275 | ## FAQ 276 | 277 | ### Subrequests hang when used with lua native ssi 278 | 279 | The following setup is often used, to avoid buffering on disk: 280 | 281 | ``` text 282 | proxy_buffer_size 16k; 283 | proxy_buffering on; 284 | proxy_max_temp_file_size 0; 285 | ``` 286 | 287 | but it will result in hanging requests, if the response size is bigger then 16k. 288 | 289 | That's why you should either use (means: disable buffering at all): 290 | 291 | ``` text 292 | proxy_buffer_size 16k; 293 | proxy_buffering off; 294 | ``` 295 | 296 | or (means: store up to 1024m in temp file) 297 | 298 | ``` text 299 | proxy_buffer_size 16k; 300 | proxy_buffering on; 301 | proxy_max_temp_file_size 1024m; 302 | ``` 303 | 304 | to work around this issue. 305 | 306 | 307 | ## TODOs 308 | 309 | See for all open TODOs. 310 | 311 | ## Changelog 312 | 313 | See [CHANGELOG.md](./CHANGELOG.md). 314 | 315 | ## License 316 | 317 | This work is copyright by DracoBlue () and licensed under the terms of MIT License. 318 | -------------------------------------------------------------------------------- /ci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dracoblue/nginx-extras:1.10.1-3 2 | RUN apt-get update 3 | RUN apt-get install lua-cjson 4 | ADD ci/port-4777-app.conf /etc/nginx/sites-enabled/port-4777-app.conf 5 | ADD ci/port-4778-app.conf /etc/nginx/sites-enabled/port-4778-app.conf 6 | ADD ci/port-4779-app.conf /etc/nginx/sites-enabled/port-4779-app.conf 7 | ADD ci/port-4780-app.conf /etc/nginx/sites-enabled/port-4780-app.conf 8 | ADD ci/port-4781-app.conf /etc/nginx/sites-enabled/port-4781-app.conf 9 | ADD src/lua-ssi-content.lua /etc/nginx/lua-ssi-content.lua 10 | ADD src/lua-ssi-header.lua /etc/nginx/lua-ssi-header.lua 11 | ADD tests /var/www/html 12 | EXPOSE 4781 13 | EXPOSE 4780 14 | EXPOSE 4779 15 | EXPOSE 4778 16 | EXPOSE 4777 17 | -------------------------------------------------------------------------------- /ci/port-4777-app.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 4777; 4 | 5 | location / { 6 | root /var/www/html; 7 | index index.html index.json index.csv; 8 | expires epoch; 9 | } 10 | 11 | location ~ ^/max-age/(\d+)-seconds/(\d+)-age/(\d+)-swr { 12 | root /var/www/html; 13 | index index.html index.json index.csv; 14 | add_header "Age" "$2"; 15 | add_header "Cache-Control" "max-age=$1, stale-while-revalidate=$3"; 16 | default_type application/json; 17 | return 200 "{\"message\": \"I last for $1 seconds and stale-while-revalidate for $3 seconds (and have an age of $2 seconds)\"}"; 18 | } 19 | 20 | location ~ ^/max-age/(\d+)-seconds/(\d+)-age { 21 | root /var/www/html; 22 | index index.html index.json index.csv; 23 | expires $1s; 24 | add_header "Age" "$2"; 25 | default_type application/json; 26 | return 200 "{\"message\": \"I last for $1 seconds (and have an age of $2 seconds)\"}"; 27 | } 28 | 29 | location ~ ^/max-age/(\d+)-seconds { 30 | root /var/www/html; 31 | index index.html index.json index.csv; 32 | expires $1s; 33 | default_type application/json; 34 | return 200 "{\"message\": \"I last for $1 seconds\"}"; 35 | } 36 | 37 | location ~ ^/max-age/.+-expires-in-(\d+).json { 38 | root /var/www/html; 39 | index index.html index.json index.csv; 40 | expires $1s; 41 | } 42 | 43 | location /max-age/broken-max-age-value { 44 | root /var/www/html; 45 | index index.html index.json index.csv; 46 | add_header "Cache-Control" "max-age=hans"; 47 | add_header "Age" "10"; 48 | default_type application/json; 49 | return 200 "{\"message\": \"I last for hans seconds (and have an age of 10 seconds)\"}"; 50 | } 51 | 52 | 53 | location /max-age/ { 54 | root /var/www/html; 55 | index index.html index.json index.csv; 56 | } 57 | 58 | location /gzip/ { 59 | gzip on; 60 | gzip_min_length 1; 61 | gzip_types "*"; 62 | gzip_proxied any; 63 | root /var/www/html; 64 | index index.html index.json; 65 | expires epoch; 66 | } 67 | 68 | location /image/ { 69 | root /var/www/html; 70 | index cc-public-domain-mark.png; 71 | expires epoch; 72 | } 73 | 74 | location /status500/ { 75 | return 500 '{"fake": "500er json"}'; 76 | } 77 | 78 | location /status409/ { 79 | return 409 '{"fake": "509er json"}'; 80 | } 81 | 82 | location /status401/ { 83 | auth_basic "This is restricted"; 84 | auth_basic_user_file /etc/nginx/does-not-exist; 85 | } 86 | 87 | location /bad-gateway/ { 88 | proxy_pass http://127.0.0.1:4776; 89 | } 90 | 91 | location /echo/ { 92 | lua_need_request_body on; 93 | content_by_lua ' 94 | ngx.header["Content-Type"] = ngx.var.http_content_type 95 | ngx.header["X-Request-Method"] = ngx.var.request_method 96 | ngx.header["X-Custom-Header"] = ngx.var.http_custom_header 97 | ngx.print(ngx.var.request_body) 98 | '; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /ci/port-4778-app.conf: -------------------------------------------------------------------------------- 1 | proxy_cache_path /tmp/nginx-api_cache levels=1:2 keys_zone=api_cache:100m max_size=10g inactive=60m use_temp_path=off; 2 | 3 | server { 4 | listen 4778; 5 | 6 | gzip off; 7 | gzip_proxied off; 8 | 9 | proxy_buffering on; 10 | 11 | log_subrequest on; 12 | lua_code_cache off; 13 | 14 | add_header "X-Upstream-Cache-Status" $upstream_cache_status always; 15 | 16 | location /ssi-api-gateway/ { 17 | internal; 18 | rewrite ^/ssi-api-gateway/(.*)$ /$1 break; 19 | proxy_pass http://127.0.0.1:4777; 20 | 21 | proxy_cache api_cache; 22 | proxy_cache_lock on; 23 | proxy_cache_lock_age 60s; 24 | proxy_cache_lock_timeout 60s; 25 | proxy_cache_min_uses 1; 26 | proxy_cache_use_stale error timeout updating; 27 | proxy_set_header Accept-Encoding ""; 28 | proxy_set_header Host $host; 29 | proxy_set_header X-Forwarded-Port 80; 30 | proxy_set_header X-Forwarded-Proto "http"; 31 | } 32 | 33 | location / { 34 | set $ssi_api_gateway_prefix "/ssi-api-gateway"; 35 | set $ssi_types "text/.*html application/.*json"; 36 | set $ssi_validate_json_types "application/json application/.*json"; 37 | set $ssi_validate_json_inline on; 38 | set $ssi_max_includes 512; 39 | set $ssi_max_ssi_depth 16; 40 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 41 | set $ssi_minimize_max_age on; 42 | set $ssi_minimize_override_stale_while_revalidate 5; 43 | content_by_lua_file "/etc/nginx/lua-ssi-content.lua"; 44 | header_filter_by_lua_file "/etc/nginx/lua-ssi-header.lua"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ci/port-4779-app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 4779; 3 | 4 | gzip off; 5 | gzip_proxied off; 6 | 7 | proxy_buffering on; 8 | 9 | log_subrequest on; 10 | lua_code_cache off; 11 | 12 | add_header "X-Upstream-Cache-Status" $upstream_cache_status always; 13 | 14 | location /ssi-api-gateway/ { 15 | internal; 16 | rewrite ^/ssi-api-gateway/(.*)$ /$1 break; 17 | proxy_pass http://127.0.0.1:4777; 18 | 19 | client_body_buffer_size 1K; #Controlling Buffer Overflow Attacks 20 | proxy_buffer_size 16k; 21 | proxy_buffers 256 16k; 22 | proxy_buffering on; 23 | proxy_busy_buffers_size 512k; # default 8k|16k 24 | proxy_max_temp_file_size 1024m; 25 | 26 | proxy_cache api_cache; 27 | proxy_cache_lock on; 28 | proxy_cache_lock_age 60s; 29 | proxy_cache_lock_timeout 60s; 30 | proxy_cache_min_uses 1; 31 | proxy_cache_use_stale error timeout updating; 32 | proxy_set_header Accept-Encoding ""; 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Forwarded-Port 80; 35 | proxy_set_header X-Forwarded-Proto "http"; 36 | 37 | } 38 | 39 | location / { 40 | set $ssi_api_gateway_prefix "/ssi-api-gateway"; 41 | set $ssi_types ".*"; 42 | set $ssi_validate_json_types ".*"; 43 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 44 | content_by_lua_file "/etc/nginx/lua-ssi-content.lua"; 45 | header_filter_by_lua_file "/etc/nginx/lua-ssi-header.lua"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ci/port-4780-app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 4780; 3 | 4 | gzip off; 5 | gzip_proxied off; 6 | 7 | proxy_buffering on; 8 | 9 | log_subrequest on; 10 | lua_code_cache off; 11 | 12 | add_header "X-Upstream-Cache-Status" $upstream_cache_status always; 13 | 14 | location /ssi-api-gateway/ { 15 | internal; 16 | rewrite ^/ssi-api-gateway/(.*)$ /$1 break; 17 | proxy_pass http://127.0.0.1:4777; 18 | 19 | client_body_buffer_size 1K; #Controlling Buffer Overflow Attacks 20 | proxy_buffer_size 16k; 21 | proxy_buffers 256 16k; 22 | proxy_buffering on; 23 | proxy_busy_buffers_size 512k; # default 8k|16k 24 | proxy_max_temp_file_size 0; 25 | 26 | proxy_cache api_cache; 27 | proxy_cache_lock on; 28 | proxy_cache_lock_age 60s; 29 | proxy_cache_lock_timeout 60s; 30 | proxy_cache_min_uses 1; 31 | proxy_cache_use_stale error timeout updating; 32 | proxy_set_header Accept-Encoding ""; 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Forwarded-Port 80; 35 | proxy_set_header X-Forwarded-Proto "http"; 36 | 37 | } 38 | 39 | location / { 40 | auth_basic "This is restricted"; 41 | auth_basic_user_file /etc/nginx/does-not-exist; 42 | 43 | set $ssi_api_gateway_prefix "/ssi-api-gateway"; 44 | set $ssi_types ".*"; 45 | set $ssi_validate_json_types ".*"; 46 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 47 | content_by_lua_file "/etc/nginx/lua-ssi-content.lua"; 48 | header_filter_by_lua_file "/etc/nginx/lua-ssi-header.lua"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ci/port-4781-app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 4781; 3 | 4 | gzip off; 5 | gzip_proxied off; 6 | 7 | proxy_buffering on; 8 | 9 | log_subrequest on; 10 | lua_code_cache off; 11 | 12 | add_header "X-Upstream-Cache-Status" $upstream_cache_status always; 13 | 14 | location /ssi-api-gateway/ { 15 | internal; 16 | rewrite ^/ssi-api-gateway/(.*)$ /$1 break; 17 | proxy_pass http://127.0.0.1:4777; 18 | 19 | proxy_cache api_cache; 20 | proxy_cache_lock on; 21 | proxy_cache_lock_age 60s; 22 | proxy_cache_lock_timeout 60s; 23 | proxy_cache_min_uses 1; 24 | proxy_cache_use_stale error timeout updating; 25 | proxy_set_header Accept-Encoding ""; 26 | proxy_set_header Host $host; 27 | proxy_set_header X-Forwarded-Port 80; 28 | proxy_set_header X-Forwarded-Proto "http"; 29 | } 30 | 31 | location / { 32 | set $ssi_api_gateway_prefix "/ssi-api-gateway"; 33 | set $ssi_types "text/.*html application/.*json"; 34 | set $ssi_validate_json_types "application/json application/.*json"; 35 | set $ssi_validate_json_inline on; 36 | set $ssi_max_includes 512; 37 | set $ssi_max_ssi_depth 16; 38 | set $ssi_invalid_json_fallback '{"error": "invalid json", "url": %%URL%%, "message": %%MESSAGE%%}'; 39 | set $ssi_minimize_max_age on; 40 | content_by_lua_file "/etc/nginx/lua-ssi-content.lua"; 41 | header_filter_by_lua_file "/etc/nginx/lua-ssi-header.lua"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /launch-test-nginx.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DOCKER_ARGS="--rm" 3 | if [ "$1" == "daemon" ] 4 | then 5 | DOCKER_ARGS="-d" 6 | fi 7 | 8 | if [ "$2" == "" ] 9 | then 10 | cp ci/Dockerfile . 11 | else 12 | echo "FROM dracoblue/nginx-extras:$2" > Dockerfile 13 | cat ci/Dockerfile | grep -v "^FROM " >> Dockerfile 14 | fi 15 | 16 | docker build -t lua-native-ss-nginx . && docker run $DOCKER_ARGS -p127.0.0.1:4777:4777 -p127.0.0.1:4778:4778 -p127.0.0.1:4779:4779 -p127.0.0.1:4780:4780 -p127.0.0.1:4781:4781 -it lua-native-ss-nginx 17 | exit $? 18 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_success=0 4 | test_errors=0 5 | 6 | cd `dirname $0` 7 | cd tests 8 | 9 | # We cannot use while read line here 10 | # @see http://fvue.nl/wiki/Bash:_Piped_%60while-read'_loop_starts_subshell 11 | # for further information 12 | for file in `ls *.txt` 13 | do 14 | TEST_NAME=`echo "$file" | cut -f 1 -d '.'` 15 | if [ -f "$TEST_NAME.sh" ] 16 | then 17 | bash "$TEST_NAME.sh" $1 > "${TEST_NAME}.result" 18 | current_exit_code="${?}" 19 | elif [ -f "$TEST_NAME.lua" ] 20 | then 21 | docker run -it -v `pwd`/../:/usr/src/app --workdir /usr/src/app --rm pirogoeth/alpine-lua:5.2 lua5.2 tests/$TEST_NAME.lua > "${TEST_NAME}.result" 22 | current_exit_code=0 23 | else 24 | curl -sS "localhost:4778/${TEST_NAME}/" > "${TEST_NAME}.result" 25 | current_exit_code="${?}" 26 | fi 27 | if [ "${current_exit_code}" -ne "0" ] 28 | then 29 | echo " [ ] $TEST_NAME" 30 | echo " -> broken! (curl did not 2xx, Exit code: $current_exit_code)" 31 | let test_errors=test_errors+1 32 | else 33 | diff "${TEST_NAME}.txt" "${TEST_NAME}.result" 34 | current_exit_code="${?}" 35 | if [ "${current_exit_code}" -ne "0" ] 36 | then 37 | echo " [ ] $TEST_NAME" 38 | echo " -> broken! (.txt != .result, Exit code: $current_exit_code)" 39 | let test_errors=test_errors+1 40 | else 41 | echo " [OK] $TEST_NAME" 42 | let test_success=test_success+1 43 | fi 44 | fi 45 | done 46 | 47 | if [ ! $test_errors -eq 0 ] 48 | then 49 | exit 1 50 | fi 51 | 52 | exit 0 53 | -------------------------------------------------------------------------------- /src/lua-ssi-content.lua: -------------------------------------------------------------------------------- 1 | 2 | local prefix = ngx.var.ssi_api_gateway_prefix 3 | local invalidJsonFallback = ngx.var.ssi_invalid_json_fallback or '{"error": "invalid json in ssi", "url": %%URL%%, "message": %%MESSAGE%%}' 4 | local maxSsiDepth = 1024 5 | local maxSsiIncludes = 65535 6 | if ngx.var.ssi_max_includes ~= nil and ngx.var.ssi_max_includes ~= "" 7 | then 8 | maxSsiIncludes = tonumber(ngx.var.ssi_max_includes) 9 | end 10 | if ngx.var.ssi_max_ssi_depth ~= nil and ngx.var.ssi_max_ssi_depth ~= "" 11 | then 12 | maxSsiDepth = tonumber(ngx.var.ssi_max_ssi_depth) 13 | end 14 | 15 | local validateJson = false 16 | local validateJsonInline = false 17 | local validateJsonTypes = {} 18 | if ngx.var.ssi_validate_json_types ~= nil and ngx.var.ssi_validate_json_types ~= "" 19 | then 20 | validateJson = true 21 | validateJsonTypes = string.gmatch(ngx.var.ssi_validate_json_types, "%S+") 22 | end 23 | if ngx.var.ssi_validate_json_inline ~= nil and ngx.var.ssi_validate_json_inline == "on" 24 | then 25 | validateJsonInline = true 26 | end 27 | local ssiTypes = string.gmatch(".*", "%S+") 28 | if ngx.var.ssi_types ~= nil and ngx.var.ssi_types ~= "" 29 | then 30 | ssiTypes = string.gmatch(ngx.var.ssi_types, "%S+") 31 | end 32 | local minimizeMaxAge = false 33 | if ngx.var.ssi_minimize_max_age ~= nil and ngx.var.ssi_minimize_max_age == "on" 34 | then 35 | minimizeMaxAge = true 36 | end 37 | local minimizeOverrideStaleWhileRevalidate = false 38 | if ngx.var.ssi_minimize_override_stale_while_revalidate ~= nil and ngx.var.ssi_minimize_override_stale_while_revalidate ~= "" 39 | then 40 | minimizeOverrideStaleWhileRevalidate = tonumber(ngx.var.ssi_minimize_override_stale_while_revalidate) 41 | end 42 | 43 | local sanatizeHeaderFieldName = function(headerFieldName) 44 | return string.gsub(string.lower(headerFieldName), "_", "-") 45 | end 46 | 47 | ngx.req.read_body() 48 | 49 | local res = ngx.location.capture( 50 | prefix .. ngx.var.request_uri, { 51 | method = ngx["HTTP_" .. ngx.var.request_method], 52 | body = ngx.req.get_body_data() 53 | } 54 | ) 55 | 56 | getSanitizedFieldFromHeaders = function(rawFieldName, headers) 57 | local sanatizedFieldName = sanatizeHeaderFieldName(rawFieldName) 58 | for k, v in pairs(headers) do 59 | if sanatizeHeaderFieldName(k) == sanatizedFieldName 60 | then 61 | return v 62 | end 63 | end 64 | 65 | return nil 66 | end 67 | 68 | getCacheControlFieldsFromHeaders = function(headers) 69 | local cacheControlHeader = getSanitizedFieldFromHeaders("cache-control", headers) 70 | if not cacheControlHeader then 71 | return {} 72 | end 73 | 74 | local cacheControlHeaderPrefixedAndSuffixedWithAWhitespace = ", " .. cacheControlHeader .. " ," 75 | 76 | local fields = {} 77 | 78 | for key in string.gmatch(cacheControlHeaderPrefixedAndSuffixedWithAWhitespace, ',[%s]-([^=%s,]+)[%s]-') 79 | do 80 | fields[key] = true 81 | end 82 | 83 | for key, value in string.gmatch(cacheControlHeaderPrefixedAndSuffixedWithAWhitespace, '[%s,]+([^=%s,]+)%s-=%s-([^%s,]+)[%s,]-') 84 | do 85 | fields[key] = value 86 | end 87 | 88 | for key, value in string.gmatch(cacheControlHeaderPrefixedAndSuffixedWithAWhitespace, '[%s,]+([^=%s,]+)%s-=%s-"([^"]+)"[%s,]-') 89 | do 90 | fields[key] = value 91 | end 92 | 93 | return fields 94 | end 95 | 96 | getMaxAgeDecreasedByAgeOrZeroFromHeaders = function(headers) 97 | local respCacheControlFields = getCacheControlFieldsFromHeaders(headers) 98 | local respCacheControlMaxAge = (respCacheControlFields["max-age"] ~= nil and tonumber(respCacheControlFields["max-age"])) or nil 99 | if respCacheControlMaxAge == nil 100 | then 101 | if respCacheControlFields["max-age"] ~= nil then 102 | ngx.log(ngx.ERR, "request cache-control max-age is an invalid number: " .. tostring(respCacheControlFields["max-age"])) 103 | end 104 | return 0, tonumber(respCacheControlFields["stale-while-revalidate"] or 0) 105 | end 106 | 107 | local respCacheAge = tonumber(getSanitizedFieldFromHeaders("age", headers)); 108 | local respCacheSwr = tonumber(respCacheControlFields["stale-while-revalidate"]); 109 | 110 | ngx.log(ngx.DEBUG, "request cache-control: " .. tostring(respCacheControlMaxAge) .. " and age: " .. tostring(respCacheAge) .. " and stale-while-revalidate: " .. tostring(respCacheSwr)); 111 | 112 | if respCacheAge ~= nil then 113 | respCacheControlMaxAge = respCacheControlMaxAge - respCacheAge 114 | end 115 | 116 | if respCacheSwr ~= nil then 117 | respCacheControlMaxAge = respCacheControlMaxAge + respCacheSwr 118 | else 119 | if respCacheControlFields["stale-while-revalidate"] ~= nil then 120 | ngx.log(ngx.ERR, "request cache-control stale-while-revalidate is an invalid number: " .. tostring(respCacheControlFields["stale-while-revalidate"])) 121 | end 122 | end 123 | 124 | if respCacheControlMaxAge < 0 then 125 | respCacheControlMaxAge = 0 126 | end 127 | 128 | return respCacheControlMaxAge, tonumber(respCacheControlFields["stale-while-revalidate"] or 0) 129 | end 130 | 131 | getContentTypeFromHeaders = function(headers) 132 | return getSanitizedFieldFromHeaders("content-type", headers) 133 | end 134 | 135 | matchesContentTypesList = function(contentType, contentTypesList) 136 | if contentType == nil 137 | then 138 | return false 139 | end 140 | 141 | for contentTypeListItem in contentTypesList do 142 | if string.match(contentType, contentTypeListItem) 143 | then 144 | return true 145 | end 146 | end 147 | return false 148 | end 149 | 150 | local cjson = (function(validateJson) 151 | if not validateJson then 152 | return false 153 | end 154 | 155 | local hasCjson, cjson = pcall(function() 156 | return require "cjson.safe" 157 | end) 158 | 159 | if (hasCjson) 160 | then 161 | return cjson 162 | end 163 | 164 | return false 165 | end)(validateJson) 166 | 167 | local generateJsonErrorFallback = function(url, message) 168 | local escapedUrl = cjson.encode(url) 169 | local escapedMessage = cjson.encode(message) 170 | 171 | local body = string.gsub(invalidJsonFallback, "%%%%URL%%%%", function() 172 | return escapedUrl 173 | end) 174 | return string.gsub(body, "%%%%MESSAGE%%%%", function() 175 | return escapedMessage 176 | end) 177 | end 178 | 179 | if validateJson and not cjson then 180 | ngx.log(ngx.ERR, "Even though ssi_validate_json is true, the cjson library is not installed! Skip validation!") 181 | end 182 | 183 | ngx.log(ngx.DEBUG, "request_uri: ", prefix .. ngx.var.request_uri) 184 | local captureRegularFileExpression = '' 185 | local captureRegularVirtualExpression = '' 186 | local captureRegularFileExpressions = {captureRegularFileExpression,captureRegularVirtualExpression} 187 | 188 | local getSsiRequestsAndCount = function(ssiResponses, body) 189 | local ssiRequests = {} 190 | local ssiRequestsCount = 0 191 | local ssiMatchesCount = 0 192 | local ssiRequestLock = {} 193 | 194 | for i,captureRegularExpression in ipairs(captureRegularFileExpressions) do 195 | local regularExpression = string.gsub(captureRegularExpression, "([%(%)])", "") 196 | local matches = string.gmatch(body, regularExpression) 197 | for match,n in matches do 198 | -- ngx.log(ngx.DEBUG, "matches", match) 199 | local ssiVirtualPath = string.match(match, captureRegularExpression) 200 | -- ngx.log(ngx.DEBUG, "ssiVirtualPath", ssiVirtualPath) 201 | if ssiResponses[prefix .. ssiVirtualPath] == nil and ssiRequestLock[prefix .. ssiVirtualPath] == nil 202 | -- if ssiResponses[prefix .. ssiVirtualPath] == nil 203 | then 204 | ssiRequestLock[prefix .. ssiVirtualPath] = true 205 | if string.sub(ssiVirtualPath, 0, 1) == "/" then 206 | table.insert(ssiRequests, { prefix .. ssiVirtualPath }) 207 | ssiRequestsCount = ssiRequestsCount + 1 208 | else 209 | ssiResponses[prefix .. ssiVirtualPath] = {status = 500, header = {}, body = generateJsonErrorFallback(ssiVirtualPath, "ssi virtual path must start with a /")} 210 | end 211 | end 212 | ssiMatchesCount = ssiMatchesCount + 1 213 | end 214 | end 215 | 216 | return ssiRequests, ssiRequestsCount, ssiMatchesCount 217 | end 218 | 219 | if res then 220 | local contentType = getContentTypeFromHeaders(res.header) 221 | ngx.status = res.status 222 | -- ngx.say("status: ", res.status) 223 | -- ngx.say("body:") 224 | -- ngx.print(res.body) 225 | local body = res.body 226 | local minimumCacheControlMaxAge = nil 227 | local rootCacheControlMaxAge = nil 228 | local rootCacheControlSwr = nil 229 | local totalMissingCacheControlCount = 0 230 | if minimizeMaxAge then 231 | rootCacheControlMaxAge, rootCacheControlSwr = getMaxAgeDecreasedByAgeOrZeroFromHeaders(res.header) 232 | if (getSanitizedFieldFromHeaders("cache-control", res.header) == nil) then 233 | ngx.log(ngx.ERR, "missing cache control on root request url: " .. ngx.var.request_uri) 234 | totalMissingCacheControlCount = totalMissingCacheControlCount + 1 235 | end 236 | ngx.ctx.ssiMinimizeMaxAgeUrl = ngx.var.request_uri 237 | ngx.ctx.ssiMinimizeMaxAgeAge = rootCacheControlMaxAge 238 | ngx.ctx.ssiMinimizeMaxAgeCacheControl = getSanitizedFieldFromHeaders("cache-control", res.header) 239 | minimumCacheControlMaxAge = rootCacheControlMaxAge 240 | if rootCacheControlMaxAge == 0 then 241 | rootCacheControlMaxAge = nil 242 | end 243 | ngx.log(ngx.DEBUG, "cache-control root: " .. tostring(rootCacheControlMaxAge)) 244 | end 245 | 246 | if (validateJson) 247 | then 248 | ngx.log(ngx.DEBUG, "check if content type matches: ", contentType) 249 | validateJson = matchesContentTypesList(contentType, validateJsonTypes) 250 | end 251 | 252 | if matchesContentTypesList(contentType, ssiTypes) 253 | then 254 | local ssiResponses = {} 255 | local totalSsiSubRequestsCount = 0 256 | local totalSsiIncludesCount = 0 257 | local totalSsiDepth = 0 258 | 259 | local ssiRequests, ssiRequestsCount, ssiMatchesCount = getSsiRequestsAndCount(ssiResponses, body) 260 | 261 | while ssiMatchesCount > 0 262 | do 263 | totalSsiDepth = totalSsiDepth + 1 264 | if (totalSsiDepth > maxSsiDepth or totalSsiIncludesCount > maxSsiIncludes) and ssiMatchesCount > 0 265 | then 266 | if (totalSsiDepth > maxSsiDepth) 267 | then 268 | ngx.log(ngx.ERR, "max recursion depth exceeded " .. maxSsiDepth .. "(was " .. totalSsiDepth .. ")") 269 | else 270 | ngx.log(ngx.ERR, "max ssi includes exceeded " .. maxSsiIncludes .. "(was " .. totalSsiIncludesCount .. ")") 271 | 272 | end 273 | for i,captureRegularExpression in ipairs(captureRegularFileExpressions) do 274 | local regularExpression = string.gsub(captureRegularExpression, "([%(%)])", "") 275 | 276 | local replacer = function(w) 277 | local ssiVirtualPath = string.match(w, captureRegularExpression) 278 | if (totalSsiDepth > maxSsiDepth) 279 | then 280 | return generateJsonErrorFallback(ssiVirtualPath, "max recursion depth exceeded " .. maxSsiDepth .. "(was " .. totalSsiDepth .. ")") 281 | else 282 | return generateJsonErrorFallback(ssiVirtualPath, "max ssi includes exceeded " .. maxSsiIncludes .. "(was " .. totalSsiIncludesCount .. ")") 283 | end 284 | end 285 | 286 | body = string.gsub(body, regularExpression, replacer) 287 | end 288 | 289 | ssiMatchesCount = 0 290 | else 291 | if (ssiRequestsCount > 0) 292 | then 293 | -- FIXME: handle ssiRequestsCount > 200, because this is the internal nginx limit 294 | -- issue all the requests at once and wait until they all return 295 | local resps = { ngx.location.capture_multi(ssiRequests) } 296 | 297 | -- loop over the responses table 298 | for i, resp in ipairs(resps) do 299 | -- ngx.log(ngx.DEBUG, "resp ", i, " with ", resp.status, " and body ", resp.body) 300 | ngx.log(ngx.DEBUG, "sub request url ", ssiRequests[i][1], " and status ", resp.status) 301 | if minimizeMaxAge and minimumCacheControlMaxAge ~= nil then 302 | local respCacheControlMaxAge = getMaxAgeDecreasedByAgeOrZeroFromHeaders(resp.header) 303 | if (getSanitizedFieldFromHeaders("cache-control", resp.header) == nil) then 304 | ngx.log(ngx.ERR, "missing cache control on sub request url: " .. ssiRequests[i][1]) 305 | totalMissingCacheControlCount = totalMissingCacheControlCount + 1 306 | end 307 | if respCacheControlMaxAge < minimumCacheControlMaxAge then 308 | ngx.log(ngx.DEBUG, "sub request cache-control: " .. tostring(respCacheControlMaxAge)) 309 | ngx.ctx.ssiMinimizeMaxAgeUrl = string.sub(ssiRequests[i][1], string.len(prefix) + 1) 310 | ngx.ctx.ssiMinimizeMaxAgeAge = respCacheControlMaxAge 311 | ngx.ctx.ssiMinimizeMaxAgeCacheControl = getSanitizedFieldFromHeaders("cache-control", resp.header) 312 | minimumCacheControlMaxAge = respCacheControlMaxAge 313 | end 314 | end 315 | 316 | if validateJson and validateJsonInline 317 | then 318 | local bodyWithoutSsiIncludes = resp.body 319 | for i,captureRegularExpression in ipairs(captureRegularFileExpressions) do 320 | local regularExpression = string.gsub(captureRegularExpression, "([%(%)])", "") 321 | bodyWithoutSsiIncludes = string.gsub(bodyWithoutSsiIncludes, regularExpression, "{}") 322 | end 323 | local value, errorMessage = cjson.decode(bodyWithoutSsiIncludes) 324 | if (errorMessage) then 325 | resp.body = generateJsonErrorFallback(string.sub(ssiRequests[i][1], string.len(prefix) + 1), errorMessage) 326 | ssiResponses[ssiRequests[i][1]] = resp 327 | else 328 | ssiResponses[ssiRequests[i][1]] = resp 329 | end 330 | end 331 | 332 | ssiResponses[ssiRequests[i][1]] = resp 333 | -- process the response table "resp" 334 | end 335 | end 336 | 337 | 338 | for i,captureRegularExpression in ipairs(captureRegularFileExpressions) do 339 | local regularExpression = string.gsub(captureRegularExpression, "([%(%)])", "") 340 | 341 | local replacer = function(w) 342 | local ssiVirtualPath = string.match(w, captureRegularExpression) 343 | if (ssiResponses[prefix .. ssiVirtualPath] == nil) 344 | then 345 | ngx.log(ngx.ERR, "did not capture multi with ssiVirtualPath ", ssiVirtualPath) 346 | return w 347 | else 348 | return ssiResponses[prefix .. ssiVirtualPath].body 349 | end 350 | end 351 | 352 | body = string.gsub(body, regularExpression, replacer) 353 | end 354 | 355 | totalSsiSubRequestsCount = totalSsiSubRequestsCount + ssiRequestsCount 356 | totalSsiIncludesCount = totalSsiIncludesCount + ssiMatchesCount 357 | ssiRequests, ssiRequestsCount, ssiMatchesCount = getSsiRequestsAndCount(ssiResponses, body) 358 | end 359 | end 360 | 361 | if ngx.status == 200 362 | then 363 | local md5 = ngx.md5(body) 364 | ngx.ctx.etag = '"' .. md5 .. '"' 365 | end 366 | 367 | -- ngx.log(ngx.DEBUG, "ssiRequestsCount", totalSsiSubRequestsCount) 368 | ngx.ctx.ssiMissingCacheControlCount = totalMissingCacheControlCount 369 | ngx.ctx.ssiRequestsCount = totalSsiSubRequestsCount 370 | ngx.ctx.ssiIncludesCount = totalSsiIncludesCount 371 | ngx.ctx.ssiDepth = totalSsiDepth 372 | 373 | if cjson and validateJson 374 | then 375 | local value, errorMessage = cjson.decode(body) 376 | if errorMessage then 377 | body = generateJsonErrorFallback(ngx.var.request_uri, errorMessage) 378 | 379 | if totalSsiSubRequestsCount ~= 0 and not validateJsonInline 380 | then 381 | local bodyTable = cjson.decode(body) 382 | bodyTable.brokenSsiRequests = {} 383 | -- loop over the responses table 384 | for ssiRequestUrl, ssiResponse in pairs(ssiResponses) do 385 | local ssiResponseBody = ssiResponse.body 386 | for i,captureRegularExpression in ipairs(captureRegularFileExpressions) do 387 | local regularExpression = string.gsub(captureRegularExpression, "([%(%)])", "") 388 | ssiResponseBody = string.gsub(ssiResponseBody, regularExpression, "{}") 389 | end 390 | local ssiResponseDecodedValue, ssiResponseDecodingErrorMessage = cjson.decode(ssiResponseBody) 391 | if (ssiResponseDecodingErrorMessage) 392 | then 393 | table.insert(bodyTable.brokenSsiRequests, {url = string.sub(ssiRequestUrl, string.len(prefix) + 1), message = ssiResponseDecodingErrorMessage }) 394 | end 395 | end 396 | 397 | body = cjson.encode(bodyTable) 398 | end 399 | 400 | ngx.ctx.etag = nil 401 | ngx.ctx.overrideContentType = "application/json"; 402 | ngx.status = 500 403 | end 404 | end 405 | 406 | end 407 | 408 | if minimizeMaxAge then 409 | if minimumCacheControlMaxAge > 0 410 | then 411 | ngx.ctx.overrideCacheControl = "max-age=" .. minimumCacheControlMaxAge; 412 | 413 | if rootCacheControlSwr and minimizeOverrideStaleWhileRevalidate then 414 | ngx.ctx.overrideCacheControl = ngx.ctx.overrideCacheControl .. ", stale-while-revalidate=" .. minimizeOverrideStaleWhileRevalidate 415 | end 416 | else 417 | ngx.ctx.overrideCacheControl = "no-cache, max-age=0"; 418 | end 419 | 420 | end 421 | 422 | ngx.ctx.res = res 423 | ngx.print(body) 424 | end 425 | -------------------------------------------------------------------------------- /src/lua-ssi-header.lua: -------------------------------------------------------------------------------- 1 | if not ngx.headers_sent and ngx.ctx.res 2 | then 3 | local requestHeaders = {} 4 | 5 | local sanatizeHeaderFieldName = function(headerFieldName) 6 | return string.gsub(string.lower(headerFieldName), "_", "-") 7 | end 8 | 9 | for k, v in pairs(ngx.req.get_headers()) do 10 | k = sanatizeHeaderFieldName(k) 11 | requestHeaders[k] = v 12 | end 13 | 14 | for k, v in pairs(ngx.ctx.res.header) do 15 | ngx.header[k] = v 16 | end 17 | if ngx.ctx.ssiRequestsCount then 18 | ngx.header["X-Ssi-Sub-Requests"] = ngx.ctx.ssiRequestsCount 19 | end 20 | if ngx.ctx.ssiIncludesCount then 21 | ngx.header["X-Ssi-Includes"] = ngx.ctx.ssiIncludesCount 22 | end 23 | if ngx.ctx.ssiDepth then 24 | ngx.header["X-Ssi-Depth"] = ngx.ctx.ssiDepth 25 | end 26 | if ngx.ctx.ssiMissingCacheControlCount then 27 | ngx.header["X-Ssi-Missing-CC-Count"] = ngx.ctx.ssiMissingCacheControlCount 28 | end 29 | if requestHeaders["x-ssi-debug"] == "true" then 30 | ngx.header["X-Ssi-Minimize-MaxAge-Url"] = ngx.ctx.ssiMinimizeMaxAgeUrl 31 | ngx.header["X-Ssi-Minimize-MaxAge-Age"] = ngx.ctx.ssiMinimizeMaxAgeAge 32 | ngx.header["X-Ssi-Minimize-MaxAge-Cache-Control"] = ngx.ctx.ssiMinimizeMaxAgeCacheControl 33 | end 34 | if ngx.ctx.overrideContentType then 35 | ngx.header["Content-Type"] = ngx.ctx.overrideContentType 36 | end 37 | if ngx.ctx.overrideCacheControl then 38 | ngx.header["Cache-Control"] = ngx.ctx.overrideCacheControl 39 | ngx.header["Age"] = "0" 40 | end 41 | ngx.header["Content-Length"] = nil 42 | if ngx.ctx.etag then 43 | ngx.header["ETag"] = ngx.ctx.etag 44 | local ifNoneMatch = ngx.req.get_headers()["If-None-Match"] or nil 45 | ngx.log(ngx.DEBUG, "If-None-Match: ", ifNoneMatch) 46 | ngx.log(ngx.DEBUG, "ETag: ", ngx.ctx.etag) 47 | 48 | if ifNoneMatch == ngx.ctx.etag 49 | then 50 | ngx.header["Content-Length"] = 0 51 | ngx.exit(ngx.HTTP_NOT_MODIFIED) 52 | return 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /tests/broken_json_include.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"error": "invalid json", "url": "\/broken_json_include\/broken_sub_resource.json", "message": "Expected object key string but found unexpected end of string at character 47"}] } -------------------------------------------------------------------------------- /tests/broken_json_include/broken_sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "with invalud json} -------------------------------------------------------------------------------- /tests/broken_json_include/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [] } -------------------------------------------------------------------------------- /tests/broken_json_include_with_percent.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"error": "invalid json", "url": "\/broken_json_include_with_percent\/sub_resource.json?fa=%20%1", "message": "Expected value but found invalid token at character 13"}, {"error": "invalid json", "url": "\/broken_json_include_with_percent\/sub_resource.json?fa=%20%1", "message": "Expected value but found invalid token at character 13"}] } -------------------------------------------------------------------------------- /tests/broken_json_include_with_percent/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/broken_json_include_with_percent/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": brokensubResource%1%2%%"} -------------------------------------------------------------------------------- /tests/broken_json_include_without_inline.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -sS "localhost:4779/broken_json_include/" 4 | exit $? -------------------------------------------------------------------------------- /tests/broken_json_include_without_inline.txt: -------------------------------------------------------------------------------- 1 | {"error":"invalid json","brokenSsiRequests":[{"url":"\/broken_json_include\/broken_sub_resource.json","message":"Expected object key string but found unexpected end of string at character 47"}],"message":"Expected object key string but found unexpected end of string at character 91","url":"\/broken_json_include\/"} -------------------------------------------------------------------------------- /tests/content_type_of_json_error.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "localhost:4779/bad-gateway/" 2>&1 | grep '< Content-Type' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/content_type_of_json_error.txt: -------------------------------------------------------------------------------- 1 | < Content-Type: application/json -------------------------------------------------------------------------------- /tests/count_root_request_without_cache_control.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -H 'X-Ssi-Debug: true' "http://localhost:4778/max-age/no-cache-control.json" 2>&1 | grep "X-Ssi-Missing-CC" | sort -n | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/count_root_request_without_cache_control.txt: -------------------------------------------------------------------------------- 1 | < X-Ssi-Missing-CC-Count: 1 -------------------------------------------------------------------------------- /tests/count_subrequests_without_cache_control.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -H 'X-Ssi-Debug: true' "http://localhost:4778/max-age/include-without-cache-control-expires-in-30.json" 2>&1 | grep "X-Ssi-Missing-CC" | sort -n | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/count_subrequests_without_cache_control.txt: -------------------------------------------------------------------------------- 1 | < X-Ssi-Missing-CC-Count: 1 -------------------------------------------------------------------------------- /tests/debug_no_include.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -H 'X-Ssi-Debug: true' "http://localhost:4778/json/" 2>&1 | grep "X-Ssi-Minimize-MaxAge" | sort -n | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/debug_no_include.txt: -------------------------------------------------------------------------------- 1 | < X-Ssi-Minimize-MaxAge-Age: 0 < X-Ssi-Minimize-MaxAge-Cache-Control: no-cache < X-Ssi-Minimize-MaxAge-Url: /json/ -------------------------------------------------------------------------------- /tests/debug_ssi_expires_stale.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -H 'X-Ssi-Debug: true' "http://localhost:4778/max-age/include-stale-expires-in-120.json" 2>&1 | grep "X-Ssi-Minimize-MaxAge" | sort -n | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/debug_ssi_expires_stale.txt: -------------------------------------------------------------------------------- 1 | < X-Ssi-Minimize-MaxAge-Age: 45 < X-Ssi-Minimize-MaxAge-Cache-Control: max-age=35, stale-while-revalidate=40 < X-Ssi-Minimize-MaxAge-Url: /max-age/35-seconds/30-age/40-swr -------------------------------------------------------------------------------- /tests/dont_minimize_max_age.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS curl -v -sS "localhost:4779/max-age/include-age-5-and-cache-control-10-and-15-expires-in-30.json" 2>&1 | grep "Cache-Control" | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/dont_minimize_max_age.txt: -------------------------------------------------------------------------------- 1 | < Cache-Control: max-age=30 -------------------------------------------------------------------------------- /tests/echo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -sS -X POST -H "Content-Type: application/json" -d '{"key":"value"}' "localhost:4778/echo/" 4 | exit $? -------------------------------------------------------------------------------- /tests/echo.txt: -------------------------------------------------------------------------------- 1 | {"key":"value"} -------------------------------------------------------------------------------- /tests/echo_custom_header.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -X POST -H "Custom-Header: LALA" -H "Content-Type: application/json" -d '{"key":"value"}' "localhost:4778/echo/" 2>&1 | grep '< X-Custom-Header' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/echo_custom_header.txt: -------------------------------------------------------------------------------- 1 | < X-Custom-Header: LALA -------------------------------------------------------------------------------- /tests/echo_method.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -X POST -H "Content-Type: application/json" -d '{"key":"value"}' "localhost:4778/echo/" 2>&1 | grep '< X-Request-Method' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/echo_method.txt: -------------------------------------------------------------------------------- 1 | < X-Request-Method: POST -------------------------------------------------------------------------------- /tests/etag_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -X POST -H "Content-Type: application/json" -d '{"key":"value"}' "localhost:4778/echo/" 2>&1 | grep '< ETag' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/etag_check.txt: -------------------------------------------------------------------------------- 1 | < ETag: "a7353f7cddce808de0032747a0b7be50" -------------------------------------------------------------------------------- /tests/excluded_content_type.txt: -------------------------------------------------------------------------------- 1 | key,, -------------------------------------------------------------------------------- /tests/excluded_content_type/index.csv: -------------------------------------------------------------------------------- 1 | key,, -------------------------------------------------------------------------------- /tests/gzip.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | hello World 3 | this 4 | 5 | footer! 6 | 7 | zwooter! 8 | 9 | drooter 10 | 11 | -------------------------------------------------------------------------------- /tests/gzip/drooter.html: -------------------------------------------------------------------------------- 1 | drooter -------------------------------------------------------------------------------- /tests/gzip/footer.html: -------------------------------------------------------------------------------- 1 | footer! -------------------------------------------------------------------------------- /tests/gzip/index.html: -------------------------------------------------------------------------------- 1 | hello world 2 | hello World 3 | this 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/gzip/zwooter.html: -------------------------------------------------------------------------------- 1 | zwooter! 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/image.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracoBlue/lua-native-ssi-nginx/e3f6b21843587d58868d8e95b2258bc427838178/tests/image.txt -------------------------------------------------------------------------------- /tests/image/cc-public-domain-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracoBlue/lua-native-ssi-nginx/e3f6b21843587d58868d8e95b2258bc427838178/tests/image/cc-public-domain-mark.png -------------------------------------------------------------------------------- /tests/json.txt: -------------------------------------------------------------------------------- 1 | {"key": "value"} -------------------------------------------------------------------------------- /tests/json/index.json: -------------------------------------------------------------------------------- 1 | {"key": "value"} -------------------------------------------------------------------------------- /tests/json_include.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}]}, {"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}]}] } -------------------------------------------------------------------------------- /tests/json_include/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/json_include/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "sub_sub_resources": [, , ]} -------------------------------------------------------------------------------- /tests/json_include/sub_sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subSubResource"} -------------------------------------------------------------------------------- /tests/json_include_bad_gateway.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"error": "invalid json", "url": "\/bad-gateway\/", "message": "Expected value but found invalid token at character 1"}, {"error": "invalid json", "url": "\/bad-gateway\/", "message": "Expected value but found invalid token at character 1"}] } -------------------------------------------------------------------------------- /tests/json_include_bad_gateway/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/json_include_bad_gateway_without_inline.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -sS "localhost:4779/json_include_bad_gateway/" 4 | exit $? -------------------------------------------------------------------------------- /tests/json_include_bad_gateway_without_inline.txt: -------------------------------------------------------------------------------- 1 | {"error":"invalid json","brokenSsiRequests":[{"url":"\/bad-gateway\/","message":"Expected value but found invalid token at character 1"}],"message":"Expected value but found invalid token at character 42","url":"\/json_include_bad_gateway\/"} -------------------------------------------------------------------------------- /tests/json_include_with_percent.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"thisIsA": "subResource%1%2%%"}, {"thisIsA": "subResource%1%2%%"}] } -------------------------------------------------------------------------------- /tests/json_include_with_percent/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/json_include_with_percent/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource%1%2%%"} -------------------------------------------------------------------------------- /tests/json_virtual_include.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}]}, {"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}, {"thisIsA": "subSubResource"}]}] } -------------------------------------------------------------------------------- /tests/json_virtual_include/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/json_virtual_include/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "sub_sub_resources": [, , ]} -------------------------------------------------------------------------------- /tests/json_virtual_include/sub_sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subSubResource"} -------------------------------------------------------------------------------- /tests/max-age/include-age-5-and-cache-control-10-and-15-expires-in-30.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "max-age index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/max-age/include-broken-max-age-value-and-expires-in-30.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "max-age index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/max-age/include-stale-expires-in-120.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "max-age index", "sub_resources": [] } 2 | -------------------------------------------------------------------------------- /tests/max-age/include-without-cache-control-expires-in-30.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "max-age index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/max-age/includes-30-max-age-35-age-40-swr-expires-in-120.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "max-age index", "sub_resources": [] } 2 | -------------------------------------------------------------------------------- /tests/max-age/no-cache-control.json: -------------------------------------------------------------------------------- 1 | {"i": "do-not-expire"} -------------------------------------------------------------------------------- /tests/max-age/‪includes-35-max-age-30-age-40-swr-expires-in-120.json‬: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "max-age index", "sub_resources": [] } 2 | -------------------------------------------------------------------------------- /tests/no_not_modified_if_not_200.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -H 'If-None-Match: "85b2aa98e0ce60179785e8a292d6166e"' "localhost:4779/no_not_modified_if_not_200/" 2>&1 | grep '< HTTP/1.1' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/no_not_modified_if_not_200.txt: -------------------------------------------------------------------------------- 1 | < HTTP/1.1 500 Internal Server Error -------------------------------------------------------------------------------- /tests/no_not_modified_if_not_200/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/no_not_modified_if_not_200/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", but invalid json} -------------------------------------------------------------------------------- /tests/not_modified_check_on_json_include.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS -H 'If-None-Match: "8cb1ed23ce8bcf345b3f285045d9a9ba"' "localhost:4778/json_include/" 2>&1 | grep '< HTTP/1.1' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/not_modified_check_on_json_include.txt: -------------------------------------------------------------------------------- 1 | < HTTP/1.1 304 Not Modified -------------------------------------------------------------------------------- /tests/one.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | hello World 3 | this 4 | 5 | footer! 6 | 7 | zwooter! 8 | 9 | drooter 10 | 11 | -------------------------------------------------------------------------------- /tests/one/drooter.html: -------------------------------------------------------------------------------- 1 | drooter -------------------------------------------------------------------------------- /tests/one/footer.html: -------------------------------------------------------------------------------- 1 | footer! -------------------------------------------------------------------------------- /tests/one/index.html: -------------------------------------------------------------------------------- 1 | hello world 2 | hello World 3 | this 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/one/zwooter.html: -------------------------------------------------------------------------------- 1 | zwooter! 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parse_cache_control.lua: -------------------------------------------------------------------------------- 1 | ngx = { 2 | log = function() end, 3 | var = { ssi_api_gateway_prefix = "/ssi-api-gateway", request_uri = "/lua-test", request_method = "GET" }, 4 | req = { read_body = function() end, get_body_data = function() end }, 5 | location = { capture = function() end } 6 | } 7 | 8 | local assertTable = function(actual, expected, message) 9 | for k,v in pairs(actual) 10 | do 11 | assert(expected[k] == v, message or ("for key '" .. tostring(k) .. "' '" .. tostring(expected[k]) .. "' expected, but '" .. tostring(v) .. "' given")) 12 | end 13 | for k,v in pairs(expected) 14 | do 15 | assert(actual[k] == v, message or ("for key '" .. tostring(k) .. "' '" .. tostring(v) .. "' expected, but '" .. tostring(actual[k]) .. "' given")) 16 | end 17 | end 18 | 19 | dofile("./src/lua-ssi-content.lua") 20 | 21 | assertTable( 22 | getCacheControlFieldsFromHeaders( 23 | { 24 | cache_control = 'stale-while-revalidate="124",max-age=123,stale-if-error=123,public,private' 25 | } 26 | ), 27 | { 28 | ['stale-while-revalidate']="124", 29 | ['max-age']="123", 30 | ['stale-if-error']="123", 31 | ['public']=true, 32 | ['private']=true, 33 | } 34 | ) 35 | 36 | assertTable( 37 | getCacheControlFieldsFromHeaders( 38 | { 39 | cache_control = 'stale-while-revalidate = "124",max-age = 123,stale-if-error = 123,public,private' 40 | } 41 | ), 42 | { 43 | ['stale-while-revalidate']="124", 44 | ['max-age']="123", 45 | ['stale-if-error']="123", 46 | ['public']=true, 47 | ['private']=true, 48 | } 49 | ) 50 | 51 | assertTable( 52 | getCacheControlFieldsFromHeaders( 53 | { 54 | cache_control = 'stale-while-revalidate="124",max-age=123,private' 55 | } 56 | ), 57 | { 58 | ['stale-while-revalidate']="124", 59 | ['max-age']="123", 60 | ['private']=true, 61 | } 62 | ) 63 | 64 | assertTable( 65 | getCacheControlFieldsFromHeaders( 66 | { 67 | cache_control = 'stale-while-revalidate="124", max-age=123, private' 68 | } 69 | ), 70 | { 71 | ['stale-while-revalidate']="124", 72 | ['max-age']="123", 73 | ['private']=true, 74 | } 75 | ) 76 | 77 | assertTable( 78 | getCacheControlFieldsFromHeaders( 79 | { 80 | cache_control = 'stale-while-revalidate="124"' 81 | } 82 | ), 83 | { 84 | ['stale-while-revalidate']="124" 85 | } 86 | ) 87 | -------------------------------------------------------------------------------- /tests/parse_cache_control.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracoBlue/lua-native-ssi-nginx/e3f6b21843587d58868d8e95b2258bc427838178/tests/parse_cache_control.txt -------------------------------------------------------------------------------- /tests/recursion_cap/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/recursion_cap/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "sub_sub_resources": [, , ]} -------------------------------------------------------------------------------- /tests/recursion_cap/sub_sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "sub_sub_resources": [, , ]} -------------------------------------------------------------------------------- /tests/recursion_cap_depth.txt: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"error": "invalid json", "url": "\/recursion_cap_depth\/sub_resource.json", "message": "max recursion depth exceeded 16(was 17)"}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}, {"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"thisIsA": "subResource", "sub_sub_resources": [{"error": "invalid json", "url": "\/recursion_cap_depth\/sub_resource.json", "message": "max recursion depth exceeded 16(was 17)"}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}] } -------------------------------------------------------------------------------- /tests/recursion_cap_depth/index.json: -------------------------------------------------------------------------------- 1 | {"thisIsThe": "index", "sub_resources": [, ] } -------------------------------------------------------------------------------- /tests/recursion_cap_depth/sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "sub_sub_resources": []} -------------------------------------------------------------------------------- /tests/recursion_cap_depth/sub_sub_resource.json: -------------------------------------------------------------------------------- 1 | {"thisIsA": "subResource", "sub_sub_resources": []} -------------------------------------------------------------------------------- /tests/relative_ssi_path.txt: -------------------------------------------------------------------------------- 1 | {"key":[{"error": "invalid json", "url": "relative_ssi_path", "message": "ssi virtual path must start with a \/"},{"error": "invalid json", "url": "relative_ssi_path", "message": "ssi virtual path must start with a \/"},{"error": "invalid json", "url": "relative_ssi_path", "message": "ssi virtual path must start with a \/"}]} 2 | -------------------------------------------------------------------------------- /tests/relative_ssi_path/index.json: -------------------------------------------------------------------------------- 1 | {"key":[,,]} 2 | -------------------------------------------------------------------------------- /tests/response_of_status_409.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -sS "localhost:4778/status409/" 4 | exit $? -------------------------------------------------------------------------------- /tests/response_of_status_409.txt: -------------------------------------------------------------------------------- 1 | {"fake": "509er json"} -------------------------------------------------------------------------------- /tests/status_401.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "localhost:4780/status401/" 2>&1 | grep '< HTTP' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/status_401.txt: -------------------------------------------------------------------------------- 1 | < HTTP/1.1 401 Unauthorized -------------------------------------------------------------------------------- /tests/status_409.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "localhost:4778/status409/" 2>&1 | grep '< HTTP' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/status_409.txt: -------------------------------------------------------------------------------- 1 | < HTTP/1.1 409 Conflict -------------------------------------------------------------------------------- /tests/status_500.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "localhost:4778/status500/" 2>&1 | grep '< HTTP' | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/status_500.txt: -------------------------------------------------------------------------------- 1 | < HTTP/1.1 500 Internal Server Error -------------------------------------------------------------------------------- /tests/use_max_age_0_if_max_age_is_broken_on_root.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS curl -v -sS "localhost:4778/max-age/broken-max-age-value" 2>&1 | grep "Cache-Control" | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/use_max_age_0_if_max_age_is_broken_on_root.txt: -------------------------------------------------------------------------------- 1 | < Cache-Control: no-cache, max-age=0 -------------------------------------------------------------------------------- /tests/use_max_age_0_if_max_age_is_broken_on_sub.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS curl -v -sS "localhost:4778/max-age/include-broken-max-age-value-and-expires-in-30.json" 2>&1 | grep "Cache-Control" | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/use_max_age_0_if_max_age_is_broken_on_sub.txt: -------------------------------------------------------------------------------- 1 | < Cache-Control: no-cache, max-age=0 -------------------------------------------------------------------------------- /tests/use_minimized_max_age_and_swr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "http://localhost:4778/max-age/include-stale-expires-in-120.json" 2>&1 | grep "Cache-Control" | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/use_minimized_max_age_and_swr.txt: -------------------------------------------------------------------------------- 1 | < Cache-Control: max-age=45, stale-while-revalidate=5 -------------------------------------------------------------------------------- /tests/use_minimized_max_age_and_swr_do_not_override_swr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "http://localhost:4781/max-age/include-stale-expires-in-120.json" 2>&1 | grep "Cache-Control" | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/use_minimized_max_age_and_swr_do_not_override_swr.txt: -------------------------------------------------------------------------------- 1 | < Cache-Control: max-age=45 -------------------------------------------------------------------------------- /tests/use_minimum_max_age.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -v -sS "localhost:4778/max-age/include-age-5-and-cache-control-10-and-15-expires-in-30.json" 2>&1 | grep "Cache-Control" | tr -d "\n" 4 | exit $? -------------------------------------------------------------------------------- /tests/use_minimum_max_age.txt: -------------------------------------------------------------------------------- 1 | < Cache-Control: max-age=5, stale-while-revalidate=5 --------------------------------------------------------------------------------