├── .github └── workflows │ └── ci.yml ├── AUTHOR ├── CHANGES ├── LICENSE ├── MERGED ├── README.md ├── config ├── ngx_http_redis_module.c └── t ├── redis.t ├── redis_auth.t ├── redis_db_maxnum.t ├── redis_db_not_set.t ├── redis_gunzip.t └── redis_keepalive.t /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: alpine CI 2 | 3 | on: 4 | 5 | push: 6 | branches: 7 | - main 8 | - develop 9 | pull_request: 10 | 11 | workflow_dispatch: 12 | inputs: 13 | logLevel: 14 | description: 'Log level' 15 | required: true 16 | default: 'warning' 17 | type: choice 18 | options: 19 | - info 20 | - warning 21 | - debug 22 | tags: 23 | description: 'Test scenario tags' 24 | required: false 25 | type: boolean 26 | environment: 27 | description: 'Environment to run tests against' 28 | type: environment 29 | required: false 30 | 31 | jobs: 32 | build: 33 | 34 | runs-on: ubuntu-latest 35 | container: alpine 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | - name: Install dependencies 42 | run: | 43 | apk add build-base git pcre2-dev zlib-dev 44 | 45 | - name: Checkout nginx 46 | run: | 47 | git clone https://github.com/nginx/nginx nginx-source 48 | 49 | - name: Checkout nginx tests 50 | run: | 51 | git clone https://github.com/nginx/nginx-tests 52 | 53 | - name: Configure and build nginx and ngx_http_redis module 54 | run: | 55 | cd nginx-source 56 | auto/configure \ 57 | --prefix=/tmp \ 58 | --with-compat \ 59 | --with-http_gunzip_module \ 60 | --with-http_gzip_static_module \ 61 | --add-dynamic-module=../ \ 62 | || cat objs/autoconf.err 63 | make -j $(nproc) 64 | 65 | - name: Install test dependencies 66 | run: | 67 | apk add perl perl-redis perl-test-harness-utils redis 68 | 69 | - name: Run tests 70 | run: | 71 | ulimit -c unlimited 72 | TEST_NGINX_BINARY=$GITHUB_WORKSPACE/nginx-source/objs/nginx \ 73 | TEST_NGINX_GLOBALS="load_module $GITHUB_WORKSPACE/nginx-source/objs/ngx_http_redis_module.so;" \ 74 | TEST_NGINX_LEAVE=1 \ 75 | TEST_NGINX_VERBOSE=1 \ 76 | TEST_NGINX_MODULES=$GITHUB_WORKSPACE/nginx-source/objs \ 77 | prove -prvv -Inginx-tests/lib t/ 78 | -------------------------------------------------------------------------------- /AUTHOR: -------------------------------------------------------------------------------- 1 | Sergey A. Osokin 2 | Sergey A. Osokin 3 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changes with ngx_http_redis 0.4.0 21 May 2025 2 | 3 | *) Change: merge multiple commits from the ngx_http_memcached 4 | module. 5 | 6 | *) Change: cleanup legacy code. 7 | 8 | *) Change: fix compatibility with nginx 1.23 mainline branch. 9 | 10 | *) Feature: add auth, max_db_num, and keepalive tests. 11 | Thanks to Maxim Dounin. 12 | 13 | *) Change: change the error to NGX_HTTP_UPSTREAM_INVALID_HEADER 14 | when redis backend returns the `-ERR' answer (previously 15 | nginx responses 502). Thanks to Maxim Dounin. 16 | 17 | 18 | Changes with ngx_http_redis 0.3.9 13 May 2018 19 | 20 | *) Feature: redis "AUTH" command support. 21 | Original idea from https://github.com/Yongke/ngx_http_redis-0.3.7 22 | Thanks to Wang Yongke. 23 | 24 | 25 | Changes with ngx_http_redis 0.3.8 21 Feb 2016 26 | 27 | *) Bugfix: it is impossible to compile the ngx_http_redis_module 28 | without http_gzip module or with --without-http_gzip_module 29 | option. 30 | Thanks to Yichun Zhang (agentzh). 31 | 32 | *) Feature: it's possible to compile as dynamic module now, this 33 | feature has been introduced in nginx 1.9.11. 34 | 35 | 36 | Changes with ngx_http_redis 0.3.7 28 Nov 2013 37 | 38 | *) Bugfix: ngx_http_redis_module might issue the error message 39 | "redis sent invalid trailer" for nginx >= 1.5.3. 40 | Thanks to Maxim Dounin. 41 | 42 | 43 | Changes with ngx_http_redis 0.3.6 03 Apr 2012 44 | 45 | *) Feature: redis_gzip_flag. Useful if you are prefer to 46 | store data compressed in redis. Works with ngx_http_gunzip_filter 47 | module. 48 | Thanks to Maxim Dounin. 49 | 50 | *) Bugfix: ngx_http_redis_module might issue the error message 51 | "redis sent invalid trailer". 52 | Thanks to agentzh. 53 | 54 | 55 | Changes with ngx_http_redis 0.3.5 30 Aug 2011 56 | 57 | *) Feature: add test for not set $redis_db directive. 58 | 59 | *) Feature: keep-alive support merged from original 60 | memcached module 1.1.4. 61 | 62 | 63 | Changes with ngx_http_redis 0.3.4 24 Aug 2011 64 | 65 | *) Change: better error messages diagnostics in select phase. 66 | 67 | *) Add more comments in source code. 68 | 69 | *) Bugfix: fix interaction with redis if redis_db was unused. 70 | Found by Sergey Makarov. 71 | Thanks to Igor Sysoev. 72 | 73 | *) Feature: add test suite for redis backend. 74 | Thanks to Maxim Dounin. 75 | 76 | 77 | Changes with ngx_http_redis 0.3.3 07 Jun 2011 78 | 79 | *) Bugfix: fix interaction with redis if redis_db was used. 80 | Also, compile with -Werror now is possible. 81 | 82 | 83 | Changes with ngx_http_redis 0.3.2 17 Aug 2010 84 | 85 | *) Bugfix: ngx_http_redis_module might issue the error message 86 | "redis sent invalid trailer". For more information see: 87 | $ diff -ruN \ 88 | nginx-0.8.34/src/http/modules/ngx_http_memcached_module.c \ 89 | nginx-0.8.35/src/http/modules/ngx_http_memcached_module.c 90 | 91 | *) Change: now the $redis_db set is not obligatory; default 92 | value is "0". 93 | 94 | 95 | Changes with ngx_http_redis 0.3.1 26 Dec 2009 96 | 97 | *) Change: return 502 instead of 404 for error. 98 | 99 | *) Change: better error messages diagnostics. 100 | 101 | *) Bugfix: improve interoperability with redis; the bug had 102 | appeared in 0.3.0. 103 | 104 | 105 | Changes with ngx_http_redis 0.3.0 23 Dec 2009 106 | 107 | *) Compatibility with latest stable (0.7.64) and 108 | development 0.8.31 releases. 109 | 110 | *) Bugfix: multiple commands issue with interoperability with 111 | redis; the bug had appeared in 0.2.0. 112 | 113 | *) Feature: redis_bind directive merge from original 114 | memcached module (for 0.8.22 and later). 115 | 116 | 117 | Changes with ngx_http_redis 0.2.0 19 Sep 2009 118 | 119 | *) Feature: the $redis_db variable: now the ngx_http_redis 120 | module uses the $redis_db variable value as the parameter 121 | for SELECT command. 122 | 123 | *) Cleanup: style/spaces fixes. 124 | 125 | 126 | Changes with ngx_http_redis 0.1.2 14 Sep 2009 127 | 128 | *) Change: backport to 0.7.61. 129 | 130 | 131 | Changes with ngx_http_redis 0.1.1 31 Aug 2009 132 | 133 | *) Change: compatibility with nginx 0.8.11. 134 | 135 | *) Cleanup: mlcf -> rlcf, i.e. 136 | (memcached location configuration) -> redis... 137 | 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2002-2009 Igor Sysoev 3 | * Copyright (C) 2009-2025 Sergey A. Osokin 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 18 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | */ 26 | -------------------------------------------------------------------------------- /MERGED: -------------------------------------------------------------------------------- 1 | Here's the list of commits, merged from the recent snapshot 2 | of the ngx_http_memcached_module, 3 | https://github.com/nginx/nginx/commits/master/src/http/modules/ngx_http_memcached_module.c 4 | 5 | Please keep the list in order of original commits. 6 | 7 | e59c209 8 | b835b57 9 | 1305b84 10 | 372b624 11 | 510986b 12 | 00ef9ff 13 | be79f5c 14 | 958d4a0 15 | 02ce6c4 16 | 12300c2 17 | cc87023 18 | 74b7a91 19 | 416b922 20 | 2c0ea0f - partially, another review/test required 21 | 5d143ca 22 | 23a9596 - review/test required 23 | b74f8ff 24 | 05552a2 25 | ffe4f11 26 | e19f005 27 | f5f4126 28 | 05b1a8f 29 | 6bdcc58 30 | bd375b9 31 | 72e9287 32 | 61b09e5 33 | 15b7420 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX HTTP Redis module 2 | 3 | NGINX HTTP Redis module is a dynamic module for [NGINX](https://nginx.org/en/download.html) 4 | that enables basic caching function with [Redis](https://redis.io/downloads/). 5 | 6 | # Description 7 | 8 | The [Redis protocol](https://redis.io/topics/protocol), 9 | not yet fully implemented with the module, but `AUTH`, `GET`, and 10 | `SELECT` commands only. 11 | 12 | # Directives 13 | 14 | ### `redis_bind` 15 | 16 | | | | 17 | |--------- | ------------------------ | 18 | | Syntax: | *redis_bind [addr]* | 19 | | Default: | \- | 20 | | Context: | *http, server, localtion* | 21 | 22 | Use the following IP address as the source address for redis connections. 23 | 24 | ### `redis_buffer_size` 25 | 26 | | | | 27 | |--------- | ------------------------ | 28 | | Syntax: | *redis_buffer_size [size]* | 29 | | Default: | see `getpagesize(2)` | 30 | | Context: | *http, server, location* | 31 | 32 | The recv/send buffer size, in bytes. 33 | 34 | ### `redis_connect_timeout` 35 | 36 | | | | 37 | |--------- | ------------------------ | 38 | | Syntax: | *redis_connect_timeout [time]* | 39 | | Default: | `redis_connect_timeout 60000ms;` | 40 | | Context: | *http, server, location* | 41 | 42 | The timeout for connecting to redis, in milliseconds. 43 | 44 | ### `redis_gzip_flag` 45 | 46 | | | | 47 | |--------- | ------------------------ | 48 | | Syntax: | *redis_gzip_flag [number]* | 49 | | Default: | `redis_gzip_flag unset;` | 50 | | Context: | *location* | 51 | 52 | Reimplementation of [memcached_gzip_flag](http://nginx.org/en/docs/http/ngx_http_memcached_module.html#memcached_gzip_flag). 53 | 54 | ### `redis_next_upstream` 55 | 56 | | | | 57 | |--------- | ------------------------ | 58 | | Syntax: | *redis_next_upstream [error] [timeout] [invalid_response] [not_found] [off]* | 59 | | Default: | `redis_next_upstream error timeout;` | 60 | | Context: | *http, server, location* | 61 | 62 | Which failure conditions should cause the request to be forwarded to another upstream 63 | server? Applies only when the value in `redis_pass_` is an upstream with two or more servers. 64 | 65 | ### `redis_next_upstream_timeout` 66 | 67 | | | | 68 | |--------- | ------------------------ | 69 | | Syntax: | *redis_next_upstream_timeout [time]* | 70 | | Default: | `redis_next_upstream_timeout 0;` | 71 | | Context: | *http, server, location* | 72 | 73 | Limits the time during which a request can be passed to the next server. 74 | The `0` value turns off this limitation. 75 | 76 | ### `redis_next_upstream_tries` 77 | 78 | | | | 79 | |--------- | ------------------------ | 80 | | Syntax: | *redis_next_upstream_tries [number]* | 81 | | Default: | `redis_next_upstream_tries 0;` | 82 | | Context: | *http, server, location* | 83 | 84 | Limits the number of possible tries for passing a request to the next server. 85 | The `0` value turns off this limitation. 86 | 87 | ### `redis_pass` 88 | 89 | | | | 90 | |--------- | ------------------------ | 91 | | Syntax: | *redis_pass [name:port]* | 92 | | Default: | \- | 93 | | Context: | *http, server, location* | 94 | 95 | The backend should set the data in redis. The redis key is `/uri?args`. 96 | 97 | ### `redis_read_timeout` 98 | 99 | | | | 100 | |--------- | ------------------------ | 101 | | Syntax: | *redis_read_timeout [time]* | 102 | | Default: | `redis_read_timeout 60000ms;` | 103 | | Context: | *http, server, location* | 104 | 105 | The timeout for reading from redis, in milliseconds. 106 | 107 | ### `redis_send_timeout` 108 | 109 | | | | 110 | |--------- | ------------------------ | 111 | | Syntax: | *redis_send_timeout [time]* | 112 | | Default: | `redis_send_timeout 60000ms;` | 113 | | Context: | *http, server, location* | 114 | 115 | The timeout for sending to redis, in milliseconds. 116 | 117 | ### `redis_socket_keepalive` 118 | 119 | | | | 120 | |--------- | ------------------------ | 121 | | Syntax: | *redis_socket_keepalive on | off* | 122 | | Default: | `redis_socket_keepalive off;` | 123 | | Context: | *http, server, location* | 124 | 125 | Configures the “TCP keepalive” behavior for outgoing connections to a 126 | redis server. By default, the operating system's settings are in effect 127 | for the socket. If the directive is set to the value " `on` ", the 128 | `SO_KEEPALIVE` socket option is turned on for the socket. 129 | 130 | 131 | # Variables 132 | 133 | ### `$redis_auth` 134 | 135 | The `PASSWORD` value for the redis `AUTH` command (since 0.3.9). 136 | 137 | ### `$redis_db` 138 | 139 | The number of redis database; default value is `0` if not defined. 140 | 141 | ### `$redis_key` 142 | 143 | The value of the redis key. 144 | 145 | 146 | # Installation 147 | 148 | You'll need to re-compile Nginx from source to include this module. 149 | Modify your compile of Nginx by adding the following directive 150 | (modified to suit your path of course): 151 | 152 | ``` 153 | ./configure --add-module=/absolute/path/to/ngx_http_redis 154 | make 155 | make install 156 | ``` 157 | 158 | Alternatively, it's possible to compile the dynamic `ngx_http_redis` 159 | module. The nginx source is required. 160 | 161 | ``` 162 | ./configure --compat --add-dynamic-module=/absolute/path/to/ngx_http_redis 163 | make modules 164 | ``` 165 | 166 | 167 | # Usage 168 | 169 | ### Example 1 170 | 171 | 172 | ``` 173 | http 174 | { 175 | ... 176 | server { 177 | location / { 178 | set $redis_key "$uri?$args"; 179 | redis_pass 127.0.0.1:6379; 180 | error_page 404 502 504 = @fallback; 181 | } 182 | 183 | location @fallback { 184 | proxy_pass backend; 185 | } 186 | } 187 | } 188 | ``` 189 | 190 | ### Example 2 191 | 192 | Capture the `User-Agent` `HTTP` header, query to redis database 193 | for lookup appropriate backend. 194 | 195 | [Eval module](http://www.grid.net.ru/nginx/eval.en.html) is required. 196 | 197 | ``` 198 | http 199 | { 200 | ... 201 | upstream redis { 202 | server 127.0.0.1:6379; 203 | } 204 | 205 | server { 206 | ... 207 | location / { 208 | 209 | eval_escalate on; 210 | 211 | eval $answer { 212 | set $redis_key "$http_user_agent"; 213 | redis_pass redis; 214 | } 215 | 216 | proxy_pass $answer; 217 | } 218 | ... 219 | } 220 | } 221 | ``` 222 | 223 | 224 | ### Example 3 225 | 226 | Compile nginx with [ngx_http_redis](https://github.com/osokin/ngx_http_redis/) 227 | and [ngx_http_gunzip](https://nginx.org/en/docs/http/ngx_http_gunzip_module.html) 228 | modules. 229 | Gzip content and put it into a redis database. 230 | 231 | ``` 232 | % cat index.html 233 | Hello from redis! 234 | % gzip index.html 235 | % cat index.html.gz | redis-cli -x set /index.html 236 | OK 237 | % cat index.html.gz | redis-cli -x set / 238 | OK 239 | % 240 | ``` 241 | 242 | Follow the next configuration, nginx: 243 | - gets a gzipped content from the redis database; 244 | - unzips it; 245 | - reply with the gunzipped content. 246 | 247 | ``` 248 | http 249 | { 250 | ... 251 | upstream redis { 252 | server 127.0.0.1:6379; 253 | } 254 | 255 | server { 256 | ... 257 | location / { 258 | gunzip on; 259 | redis_gzip_flag 1; 260 | set $redis_key "$uri"; 261 | redis_pass redis; 262 | } 263 | } 264 | } 265 | ``` 266 | 267 | ### Example 4 268 | 269 | The same as Example 1 but with `AUTH`. 270 | 271 | ``` 272 | http 273 | { 274 | ... 275 | server { 276 | location / { 277 | set $redis_auth somepasswd; 278 | set $redis_key "$uri?$args"; 279 | redis_pass 127.0.0.1:6379; 280 | error_page 404 502 504 = @fallback; 281 | } 282 | } 283 | } 284 | 285 | ``` 286 | 287 | ### Development and testing 288 | 289 | 1. Clone the [nginx-tests](https://github.com/nginx/nginx-test) repository. 290 | ``` 291 | % git clone https://github.com/nginx/nginx-tests.git 292 | ``` 293 | 294 | 2. Build [and install] nginx 295 | 296 | Installation of nginx is the optional step. 297 | Also, install: 298 | - redis server; 299 | - perl; 300 | - perl module for redis. 301 | 302 | 303 | For FreeBSD ports tree: 304 | ``` 305 | % cd /usr/ports/www/nginx-devel && make [ && make install ] 306 | % ASSUME_ALWAYS_YES=true pkg install redis p5-Redis 307 | ``` 308 | 309 | 310 | 3. Run the following command 311 | 312 | ``` 313 | % PERL5LIB=/path/to/nginx-tests/dir \ 314 | TEST_NGINX_VERBOSE=1 \ 315 | TEST_NGINX_LEAVE=1 \ 316 | TEST_NGINX_BINARY=/path/to/nginx/binary \ 317 | TEST_NGINX_MODULES=/path/to/nginx/modules/directory \ 318 | TEST_NGINX_GLOBALS="load_module /path/to/nginx/modules/directory/ngx_http_redis_module.so;" \ 319 | prove 320 | ``` 321 | 322 | Here's the example, when nginx built from the FreeBSD ports tree 323 | ``` 324 | % PERL5LIB=/home/nginx/github/nginx-tests/lib:${PERL5LIB} \ 325 | TEST_NGINX_VERBOSE=1 \ 326 | TEST_NGINX_LEAVE=1 \ 327 | TEST_NGINX_BINARY=/usr/local/sbin/nginx \ 328 | TEST_NGINX_MODULES=/usr/local/libexec/nginx \ 329 | TEST_NGINX_GLOBALS="load_module /usr/local/libexec/nginx/ngx_http_redis_module.so;" \ 330 | prove -prvv 331 | ``` 332 | 333 | # Thanks to 334 | 335 | - [Maxim Dounin](https://mdounin.ru/) 336 | - [Vsevolod Stakhov](https://github.com/vstakhov) 337 | - [Ezra Zygmuntowicz](https://github.com/ezmobius) 338 | 339 | 340 | # Special thanks to 341 | 342 | - [Evan Miller](https://github.com/evanmiller) for his 343 | [Guide To Nginx Module Development](https://www.evanmiller.org/nginx-modules-guide.html) 344 | and [Advanced Topics In Nginx Module Development](https://www.evanmiller.org/nginx-modules-guide-advanced.html) 345 | - [Valery Kholodkov](https://github.com/vkholodkov) for his 346 | [Nginx modules development](http://antoine.bonavita.free.fr/nginx_mod_dev_en.html), 347 | English translation 348 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_redis_module 2 | 3 | HTTP_REDIS_SRCS=" \ 4 | $ngx_addon_dir/ngx_http_redis_module.c 5 | " 6 | if test -n "$ngx_module_link"; then 7 | ngx_module_type=HTTP 8 | ngx_module_name=$ngx_addon_name 9 | ngx_module_incs= 10 | ngx_module_deps= 11 | ngx_module_srcs="$HTTP_REDIS_SRCS" 12 | ngx_module_libs= 13 | 14 | . auto/module 15 | else 16 | HTTP_MODULES="$HTTP_MODULES $ngx_addon_name" 17 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $HTTP_REDIS_SRCS" 18 | fi 19 | -------------------------------------------------------------------------------- /ngx_http_redis_module.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (C) Igor Sysoev 4 | * Copyright (C) Sergey A. Osokin 5 | */ 6 | 7 | #define NGX_ESCAPE_REDIS 4 8 | 9 | #define REDIS_AUTH_CMD "*2\r\n$4\r\nauth\r\n" 10 | #define REDIS_GET_CMD "*2\r\n$3\r\nget\r\n" 11 | #define REDIS_SELECT_CMD "*2\r\n$6\r\nselect\r\n" 12 | #define REDIS_PLUSOKCRLF "+OK\r\n" 13 | #define REDIS_ERR "$-1" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | typedef struct { 22 | ngx_http_upstream_conf_t upstream; 23 | ngx_int_t index; 24 | ngx_int_t db; 25 | ngx_int_t auth; 26 | ngx_uint_t gzip_flag; 27 | } ngx_http_redis_loc_conf_t; 28 | 29 | 30 | typedef struct { 31 | size_t rest; 32 | ngx_http_request_t *request; 33 | ngx_str_t key; 34 | } ngx_http_redis_ctx_t; 35 | 36 | 37 | static ngx_int_t ngx_http_redis_create_request(ngx_http_request_t *r); 38 | static ngx_int_t ngx_http_redis_reinit_request(ngx_http_request_t *r); 39 | static ngx_int_t ngx_http_redis_process_header(ngx_http_request_t *r); 40 | static ngx_int_t ngx_http_redis_filter_init(void *data); 41 | static ngx_int_t ngx_http_redis_filter(void *data, ssize_t bytes); 42 | static void ngx_http_redis_abort_request(ngx_http_request_t *r); 43 | static void ngx_http_redis_finalize_request(ngx_http_request_t *r, 44 | ngx_int_t rc); 45 | 46 | static ngx_int_t ngx_http_redis_add_variables(ngx_conf_t *cf); 47 | static void *ngx_http_redis_create_loc_conf(ngx_conf_t *cf); 48 | static char *ngx_http_redis_merge_loc_conf(ngx_conf_t *cf, 49 | void *parent, void *child); 50 | 51 | static char *ngx_http_redis_pass(ngx_conf_t *cf, ngx_command_t *cmd, 52 | void *conf); 53 | 54 | static ngx_conf_bitmask_t ngx_http_redis_next_upstream_masks[] = { 55 | { ngx_string("error"), NGX_HTTP_UPSTREAM_FT_ERROR }, 56 | { ngx_string("timeout"), NGX_HTTP_UPSTREAM_FT_TIMEOUT }, 57 | { ngx_string("invalid_response"), NGX_HTTP_UPSTREAM_FT_INVALID_HEADER }, 58 | { ngx_string("not_found"), NGX_HTTP_UPSTREAM_FT_HTTP_404 }, 59 | { ngx_string("off"), NGX_HTTP_UPSTREAM_FT_OFF }, 60 | { ngx_null_string, 0 } 61 | }; 62 | 63 | 64 | static ngx_command_t ngx_http_redis_commands[] = { 65 | 66 | { ngx_string("redis_pass"), 67 | NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1, 68 | ngx_http_redis_pass, 69 | NGX_HTTP_LOC_CONF_OFFSET, 70 | 0, 71 | NULL }, 72 | 73 | { ngx_string("redis_bind"), 74 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12, 75 | ngx_http_upstream_bind_set_slot, 76 | NGX_HTTP_LOC_CONF_OFFSET, 77 | offsetof(ngx_http_redis_loc_conf_t, upstream.local), 78 | NULL }, 79 | 80 | { ngx_string("redis_socket_keepalive"), 81 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 82 | ngx_conf_set_flag_slot, 83 | NGX_HTTP_LOC_CONF_OFFSET, 84 | offsetof(ngx_http_redis_loc_conf_t, upstream.socket_keepalive), 85 | NULL }, 86 | 87 | { ngx_string("redis_connect_timeout"), 88 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 89 | ngx_conf_set_msec_slot, 90 | NGX_HTTP_LOC_CONF_OFFSET, 91 | offsetof(ngx_http_redis_loc_conf_t, upstream.connect_timeout), 92 | NULL }, 93 | 94 | { ngx_string("redis_send_timeout"), 95 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 96 | ngx_conf_set_msec_slot, 97 | NGX_HTTP_LOC_CONF_OFFSET, 98 | offsetof(ngx_http_redis_loc_conf_t, upstream.send_timeout), 99 | NULL }, 100 | 101 | { ngx_string("redis_buffer_size"), 102 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 103 | ngx_conf_set_size_slot, 104 | NGX_HTTP_LOC_CONF_OFFSET, 105 | offsetof(ngx_http_redis_loc_conf_t, upstream.buffer_size), 106 | NULL }, 107 | 108 | { ngx_string("redis_read_timeout"), 109 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 110 | ngx_conf_set_msec_slot, 111 | NGX_HTTP_LOC_CONF_OFFSET, 112 | offsetof(ngx_http_redis_loc_conf_t, upstream.read_timeout), 113 | NULL }, 114 | 115 | { ngx_string("redis_next_upstream"), 116 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, 117 | ngx_conf_set_bitmask_slot, 118 | NGX_HTTP_LOC_CONF_OFFSET, 119 | offsetof(ngx_http_redis_loc_conf_t, upstream.next_upstream), 120 | &ngx_http_redis_next_upstream_masks }, 121 | 122 | { ngx_string("redis_next_upstream_tries"), 123 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 124 | ngx_conf_set_num_slot, 125 | NGX_HTTP_LOC_CONF_OFFSET, 126 | offsetof(ngx_http_redis_loc_conf_t, upstream.next_upstream_tries), 127 | NULL }, 128 | 129 | { ngx_string("redis_next_upstream_timeout"), 130 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 131 | ngx_conf_set_msec_slot, 132 | NGX_HTTP_LOC_CONF_OFFSET, 133 | offsetof(ngx_http_redis_loc_conf_t, upstream.next_upstream_timeout), 134 | NULL }, 135 | 136 | { ngx_string("redis_gzip_flag"), 137 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 138 | ngx_conf_set_num_slot, 139 | NGX_HTTP_LOC_CONF_OFFSET, 140 | offsetof(ngx_http_redis_loc_conf_t, gzip_flag), 141 | NULL }, 142 | 143 | ngx_null_command 144 | }; 145 | 146 | 147 | static ngx_http_module_t ngx_http_redis_module_ctx = { 148 | ngx_http_redis_add_variables, /* preconfiguration */ 149 | NULL, /* postconfiguration */ 150 | 151 | NULL, /* create main configuration */ 152 | NULL, /* init main configuration */ 153 | 154 | NULL, /* create server configuration */ 155 | NULL, /* merge server configuration */ 156 | 157 | ngx_http_redis_create_loc_conf, /* create location configuration */ 158 | ngx_http_redis_merge_loc_conf /* merge location configuration */ 159 | }; 160 | 161 | 162 | ngx_module_t ngx_http_redis_module = { 163 | NGX_MODULE_V1, 164 | &ngx_http_redis_module_ctx, /* module context */ 165 | ngx_http_redis_commands, /* module directives */ 166 | NGX_HTTP_MODULE, /* module type */ 167 | NULL, /* init master */ 168 | NULL, /* init module */ 169 | NULL, /* init process */ 170 | NULL, /* init thread */ 171 | NULL, /* exit thread */ 172 | NULL, /* exit process */ 173 | NULL, /* exit master */ 174 | NGX_MODULE_V1_PADDING 175 | }; 176 | 177 | static ngx_str_t ngx_http_redis_key = ngx_string("redis_key"); 178 | static ngx_str_t ngx_http_redis_db = ngx_string("redis_db"); 179 | static ngx_str_t ngx_http_redis_auth = ngx_string("redis_auth"); 180 | static ngx_uint_t ngx_http_redis_db_index; 181 | static ngx_uint_t ngx_http_redis_auth_index; 182 | 183 | 184 | #define NGX_HTTP_REDIS_END (sizeof(ngx_http_redis_end) - 1) 185 | static u_char ngx_http_redis_end[] = CRLF; 186 | 187 | 188 | static ngx_int_t 189 | ngx_http_redis_handler(ngx_http_request_t *r) 190 | { 191 | ngx_int_t rc; 192 | ngx_http_upstream_t *u; 193 | ngx_http_redis_ctx_t *ctx; 194 | ngx_http_redis_loc_conf_t *rlcf; 195 | 196 | if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) { 197 | return NGX_HTTP_NOT_ALLOWED; 198 | } 199 | 200 | rc = ngx_http_discard_request_body(r); 201 | 202 | if (rc != NGX_OK) { 203 | return rc; 204 | } 205 | 206 | if (ngx_http_set_content_type(r) != NGX_OK) { 207 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 208 | } 209 | 210 | if (ngx_http_upstream_create(r) != NGX_OK) { 211 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 212 | } 213 | 214 | u = r->upstream; 215 | 216 | ngx_str_set(&u->schema, "redis://"); 217 | 218 | u->output.tag = (ngx_buf_tag_t) &ngx_http_redis_module; 219 | 220 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 221 | 222 | u->conf = &rlcf->upstream; 223 | 224 | u->create_request = ngx_http_redis_create_request; 225 | u->reinit_request = ngx_http_redis_reinit_request; 226 | u->process_header = ngx_http_redis_process_header; 227 | u->abort_request = ngx_http_redis_abort_request; 228 | u->finalize_request = ngx_http_redis_finalize_request; 229 | 230 | ctx = ngx_palloc(r->pool, sizeof(ngx_http_redis_ctx_t)); 231 | if (ctx == NULL) { 232 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 233 | } 234 | 235 | ctx->rest = NGX_HTTP_REDIS_END; 236 | ctx->request = r; 237 | 238 | ngx_http_set_ctx(r, ctx, ngx_http_redis_module); 239 | 240 | u->input_filter_init = ngx_http_redis_filter_init; 241 | u->input_filter = ngx_http_redis_filter; 242 | u->input_filter_ctx = ctx; 243 | 244 | r->main->count++; 245 | 246 | ngx_http_upstream_init(r); 247 | 248 | return NGX_DONE; 249 | } 250 | 251 | 252 | static ngx_int_t 253 | ngx_http_redis_create_request(ngx_http_request_t *r) 254 | { 255 | size_t len = 0; 256 | uintptr_t escape; 257 | ngx_buf_t *b; 258 | ngx_chain_t *cl; 259 | ngx_http_redis_ctx_t *ctx; 260 | ngx_http_variable_value_t *vv[3]; 261 | ngx_http_redis_loc_conf_t *rlcf; 262 | u_char lenbuf[NGX_INT_T_LEN]; 263 | 264 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 265 | 266 | vv[0] = ngx_http_get_indexed_variable(r, ngx_http_redis_auth_index); 267 | if (vv[0] == NULL || vv[0]->not_found || vv[0]->len == 0) { 268 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 269 | "no auth command provided" ); 270 | } else { 271 | len += sizeof(REDIS_AUTH_CMD) + sizeof("$") - 1; 272 | len += ngx_sprintf(lenbuf, "%d", vv[0]->len) - lenbuf; 273 | len += sizeof(CRLF) - 1 + vv[0]->len; 274 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 275 | "auth info: %s", vv[0]->data); 276 | } 277 | len += sizeof(CRLF) - 1; 278 | 279 | vv[1] = ngx_http_get_indexed_variable(r, ngx_http_redis_db_index); 280 | 281 | /* 282 | * If user do not select redis database in nginx.conf by redis_db 283 | * variable, just add size of "select 0" to request. This is add 284 | * some overhead in talk with redis, but this way simplify parsing 285 | * the redis answer in ngx_http_redis_process_header(). 286 | */ 287 | if (vv[1] == NULL || vv[1]->not_found || vv[1]->len == 0) { 288 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 289 | "select 0 redis database" ); 290 | len += sizeof(REDIS_SELECT_CMD) + sizeof("$1") + sizeof(CRLF) + sizeof("0") - 1; 291 | 292 | } else { 293 | len += sizeof(REDIS_SELECT_CMD) + sizeof("$") - 1; 294 | len += ngx_sprintf(lenbuf, "%d", vv[1]->len) - lenbuf; 295 | len += sizeof(CRLF) - 1 + vv[1]->len; 296 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 297 | "select %s redis database", vv[1]->data); 298 | } 299 | len += sizeof(CRLF) - 1; 300 | 301 | vv[2] = ngx_http_get_indexed_variable(r, rlcf->index); 302 | 303 | /* If nginx.conf have no redis_key return error. */ 304 | if (vv[2] == NULL || vv[2]->not_found || vv[2]->len == 0) { 305 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 306 | "the \"$redis_key\" variable is not set"); 307 | return NGX_ERROR; 308 | } 309 | 310 | /* Count have space required escape symbols. */ 311 | escape = 2 * ngx_escape_uri(NULL, vv[2]->data, vv[2]->len, NGX_ESCAPE_REDIS); 312 | 313 | len += sizeof(REDIS_GET_CMD) + sizeof("$") - 1; 314 | len += ngx_sprintf(lenbuf, "%d", vv[2]->len) - lenbuf; 315 | len += sizeof(CRLF) - 1 + vv[2]->len + escape + sizeof(CRLF) - 1; 316 | 317 | /* Create temporary buffer for request with size len. */ 318 | b = ngx_create_temp_buf(r->pool, len); 319 | if (b == NULL) { 320 | return NGX_ERROR; 321 | } 322 | 323 | cl = ngx_alloc_chain_link(r->pool); 324 | if (cl == NULL) { 325 | return NGX_ERROR; 326 | } 327 | 328 | cl->buf = b; 329 | cl->next = NULL; 330 | 331 | r->upstream->request_bufs = cl; 332 | 333 | /* add "auth " for request */ 334 | if (vv[0] != NULL && !(vv[0]->not_found) && vv[0]->len != 0) { 335 | /* Add "auth " for request. */ 336 | b->last = ngx_sprintf(b->last, "%s$%d%s", REDIS_AUTH_CMD, vv[0]->len, CRLF); 337 | b->last = ngx_copy(b->last, vv[0]->data, vv[0]->len); 338 | *b->last++ = CR; *b->last++ = LF; 339 | } 340 | 341 | /* Get context redis_db from configuration file. */ 342 | ctx = ngx_http_get_module_ctx(r, ngx_http_redis_module); 343 | 344 | ctx->key.data = b->last; 345 | 346 | /* 347 | * Add "0" as redis number db to request if redis_db undefined, 348 | * othervise add real number from context. 349 | */ 350 | if (vv[1] == NULL || vv[1]->not_found || vv[1]->len == 0) { 351 | b->last = ngx_sprintf(b->last, "%s$1%s", REDIS_SELECT_CMD, CRLF); 352 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 353 | "select 0 redis database" ); 354 | *b->last++ = '0'; 355 | 356 | } else { 357 | b->last = ngx_sprintf(b->last, "%s$%d%s", REDIS_SELECT_CMD, vv[1]->len, CRLF); 358 | b->last = ngx_copy(b->last, vv[1]->data, vv[1]->len); 359 | ctx->key.len = b->last - ctx->key.data; 360 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 361 | "select %V redis database", &ctx->key); 362 | } 363 | 364 | /* Add "\r\n". */ 365 | *b->last++ = CR; *b->last++ = LF; 366 | 367 | b->last = ngx_sprintf(b->last, "%s$%d%s", REDIS_GET_CMD, vv[2]->len, CRLF); 368 | /* Get context redis_key from nginx.conf. */ 369 | ctx = ngx_http_get_module_ctx(r, ngx_http_redis_module); 370 | 371 | ctx->key.data = b->last; 372 | 373 | /* 374 | * If no escape symbols then copy data as is, othervise use 375 | * escape-copy function. 376 | */ 377 | 378 | if (escape == 0) { 379 | b->last = ngx_copy(b->last, vv[2]->data, vv[2]->len); 380 | 381 | } else { 382 | b->last = (u_char *) ngx_escape_uri(b->last, vv[2]->data, vv[2]->len, 383 | NGX_ESCAPE_REDIS); 384 | } 385 | 386 | ctx->key.len = b->last - ctx->key.data; 387 | 388 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 389 | "http redis request: \"%V\"", &ctx->key); 390 | 391 | /* Add one more "\r\n". */ 392 | *b->last++ = CR; *b->last++ = LF; 393 | 394 | /* 395 | * Summary, the request looks like this: 396 | * "auth $redis_auth\r\nselect $redis_db\r\nget $redis_key\r\n", where 397 | * $redis_auth, $redis_db and $redis_key are variable's values. 398 | */ 399 | 400 | return NGX_OK; 401 | } 402 | 403 | 404 | static ngx_int_t 405 | ngx_http_redis_reinit_request(ngx_http_request_t *r) 406 | { 407 | return NGX_OK; 408 | } 409 | 410 | 411 | static ngx_int_t 412 | ngx_http_redis_process_header(ngx_http_request_t *r) 413 | { 414 | u_char *p, *len; 415 | u_int c, try; 416 | ngx_str_t line; 417 | ngx_table_elt_t *h; 418 | ngx_http_upstream_t *u; 419 | ngx_http_redis_ctx_t *ctx; 420 | ngx_http_redis_loc_conf_t *rlcf; 421 | ngx_http_variable_value_t *vv; 422 | ngx_int_t no_auth_cmd; 423 | 424 | vv = ngx_http_get_indexed_variable(r, ngx_http_redis_auth_index); 425 | no_auth_cmd = (vv == NULL || vv->not_found || vv->len == 0); 426 | 427 | c = try = 0; 428 | 429 | u = r->upstream; 430 | 431 | p = u->buffer.pos; 432 | 433 | if ((u->buffer.last - p) <= 0) { 434 | return NGX_AGAIN; 435 | } 436 | 437 | /* 438 | * Good answer from redis should looks like this: 439 | * "+OK\r\n+OK\r\n$8\r\n12345678\r\n" 440 | * 441 | * Here is: 442 | * "+OK\r\n+OK\r\n" is answer for first two commands 443 | * "auth password" and "select 0" 444 | * Next two strings are answer for command "get $redis_key", where 445 | * 446 | * "$8" is length of following next string and 447 | * "12345678" is value of $redis_key, the string. 448 | * 449 | * So, if the first symbol is: 450 | * "+" (good answer) - try to find 2 or 3 strings; 451 | * "-" (bad answer) - try to find 1 string; 452 | * othervise answer is invalid. 453 | */ 454 | if (*p == '+') { 455 | if (no_auth_cmd) { 456 | try = 2; 457 | 458 | } else { 459 | try = 3; 460 | } 461 | 462 | } else if (*p == '-') { 463 | try = 1; 464 | 465 | } else { 466 | goto no_valid; 467 | } 468 | 469 | for (p = u->buffer.pos; p < u->buffer.last; p++) { 470 | if (*p == LF) { 471 | c++; 472 | if (c == try) { 473 | goto found; 474 | } 475 | } 476 | } 477 | 478 | return NGX_AGAIN; 479 | 480 | found: 481 | 482 | *p = '\0'; 483 | 484 | line.len = p - u->buffer.pos - 1; 485 | line.data = u->buffer.pos; 486 | 487 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 488 | "redis: \"%V\"", &line); 489 | 490 | p = u->buffer.pos; 491 | 492 | /* Get context of redis_key for future error messages, i.e. ctx->key */ 493 | ctx = ngx_http_get_module_ctx(r, ngx_http_redis_module); 494 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 495 | 496 | /* Compare pointer and error message, if yes go to no_valid */ 497 | if (ngx_strncmp(p, "-ERR", sizeof("-ERR") - 1) == 0) { 498 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 499 | "redis sent error in response \"%V\" " 500 | "for key \"%V\"", 501 | &line, &ctx->key); 502 | 503 | goto no_valid; 504 | } 505 | 506 | /* Compare pointer and good message, if yes move on the pointer */ 507 | vv = ngx_http_get_indexed_variable(r, ngx_http_redis_auth_index); 508 | if (no_auth_cmd) { 509 | if (ngx_strncmp(p, REDIS_PLUSOKCRLF, sizeof(REDIS_PLUSOKCRLF) - 1) == 0) { 510 | p += sizeof(REDIS_PLUSOKCRLF) - 1; 511 | 512 | } else { 513 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 514 | "+OK\\r\\n was expected here"); 515 | } 516 | 517 | } else { /* check double of the "+OK\r\n" */ 518 | if ((ngx_strncmp(p, REDIS_PLUSOKCRLF, sizeof(REDIS_PLUSOKCRLF) - 1) == 0) && 519 | (ngx_strncmp(p + sizeof(REDIS_PLUSOKCRLF) - 1, 520 | REDIS_PLUSOKCRLF, sizeof(REDIS_PLUSOKCRLF) - 1) == 0)) { 521 | p += 2 * (sizeof(REDIS_PLUSOKCRLF) - 1); 522 | 523 | } else { 524 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 525 | "+OK\\r\\n+OK\\r\\n was expected here"); 526 | } 527 | } 528 | 529 | /* 530 | * Compare pointer and "get" answer. As said before, "$" means, that 531 | * next symbols are length for upcoming key, "-1" means no key. 532 | * Set 404 and return. 533 | */ 534 | if (ngx_strncmp(p, REDIS_ERR, sizeof(REDIS_ERR) - 1) == 0) { 535 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 536 | "key: \"%V\" was not found by redis", &ctx->key); 537 | 538 | u->headers_in.content_length_n = 0; 539 | u->headers_in.status_n = 404; 540 | u->state->status = 404; 541 | u->buffer.pos = p + sizeof(REDIS_ERR CRLF) - 1; 542 | u->keepalive = 1; 543 | 544 | return NGX_OK; 545 | } 546 | 547 | /* Compare pointer and "get" answer, if "$"... */ 548 | if (ngx_strncmp(p, "$", sizeof("$") - 1) == 0) { 549 | 550 | /* move on pointer */ 551 | p += sizeof("$") - 1; 552 | 553 | /* set len to pointer */ 554 | len = p; 555 | 556 | /* if defined gzip_flag... */ 557 | if (rlcf->gzip_flag) { 558 | /* hash init */ 559 | h = ngx_list_push(&r->headers_out.headers); 560 | if (h == NULL) { 561 | return NGX_ERROR; 562 | } 563 | 564 | /* 565 | * add Content-Encoding header for future gunzipping 566 | * with ngx_http_gunzip_filter module 567 | */ 568 | h->hash = 1; 569 | h->next = NULL; 570 | ngx_str_set(&h->key, "Content-Encoding"); 571 | ngx_str_set(&h->value, "gzip"); 572 | r->headers_out.content_encoding = h; 573 | } 574 | 575 | /* try to find end of string */ 576 | while (*p && *p++ != CR) { /* void */ } 577 | 578 | /* 579 | * get the length of upcoming redis_key value, convert from ascii 580 | * if the length is empty, return 581 | */ 582 | u->headers_in.content_length_n = ngx_atoof(len, p - len - 1); 583 | if (u->headers_in.content_length_n == NGX_ERROR) { 584 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 585 | "redis sent invalid length in response \"%V\" " 586 | "for key \"%V\"", 587 | &line, &ctx->key); 588 | return NGX_HTTP_UPSTREAM_INVALID_HEADER; 589 | } 590 | 591 | /* The length of answer is not empty, set 200 */ 592 | u->headers_in.status_n = 200; 593 | u->state->status = 200; 594 | /* Set position to the first symbol of data and return */ 595 | u->buffer.pos = p + 1; 596 | 597 | return NGX_OK; 598 | } 599 | 600 | no_valid: 601 | 602 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 603 | "redis sent invalid response: \"%V\"", &line); 604 | 605 | return NGX_HTTP_UPSTREAM_INVALID_HEADER; 606 | } 607 | 608 | 609 | static ngx_int_t 610 | ngx_http_redis_filter_init(void *data) 611 | { 612 | ngx_http_redis_ctx_t *ctx = data; 613 | 614 | ngx_http_upstream_t *u; 615 | 616 | u = ctx->request->upstream; 617 | 618 | if (u->headers_in.status_n != 404) { 619 | u->length = u->headers_in.content_length_n + NGX_HTTP_REDIS_END; 620 | ctx->rest = NGX_HTTP_REDIS_END; 621 | 622 | } else { 623 | u->length = 0; 624 | } 625 | 626 | return NGX_OK; 627 | } 628 | 629 | 630 | static ngx_int_t 631 | ngx_http_redis_filter(void *data, ssize_t bytes) 632 | { 633 | ngx_http_redis_ctx_t *ctx = data; 634 | 635 | u_char *last; 636 | ngx_buf_t *b; 637 | ngx_chain_t *cl, **ll; 638 | ngx_http_upstream_t *u; 639 | 640 | u = ctx->request->upstream; 641 | b = &u->buffer; 642 | 643 | if (u->length == (ssize_t) ctx->rest) { 644 | 645 | if (bytes > u->length 646 | || ngx_strncmp(b->last, 647 | ngx_http_redis_end + NGX_HTTP_REDIS_END - ctx->rest, 648 | bytes) 649 | != 0) 650 | { 651 | ngx_log_error(NGX_LOG_ERR, ctx->request->connection->log, 0, 652 | "redis sent invalid trailer"); 653 | 654 | u->length = 0; 655 | ctx->rest = 0; 656 | 657 | return NGX_OK; 658 | } 659 | 660 | u->length -= bytes; 661 | ctx->rest -= bytes; 662 | 663 | if (u->length == 0) { 664 | u->keepalive = 1; 665 | } 666 | 667 | return NGX_OK; 668 | } 669 | 670 | for (cl = u->out_bufs, ll = &u->out_bufs; cl; cl = cl->next) { 671 | ll = &cl->next; 672 | } 673 | 674 | cl = ngx_chain_get_free_buf(ctx->request->pool, &u->free_bufs); 675 | if (cl == NULL) { 676 | return NGX_ERROR; 677 | } 678 | 679 | cl->buf->flush = 1; 680 | cl->buf->memory = 1; 681 | 682 | *ll = cl; 683 | 684 | last = b->last; 685 | cl->buf->pos = last; 686 | b->last += bytes; 687 | cl->buf->last = b->last; 688 | cl->buf->tag = u->output.tag; 689 | 690 | ngx_log_debug4(NGX_LOG_DEBUG_HTTP, ctx->request->connection->log, 0, 691 | "redis filter bytes:%z size:%z length:%O rest:%z", 692 | bytes, b->last - b->pos, u->length, ctx->rest); 693 | 694 | if (bytes <= (ssize_t) (u->length - NGX_HTTP_REDIS_END)) { 695 | u->length -= bytes; 696 | return NGX_OK; 697 | } 698 | 699 | last += (size_t) (u->length - NGX_HTTP_REDIS_END); 700 | 701 | if (bytes > u->length 702 | || ngx_strncmp(last, ngx_http_redis_end, b->last - last) != 0) 703 | { 704 | ngx_log_error(NGX_LOG_ERR, ctx->request->connection->log, 0, 705 | "redis sent invalid trailer"); 706 | 707 | b->last = last; 708 | cl->buf->last = last; 709 | u->length = 0; 710 | ctx->rest = 0; 711 | 712 | return NGX_OK; 713 | } 714 | 715 | ctx->rest -= b->last - last; 716 | b->last = last; 717 | cl->buf->last = last; 718 | u->length = ctx->rest; 719 | 720 | if (u->length == 0) { 721 | u->keepalive = 1; 722 | } 723 | 724 | return NGX_OK; 725 | } 726 | 727 | 728 | static void 729 | ngx_http_redis_abort_request(ngx_http_request_t *r) 730 | { 731 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 732 | "abort http redis request"); 733 | return; 734 | } 735 | 736 | 737 | static void 738 | ngx_http_redis_finalize_request(ngx_http_request_t *r, ngx_int_t rc) 739 | { 740 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 741 | "finalize http redis request"); 742 | return; 743 | } 744 | 745 | 746 | static void * 747 | ngx_http_redis_create_loc_conf(ngx_conf_t *cf) 748 | { 749 | ngx_http_redis_loc_conf_t *conf; 750 | 751 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_redis_loc_conf_t)); 752 | if (conf == NULL) { 753 | return NULL; 754 | } 755 | 756 | /* 757 | * set by ngx_pcalloc(): 758 | * 759 | * conf->upstream.bufs.num = 0; 760 | * conf->upstream.next_upstream = 0; 761 | * conf->upstream.temp_path = NULL; 762 | */ 763 | 764 | conf->upstream.local = NGX_CONF_UNSET_PTR; 765 | conf->upstream.socket_keepalive = NGX_CONF_UNSET; 766 | conf->upstream.next_upstream_tries = NGX_CONF_UNSET_UINT; 767 | conf->upstream.connect_timeout = NGX_CONF_UNSET_MSEC; 768 | conf->upstream.send_timeout = NGX_CONF_UNSET_MSEC; 769 | conf->upstream.read_timeout = NGX_CONF_UNSET_MSEC; 770 | conf->upstream.next_upstream_timeout = NGX_CONF_UNSET_MSEC; 771 | 772 | conf->upstream.buffer_size = NGX_CONF_UNSET_SIZE; 773 | 774 | /* the hardcoded values */ 775 | conf->upstream.cyclic_temp_file = 0; 776 | conf->upstream.buffering = 0; 777 | conf->upstream.ignore_client_abort = 0; 778 | conf->upstream.send_lowat = 0; 779 | conf->upstream.bufs.num = 0; 780 | conf->upstream.busy_buffers_size = 0; 781 | conf->upstream.max_temp_file_size = 0; 782 | conf->upstream.temp_file_write_size = 0; 783 | conf->upstream.intercept_errors = 1; 784 | conf->upstream.intercept_404 = 1; 785 | conf->upstream.pass_request_headers = 0; 786 | conf->upstream.pass_request_body = 0; 787 | conf->upstream.force_ranges = 1; 788 | 789 | conf->index = NGX_CONF_UNSET; 790 | conf->db = NGX_CONF_UNSET; 791 | conf->auth = NGX_CONF_UNSET; 792 | conf->gzip_flag = NGX_CONF_UNSET_UINT; 793 | 794 | return conf; 795 | } 796 | 797 | 798 | static char * 799 | ngx_http_redis_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 800 | { 801 | ngx_http_redis_loc_conf_t *prev = parent; 802 | ngx_http_redis_loc_conf_t *conf = child; 803 | 804 | ngx_conf_merge_ptr_value(conf->upstream.local, 805 | prev->upstream.local, NULL); 806 | 807 | ngx_conf_merge_value(conf->upstream.socket_keepalive, 808 | prev->upstream.socket_keepalive, 0); 809 | 810 | ngx_conf_merge_uint_value(conf->upstream.next_upstream_tries, 811 | prev->upstream.next_upstream_tries, 812 | 0); 813 | 814 | ngx_conf_merge_msec_value(conf->upstream.connect_timeout, 815 | prev->upstream.connect_timeout, 60000); 816 | 817 | ngx_conf_merge_msec_value(conf->upstream.send_timeout, 818 | prev->upstream.send_timeout, 60000); 819 | 820 | ngx_conf_merge_msec_value(conf->upstream.read_timeout, 821 | prev->upstream.read_timeout, 60000); 822 | 823 | ngx_conf_merge_msec_value(conf->upstream.next_upstream_timeout, 824 | prev->upstream.next_upstream_timeout, 0); 825 | 826 | ngx_conf_merge_size_value(conf->upstream.buffer_size, 827 | prev->upstream.buffer_size, 828 | (size_t) ngx_pagesize); 829 | 830 | ngx_conf_merge_bitmask_value(conf->upstream.next_upstream, 831 | prev->upstream.next_upstream, 832 | (NGX_CONF_BITMASK_SET 833 | |NGX_HTTP_UPSTREAM_FT_ERROR 834 | |NGX_HTTP_UPSTREAM_FT_TIMEOUT)); 835 | 836 | if (conf->upstream.next_upstream & NGX_HTTP_UPSTREAM_FT_OFF) { 837 | conf->upstream.next_upstream = NGX_CONF_BITMASK_SET 838 | |NGX_HTTP_UPSTREAM_FT_OFF; 839 | } 840 | 841 | if (conf->upstream.upstream == NULL) { 842 | conf->upstream.upstream = prev->upstream.upstream; 843 | } 844 | 845 | if (conf->index == NGX_CONF_UNSET) { 846 | conf->index = prev->index; 847 | } 848 | 849 | if (conf->db == NGX_CONF_UNSET) { 850 | conf->db = prev->db; 851 | } 852 | 853 | if (conf->auth == NGX_CONF_UNSET) { 854 | conf->auth = prev->auth; 855 | } 856 | 857 | ngx_conf_merge_uint_value(conf->gzip_flag, prev->gzip_flag, 0); 858 | 859 | return NGX_CONF_OK; 860 | } 861 | 862 | 863 | static char * 864 | ngx_http_redis_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 865 | { 866 | ngx_http_redis_loc_conf_t *rlcf = conf; 867 | 868 | ngx_str_t *value; 869 | ngx_url_t u; 870 | ngx_http_core_loc_conf_t *clcf; 871 | 872 | if (rlcf->upstream.upstream) { 873 | return "is duplicate"; 874 | } 875 | 876 | value = cf->args->elts; 877 | 878 | ngx_memzero(&u, sizeof(ngx_url_t)); 879 | 880 | u.url = value[1]; 881 | u.no_resolve = 1; 882 | 883 | rlcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0); 884 | if (rlcf->upstream.upstream == NULL) { 885 | return NGX_CONF_ERROR; 886 | } 887 | 888 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 889 | 890 | clcf->handler = ngx_http_redis_handler; 891 | 892 | if (clcf->name.len && clcf->name.data[clcf->name.len - 1] == '/') { 893 | clcf->auto_redirect = 1; 894 | } 895 | 896 | rlcf->index = ngx_http_get_variable_index(cf, &ngx_http_redis_key); 897 | 898 | if (rlcf->index == NGX_ERROR) { 899 | return NGX_CONF_ERROR; 900 | } 901 | 902 | rlcf->db = ngx_http_get_variable_index(cf, &ngx_http_redis_db); 903 | rlcf->auth = ngx_http_get_variable_index(cf, &ngx_http_redis_auth); 904 | 905 | return NGX_CONF_OK; 906 | } 907 | 908 | 909 | static ngx_int_t 910 | ngx_http_redis_reset_variable(ngx_http_request_t *r, 911 | ngx_http_variable_value_t *v, uintptr_t data) 912 | { 913 | *v = ngx_http_variable_null_value; 914 | 915 | return NGX_OK; 916 | } 917 | 918 | 919 | static ngx_int_t 920 | ngx_http_redis_add_variables(ngx_conf_t *cf) 921 | { 922 | ngx_int_t n; 923 | ngx_http_variable_t *var; 924 | ngx_http_variable_t *authvar; 925 | 926 | var = ngx_http_add_variable(cf, &ngx_http_redis_db, 927 | NGX_HTTP_VAR_CHANGEABLE); 928 | if (var == NULL) { 929 | return NGX_ERROR; 930 | } 931 | 932 | var->get_handler = ngx_http_redis_reset_variable; 933 | 934 | n = ngx_http_get_variable_index(cf, &ngx_http_redis_db); 935 | if (n == NGX_ERROR) { 936 | return NGX_ERROR; 937 | } 938 | 939 | ngx_http_redis_db_index = n; 940 | 941 | authvar = ngx_http_add_variable(cf, &ngx_http_redis_auth, 942 | NGX_HTTP_VAR_CHANGEABLE); 943 | if (authvar == NULL) { 944 | return NGX_ERROR; 945 | } 946 | 947 | authvar->get_handler = ngx_http_redis_reset_variable; 948 | 949 | n = ngx_http_get_variable_index(cf, &ngx_http_redis_auth); 950 | if (n == NGX_ERROR) { 951 | return NGX_ERROR; 952 | } 953 | 954 | ngx_http_redis_auth_index = n; 955 | 956 | return NGX_OK; 957 | } 958 | -------------------------------------------------------------------------------- /t/redis.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Maxim Dounin 4 | # (C) Sergey A. Osokin 5 | 6 | # Test for redis backend. 7 | 8 | ############################################################################### 9 | 10 | use warnings; 11 | use strict; 12 | 13 | use Test::More; 14 | 15 | BEGIN { use FindBin; chdir($FindBin::Bin); } 16 | 17 | use lib 'lib'; 18 | use Test::Nginx; 19 | 20 | ############################################################################### 21 | 22 | select STDERR; $| = 1; 23 | select STDOUT; $| = 1; 24 | 25 | eval { require Redis; }; 26 | plan(skip_all => 'Redis not installed') if $@; 27 | 28 | 29 | my $t = Test::Nginx->new()->has(qw/http/) 30 | ->has_daemon('redis-server')->plan(6) 31 | ->write_file_expand('nginx.conf', <<'EOF'); 32 | 33 | %%TEST_GLOBALS%% 34 | 35 | daemon off; 36 | 37 | events { 38 | } 39 | 40 | http { 41 | %%TEST_GLOBALS_HTTP%% 42 | 43 | upstream redisbackend { 44 | server 127.0.0.1:8081; 45 | } 46 | 47 | server { 48 | listen 127.0.0.1:8080; 49 | server_name localhost; 50 | 51 | location / { 52 | set $redis_key $uri; 53 | redis_pass redisbackend; 54 | } 55 | 56 | location /0 { 57 | set $redis_key $uri; 58 | set $redis_db "0"; 59 | redis_pass redisbackend; 60 | } 61 | 62 | location /1 { 63 | set $redis_key $uri; 64 | set $redis_db "1"; 65 | redis_pass redisbackend; 66 | } 67 | } 68 | } 69 | 70 | EOF 71 | 72 | $t->write_file('redis.conf', <run_daemon('redis-server', $t->testdir() . '/redis.conf'); 92 | $t->run(); 93 | 94 | $t->waitforsocket('127.0.0.1:8081') 95 | or die "Can't start redis"; 96 | 97 | 98 | ############################################################################### 99 | 100 | my $r = Redis->new(server => '127.0.0.1:8081'); 101 | $r->set('/' => 'SEE-THIS') or die "can't put value into redis: $!"; 102 | $r->set('/0/' => 'SEE-THIS.0') or die "can't put value into redis: $!"; 103 | 104 | $r->select("1") or die "can't select db 1 in redis: $!"; 105 | $r->set('/1/' => 'SEE-THIS.1') or die "can't put value into redis: $!"; 106 | 107 | like(http_get('/'), qr/SEE-THIS/, 'redis request /'); 108 | 109 | like(http_get('/0/'), qr/SEE-THIS.0/, 'redis request from db 0'); 110 | 111 | like(http_get('/1/'), qr/SEE-THIS.1/, 'redis request from db 1'); 112 | 113 | like(http_get('/0/'), qr/SEE-THIS.0/, 'redis request from db 0'); 114 | 115 | like(http_get('/notfound'), qr/404/, 'redis not found'); 116 | 117 | unlike (http_head('/'), qr/SEE-THIS/, 'redis no data in HEAD'); 118 | 119 | ############################################################################### 120 | -------------------------------------------------------------------------------- /t/redis_auth.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Maxim Dounin 4 | # (C) Sergey A. Osokin 5 | 6 | # Test for redis auth. 7 | 8 | ############################################################################### 9 | 10 | use warnings; 11 | use strict; 12 | 13 | use Test::More; 14 | 15 | BEGIN { use FindBin; chdir($FindBin::Bin); } 16 | 17 | use lib 'lib'; 18 | use Test::Nginx; 19 | 20 | ############################################################################### 21 | 22 | select STDERR; $| = 1; 23 | select STDOUT; $| = 1; 24 | 25 | eval { require Redis; }; 26 | plan(skip_all => 'Redis not installed') if $@; 27 | 28 | my $t = Test::Nginx->new()->has(qw/http/) 29 | ->has_daemon('redis-server')->plan(2) 30 | ->write_file_expand('nginx.conf', <<'EOF'); 31 | 32 | %%TEST_GLOBALS%% 33 | 34 | daemon off; 35 | 36 | events { 37 | } 38 | 39 | http { 40 | %%TEST_GLOBALS_HTTP%% 41 | 42 | upstream redisbackend { 43 | server 127.0.0.1:8081; 44 | } 45 | 46 | server { 47 | listen 127.0.0.1:8080; 48 | server_name localhost; 49 | 50 | location /auth { 51 | set $redis_auth somepasswd; 52 | set $redis_key $uri; 53 | redis_pass redisbackend; 54 | } 55 | location /no-auth { 56 | set $redis_key $uri; 57 | redis_pass redisbackend; 58 | } 59 | } 60 | } 61 | 62 | EOF 63 | 64 | $t->write_file('redis.conf', <run_daemon('redis-server', $t->testdir() . '/redis.conf'); 85 | $t->run(); 86 | 87 | $t->waitforsocket('127.0.0.1:8081') 88 | or die "Can't start redis"; 89 | 90 | ############################################################################### 91 | 92 | my $r = Redis->new(server => '127.0.0.1:8081'); 93 | $r->auth('somepasswd'); 94 | $r->set('/auth' => 'SEE-THIS') or die "can't put value into redis: $!"; 95 | 96 | like(http_get('/auth'), qr/SEE-THIS/, 'redis request with auth'); 97 | unlike(http_get('/no-auth'), qr/SEE-THIS/, 'redis request with no auth'); 98 | ############################################################################### 99 | -------------------------------------------------------------------------------- /t/redis_db_maxnum.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Maxim Dounin 4 | # (C) Sergey A. Osokin 5 | 6 | # Test redis database maximum database number 7 | 8 | ############################################################################### 9 | 10 | use warnings; 11 | use strict; 12 | 13 | use Test::More; 14 | 15 | BEGIN { use FindBin; chdir($FindBin::Bin); } 16 | 17 | use lib 'lib'; 18 | use Test::Nginx; 19 | 20 | ############################################################################### 21 | 22 | select STDERR; $| = 1; 23 | select STDOUT; $| = 1; 24 | 25 | eval { require Redis; }; 26 | plan(skip_all => 'Redis not installed') if $@; 27 | 28 | my $t = Test::Nginx->new()->has(qw/http/) 29 | ->has_daemon('redis-server')->plan(2) 30 | ->write_file_expand('nginx.conf', <<'EOF'); 31 | 32 | %%TEST_GLOBALS%% 33 | 34 | daemon off; 35 | 36 | events { 37 | } 38 | 39 | http { 40 | %%TEST_GLOBALS_HTTP%% 41 | 42 | upstream redisbackend { 43 | server 127.0.0.1:8081; 44 | } 45 | 46 | server { 47 | listen 127.0.0.1:8080; 48 | server_name localhost; 49 | 50 | location /0 { 51 | set $redis_db "0"; 52 | set $redis_key $uri; 53 | redis_pass redisbackend; 54 | } 55 | location /1048575 { 56 | set $redis_db "1048575"; 57 | set $redis_key $uri; 58 | redis_pass redisbackend; 59 | } 60 | } 61 | } 62 | 63 | EOF 64 | 65 | $t->write_file('redis.conf', <run_daemon('redis-server', $t->testdir() . '/redis.conf'); 85 | $t->run(); 86 | 87 | $t->waitforsocket('127.0.0.1:8081') 88 | or die "Can't start redis"; 89 | 90 | ############################################################################### 91 | 92 | my $r = Redis->new(server => '127.0.0.1:8081'); 93 | $r->select(0); 94 | $r->set('/0' => 'SEE-THIS-0') or die "can't put value into redis: $!"; 95 | $r->select(1048575); 96 | $r->set('/1048575' => 'SEE-THIS-1048575') or die "can't put value into redis: $!"; 97 | 98 | like(http_get('/0'), qr/SEE-THIS-0/, 'redis request to db 0'); 99 | like(http_get('/1048575'), qr/SEE-THIS-1048575/, 'redis request to db 1048575'); 100 | 101 | ############################################################################### 102 | -------------------------------------------------------------------------------- /t/redis_db_not_set.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Maxim Dounin 4 | # (C) Sergey A. Osokin 5 | 6 | # Test for redis backend. 7 | 8 | ############################################################################### 9 | 10 | use warnings; 11 | use strict; 12 | 13 | use Test::More; 14 | 15 | BEGIN { use FindBin; chdir($FindBin::Bin); } 16 | 17 | use lib 'lib'; 18 | use Test::Nginx; 19 | 20 | ############################################################################### 21 | 22 | select STDERR; $| = 1; 23 | select STDOUT; $| = 1; 24 | 25 | eval { require Redis; }; 26 | plan(skip_all => 'Redis not installed') if $@; 27 | 28 | my $t = Test::Nginx->new()->has(qw/http/) 29 | ->has_daemon('redis-server')->plan(1) 30 | ->write_file_expand('nginx.conf', <<'EOF'); 31 | 32 | %%TEST_GLOBALS%% 33 | 34 | daemon off; 35 | 36 | events { 37 | } 38 | 39 | http { 40 | %%TEST_GLOBALS_HTTP%% 41 | 42 | upstream redisbackend { 43 | server 127.0.0.1:8081; 44 | } 45 | 46 | server { 47 | listen 127.0.0.1:8080; 48 | server_name localhost; 49 | 50 | location / { 51 | set $redis_key $uri; 52 | redis_pass redisbackend; 53 | } 54 | } 55 | } 56 | 57 | EOF 58 | 59 | $t->write_file('redis.conf', <run_daemon('redis-server', $t->testdir() . '/redis.conf'); 79 | $t->run(); 80 | 81 | $t->waitforsocket('127.0.0.1:8081') 82 | or die "Can't start redis"; 83 | 84 | ############################################################################### 85 | 86 | my $r = Redis->new(server => '127.0.0.1:8081'); 87 | $r->set('/' => 'SEE-THIS') or die "can't put value into redis: $!"; 88 | 89 | like(http_get('/'), qr/SEE-THIS/, 'redis request /'); 90 | 91 | ############################################################################### 92 | -------------------------------------------------------------------------------- /t/redis_gunzip.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Maxim Dounin 4 | # (C) Sergey A. Osokin 5 | 6 | # Test for a gzipped content in redis. 7 | 8 | ############################################################################### 9 | 10 | use warnings; 11 | use strict; 12 | 13 | use Test::More; 14 | 15 | BEGIN { use FindBin; chdir($FindBin::Bin); } 16 | 17 | use lib 'lib'; 18 | use Test::Nginx qw/ :DEFAULT :gzip /; 19 | 20 | ############################################################################### 21 | 22 | select STDERR; $| = 1; 23 | select STDOUT; $| = 1; 24 | 25 | eval { require Redis; }; 26 | plan(skip_all => 'Redis not installed') if $@; 27 | 28 | eval { require IO::Compress::Gzip; }; 29 | plan(skip_all => "IO::Compress::Gzip not found") if $@; 30 | 31 | my $t = Test::Nginx->new()->has(qw/http gunzip rewrite/) 32 | ->has_daemon('redis-server') 33 | ->write_file_expand('nginx.conf', <<'EOF'); 34 | 35 | %%TEST_GLOBALS%% 36 | 37 | daemon off; 38 | 39 | events { 40 | } 41 | 42 | http { 43 | %%TEST_GLOBALS_HTTP%% 44 | 45 | upstream redisbackend { 46 | server 127.0.0.1:8081; 47 | } 48 | 49 | server { 50 | listen 127.0.0.1:8080; 51 | server_name localhost; 52 | 53 | location /t1 { 54 | set $redis_key $uri; 55 | redis_pass redisbackend; 56 | redis_gzip_flag 2; 57 | } 58 | location /t2 { 59 | gunzip on; 60 | set $redis_key $uri; 61 | redis_pass redisbackend; 62 | redis_gzip_flag 2; 63 | } 64 | } 65 | } 66 | 67 | EOF 68 | 69 | $t->write_file('redis.conf', < \$out); 92 | 93 | $t->run_daemon('redis-server', $t->testdir() . '/redis.conf'); 94 | 95 | $t->run()->plan(2); 96 | 97 | $t->waitforsocket('127.0.0.1:8081') 98 | or die "Can't start redis"; 99 | 100 | my $r = Redis->new(server => '127.0.0.1:8081'); 101 | $r->set('/t1', $out) 102 | or die "can't put value into redis: $!"; 103 | $r->set('/t2', $out) 104 | or die "can't put value into redis: $!"; 105 | 106 | ############################################################################### 107 | 108 | like(http_gzip_request('/t1'), qr/Content-Encoding: gzip.*/, 'redis response gzipped'); 109 | like(http_get('/t2'), qr/(?!Content-Encoding).*^(X\d\d\dXXXXXX){100}$/m, 110 | 'correct gunzipped response'); 111 | 112 | ############################################################################### 113 | -------------------------------------------------------------------------------- /t/redis_keepalive.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Maxim Dounin 4 | # (C) Sergey A. Osokin 5 | 6 | # Test for redis with keepalive. 7 | 8 | ############################################################################### 9 | 10 | use warnings; 11 | use strict; 12 | 13 | use Test::More; 14 | 15 | BEGIN { use FindBin; chdir($FindBin::Bin); } 16 | 17 | use lib 'lib'; 18 | use Test::Nginx; 19 | 20 | ############################################################################### 21 | 22 | select STDERR; $| = 1; 23 | select STDOUT; $| = 1; 24 | 25 | eval { require Redis; }; 26 | plan(skip_all => 'Redis not installed') if $@; 27 | 28 | my $t = Test::Nginx->new()->has(qw/http upstream_keepalive rewrite/) 29 | ->has_daemon('redis-server')->plan(15) 30 | ->write_file_expand('nginx.conf', <<'EOF'); 31 | 32 | %%TEST_GLOBALS%% 33 | 34 | daemon off; 35 | 36 | events { 37 | } 38 | 39 | http { 40 | %%TEST_GLOBALS_HTTP%% 41 | 42 | upstream rs { 43 | server 127.0.0.1:8081; 44 | keepalive 1; 45 | } 46 | 47 | upstream rs3 { 48 | server 127.0.0.1:8081; 49 | server 127.0.0.1:8082; 50 | keepalive 1; 51 | } 52 | 53 | upstream rs4 { 54 | server 127.0.0.1:8081; 55 | server 127.0.0.1:8082; 56 | keepalive 10; 57 | } 58 | 59 | server { 60 | listen 127.0.0.1:8080; 61 | server_name localhost; 62 | 63 | location / { 64 | set $redis_key $uri; 65 | redis_pass rs; 66 | } 67 | 68 | location /next { 69 | set $redis_key $uri; 70 | redis_next_upstream not_found; 71 | redis_pass rs; 72 | } 73 | 74 | location /rs3 { 75 | set $redis_key "/"; 76 | redis_pass rs3; 77 | } 78 | 79 | location /rs4 { 80 | set $redis_key "/"; 81 | redis_pass rs4; 82 | } 83 | } 84 | } 85 | 86 | EOF 87 | 88 | $t->write_file('rs1.conf', <write_file('rs2.conf', <run_daemon('redis-server', $t->testdir() . '/rs1.conf'); 130 | $t->run_daemon('redis-server', $t->testdir() . '/rs2.conf'); 131 | 132 | $t->run(); 133 | 134 | $t->waitforsocket('127.0.0.1:8081') 135 | or die "Can't start redis1"; 136 | 137 | $t->waitforsocket('127.0.0.1:8082') 138 | or die "Can't start redis2"; 139 | 140 | 141 | ############################################################################### 142 | 143 | my $r1 = Redis->new(server => '127.0.0.1:8081'); 144 | my $r2 = Redis->new(server => '127.0.0.1:8082'); 145 | 146 | $r1->set('/', 'SEE-THIS') or die "can't put value into redis1: $!"; 147 | $r2->set('/', 'SEE-THIS') or die "can't put value into redis2: $!"; 148 | $r1->set('/big', 'X' x 1000000) or die "can't put value into redis1: $!"; 149 | 150 | my $total = $r1->info->{connected_clients}; # 0 151 | 152 | like(http_get('/'), qr/SEE-THIS/, 'keepalive redis request'); 153 | like(http_get('/notfound'), qr/ 404 /, 'keepalive redis not found'); 154 | like(http_get('/next'), qr/ 404 /, 155 | 'keepalive not found with redis_next_upstream'); 156 | like(http_get('/'), qr/SEE-THIS/, 'keepalive redis request again'); 157 | like(http_get('/'), qr/SEE-THIS/, 'keepalive redis request again'); 158 | like(http_get('/'), qr/SEE-THIS/, 'keepalive redis request again'); 159 | 160 | is($r1->info->{connected_clients}, $total + 1, # 1 161 | 'only one connection used'); 162 | 163 | # Since nginx doesn't read all data from connection in some situations (head 164 | # requests, post_action, errors writing to client) we have to close such 165 | # connections. Check if we really do close them. 166 | 167 | $total = $r1->info->{connected_client} ? $r1->info->{connected_client} : 0; 168 | 169 | unlike(http_head('/'), qr/SEE-THIS/, 'head request'); 170 | like(http_get('/'), qr/SEE-THIS/, 'get after head'); 171 | 172 | is($r1->info->{connected_clients}, $total + 2, 173 | 'head request does not close connection'); 174 | 175 | $total = $r1->info->{connected_clients}; 176 | 177 | unlike(http_head('/big'), qr/XXX/, 'big head'); 178 | like(http_get('/'), qr/SEE-THIS/, 'get after big head'); 179 | 180 | is($r1->info()->{connected_clients}, $total, 181 | 'big head request does not close connection'); 182 | 183 | # two backends with maximum number of cached connections set to 1, 184 | # should establish new connection on each request 185 | 186 | $total = $r1->info->{connected_clients} + 187 | $r2->info->{connected_clients}; 188 | 189 | http_get('/rs3'); 190 | http_get('/rs3'); 191 | http_get('/rs3'); 192 | 193 | is($r1->info->{connected_clients} + 194 | $r2->info->{connected_clients}, $total + 1, 195 | '3 connections should be established'); 196 | 197 | # two backends with maximum number of cached connections set to 10, 198 | # should establish only two connections (1 per backend) 199 | 200 | $total = $r1->info->{connected_clients} + 201 | $r2->info->{connected_clients}; 202 | 203 | http_get('/rs4'); 204 | http_get('/rs4'); 205 | http_get('/rs4'); 206 | 207 | is($r1->info->{connected_clients} + 208 | $r2->info->{connected_clients}, $total + 2, 209 | 'connection per backend'); 210 | 211 | ############################################################################### 212 | --------------------------------------------------------------------------------