├── AUTHOR ├── config ├── t ├── redis_db_not_set.t ├── redis.t └── Test │ └── Nginx.pm ├── LICENSE ├── README.md ├── CHANGES └── ngx_http_redis_module.c /AUTHOR: -------------------------------------------------------------------------------- 1 | Sergey A. Osokin 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 redis/) 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->run(); 60 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2002-2009 Igor Sysoev 3 | * Copyright (C) 2009-2013,2016 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 | -------------------------------------------------------------------------------- /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 redis/) 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'); 94 | $t->run(); 95 | 96 | $t->waitforsocket('127.0.0.1:8081') 97 | or die "Can't start redis"; 98 | 99 | 100 | ############################################################################### 101 | 102 | my $r = Redis->new(server => '127.0.0.1:8081'); 103 | $r->set('/' => 'SEE-THIS') or die "can't put value into redis: $!"; 104 | $r->set('/0/' => 'SEE-THIS.0') or die "can't put value into redis: $!"; 105 | 106 | $r->select("1") or die "can't select db 1 in redis: $!"; 107 | $r->set('/1/' => 'SEE-THIS.1') or die "can't put value into redis: $!"; 108 | 109 | like(http_get('/'), qr/SEE-THIS/, 'redis request /'); 110 | 111 | like(http_get('/0/'), qr/SEE-THIS.0/, 'redis request from db 0'); 112 | 113 | like(http_get('/1/'), qr/SEE-THIS.1/, 'redis request from db 1'); 114 | 115 | like(http_get('/0/'), qr/SEE-THIS.0/, 'redis request from db 0'); 116 | 117 | like(http_get('/notfound'), qr/404/, 'redis not found'); 118 | 119 | unlike (http_head('/'), qr/SEE-THIS/, 'redis no data in HEAD'); 120 | 121 | ############################################################################### 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NGiNX HTTP Redis module 2 | ======================= 3 | 4 | This is a fork of the 0.3.8 module, with updates to support the newer Redis 5 | protocol. This allows it to work with many non-Redis implementations of the 6 | protocol that don't support the legacy protocol. 7 | 8 | Description: 9 | -- 10 | 11 | The nginx HTTP redis module for caching with redis, 12 | http://code.google.com/p/redis/. 13 | 14 | The redis protocol 15 | (http://code.google.com/p/redis/wiki/ProtocolSpecification) 16 | not yet fully implemented, but GET and SELECT commands only. 17 | 18 | 19 | Installation: 20 | -- 21 | 22 | You'll need to re-compile Nginx from source to include this module. 23 | Modify your compile of Nginx by adding the following directive 24 | (modified to suit your path of course): 25 | 26 | ./configure --add-module=/absolute/path/to/ngx_http_redis 27 | make 28 | make install 29 | 30 | 31 | Usage: 32 | -- 33 | 34 | ### Example 1. 35 | 36 | 37 | ``` 38 | http 39 | { 40 | ... 41 | server { 42 | location / { 43 | set $redis_key "$uri?$args"; 44 | redis_pass 127.0.0.1:6379; 45 | error_page 404 502 504 = @fallback; 46 | } 47 | 48 | location @fallback { 49 | proxy_pass backed; 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | 56 | ### Example 2. 57 | 58 | Capture User-Agent from an HTTP header, query to redis database 59 | for lookup appropriate backend. 60 | 61 | Eval module (http://www.grid.net.ru/nginx/eval.en.html) required. 62 | 63 | ``` 64 | http 65 | { 66 | ... 67 | upstream redis { 68 | server 127.0.0.1:6379; 69 | } 70 | 71 | server { 72 | ... 73 | location / { 74 | 75 | eval_escalate on; 76 | 77 | eval $answer { 78 | set $redis_key "$http_user_agent"; 79 | redis_pass redis; 80 | } 81 | 82 | proxy_pass $answer; 83 | } 84 | ... 85 | } 86 | } 87 | ``` 88 | 89 | ### Example 3. 90 | 91 | Compile nginx with ngx_http_redis and ngx_http_gunzip_filter modules 92 | (http://mdounin.ru/hg/ngx_http_gunzip_filter_module/). 93 | Gzip content and put it into redis database. 94 | 95 | ``` 96 | % cat index.html 97 | Hello from redis! 98 | % gzip index.html 99 | % cat index.html.gz | redis-cli -x set /index.html 100 | OK 101 | % cat index.html.gz | redis-cli -x set / 102 | OK 103 | % 104 | ``` 105 | 106 | Using following configuration nginx get data from redis database and 107 | gunzip it. 108 | 109 | ``` 110 | http 111 | { 112 | ... 113 | upstream redis { 114 | server 127.0.0.1:6379; 115 | } 116 | 117 | server { 118 | ... 119 | location / { 120 | gunzip on; 121 | redis_gzip_flag 1; 122 | set $redis_key "$uri"; 123 | redis_pass redis; 124 | } 125 | } 126 | ``` 127 | 128 | 129 | Thanks to: 130 | -- 131 | 132 | * Maxim Dounin 133 | * Vsevolod Stakhov 134 | * Ezra Zygmuntowicz 135 | 136 | 137 | 138 | Special thanks to: 139 | -- 140 | * Evan Miller for his "Guide To Nginx Module Development" and "Advanced Topics 141 | In Nginx Module Development" 142 | * Valery Kholodkov for his "Nginx modules development" 143 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changes with ngx_http_redis 0.3.8 21 Feb 2016 2 | 3 | *) Bugfix: it is impossible to compile the ngx_http_redis_module 4 | without http_gzip module or with --without-http_gzip_module 5 | option. 6 | Thanks to Yichun Zhang (agentzh). 7 | 8 | *) Feature: it's possible to compile as dynamic module now, this 9 | feature has been introduced in nginx 1.9.11. 10 | 11 | 12 | Changes with ngx_http_redis 0.3.7 28 Nov 2013 13 | 14 | *) Bugfix: ngx_http_redis_module might issue the error message 15 | "redis sent invalid trailer" for nginx >= 1.5.3. 16 | Thanks to Maxim Dounin. 17 | 18 | 19 | Changes with ngx_http_redis 0.3.6 03 Apr 2012 20 | 21 | *) Feature: redis_gzip_flag. Useful if you are prefer to 22 | store data compressed in redis. Works with ngx_http_gunzip_filter 23 | module. 24 | Thanks to Maxim Dounin. 25 | 26 | *) Bugfix: ngx_http_redis_module might issue the error message 27 | "redis sent invalid trailer". 28 | Thanks to agentzh. 29 | 30 | 31 | Changes with ngx_http_redis 0.3.5 30 Aug 2011 32 | 33 | *) Feature: add test for not set $redis_db directive. 34 | 35 | *) Feature: keep-alive support merged from original 36 | memcached module 1.1.4. 37 | 38 | 39 | Changes with ngx_http_redis 0.3.4 24 Aug 2011 40 | 41 | *) Change: better error messages diagnostics in select phase. 42 | 43 | *) Add more comments in source code. 44 | 45 | *) Bugfix: fix interaction with redis if redis_db was unused. 46 | Found by Sergey Makarov. 47 | Thanks to Igor Sysoev. 48 | 49 | *) Feature: add test suite for redis backend. 50 | Thanks to Maxim Dounin. 51 | 52 | 53 | Changes with ngx_http_redis 0.3.3 07 Jun 2011 54 | 55 | *) Bugfix: fix interaction with redis if redis_db was used. 56 | Also, compile with -Werror now is possible. 57 | 58 | 59 | Changes with ngx_http_redis 0.3.2 17 Aug 2010 60 | 61 | *) Bugfix: ngx_http_redis_module might issue the error message 62 | "redis sent invalid trailer". For more information see: 63 | $ diff -ruN \ 64 | nginx-0.8.34/src/http/modules/ngx_http_memcached_module.c \ 65 | nginx-0.8.35/src/http/modules/ngx_http_memcached_module.c 66 | 67 | *) Change: now the $redis_db set is not obligatory; default 68 | value is "0". 69 | 70 | 71 | Changes with ngx_http_redis 0.3.1 26 Dec 2009 72 | 73 | *) Change: return 502 instead of 404 for error. 74 | 75 | *) Change: better error messages diagnostics. 76 | 77 | *) Bugfix: improve interoperability with redis; the bug had 78 | appeared in 0.3.0. 79 | 80 | 81 | Changes with ngx_http_redis 0.3.0 23 Dec 2009 82 | 83 | *) Compatibility with latest stable (0.7.64) and 84 | development 0.8.31 releases. 85 | 86 | *) Bugfix: multiple commands issue with interoperability with 87 | redis; the bug had appeared in 0.2.0. 88 | 89 | *) Feature: redis_bind directive merge from original 90 | memcached module (for 0.8.22 and later). 91 | 92 | 93 | Changes with ngx_http_redis 0.2.0 19 Sep 2009 94 | 95 | *) Feature: the $redis_db variable: now the ngx_http_redis 96 | module uses the $redis_db variable value as the parameter 97 | for SELECT command. 98 | 99 | *) Cleanup: style/spaces fixes. 100 | 101 | 102 | Changes with ngx_http_redis 0.1.2 14 Sep 2009 103 | 104 | *) Change: backport to 0.7.61. 105 | 106 | 107 | Changes with ngx_http_redis 0.1.1 31 Aug 2009 108 | 109 | *) Change: compatibility with nginx 0.8.11. 110 | 111 | *) Cleanup: mlcf -> rlcf, i.e. 112 | (memcached location configuration) -> redis... 113 | 114 | -------------------------------------------------------------------------------- /t/Test/Nginx.pm: -------------------------------------------------------------------------------- 1 | package Test::Nginx; 2 | 3 | # (C) Maxim Dounin 4 | 5 | # Generic module for nginx tests. 6 | 7 | ############################################################################### 8 | 9 | use warnings; 10 | use strict; 11 | 12 | use base qw/ Exporter /; 13 | 14 | our @EXPORT = qw/ log_in log_out http http_get http_head /; 15 | our @EXPORT_OK = qw/ http_gzip_request http_gzip_like /; 16 | our %EXPORT_TAGS = ( 17 | gzip => [ qw/ http_gzip_request http_gzip_like / ] 18 | ); 19 | 20 | ############################################################################### 21 | 22 | use File::Temp qw/ tempdir /; 23 | use IO::Socket; 24 | use Socket qw/ CRLF /; 25 | use Test::More qw//; 26 | 27 | ############################################################################### 28 | 29 | our $NGINX = defined $ENV{TEST_NGINX_BINARY} ? $ENV{TEST_NGINX_BINARY} 30 | : '../nginx/objs/nginx'; 31 | 32 | sub new { 33 | my $self = {}; 34 | bless $self; 35 | 36 | $self->{_testdir} = tempdir( 37 | 'nginx-test-XXXXXXXXXX', 38 | TMPDIR => 1, 39 | CLEANUP => not $ENV{TEST_NGINX_LEAVE} 40 | ) 41 | or die "Can't create temp directory: $!\n"; 42 | 43 | return $self; 44 | } 45 | 46 | sub DESTROY { 47 | my ($self) = @_; 48 | $self->stop(); 49 | $self->stop_daemons(); 50 | if ($ENV{TEST_NGINX_CATLOG}) { 51 | system("cat $self->{_testdir}/error.log"); 52 | } 53 | } 54 | 55 | sub has($;) { 56 | my ($self, @features) = @_; 57 | 58 | foreach my $feature (@features) { 59 | Test::More::plan(skip_all => "$feature not compiled in") 60 | unless $self->has_module($feature); 61 | } 62 | 63 | return $self; 64 | } 65 | 66 | sub has_module($) { 67 | my ($self, $feature) = @_; 68 | 69 | my %regex = ( 70 | mail => '--with-mail(?!\S)', 71 | flv => '--with-http_flv_module', 72 | perl => '--with-http_perl_module', 73 | redis => '(?s)^(?!--add-module=.*ngx_http_redis-)', 74 | charset => '(?s)^(?!.*--without-http_charset_module)', 75 | gzip => '(?s)^(?!.*--without-http_gzip_module)', 76 | ssi => '(?s)^(?!.*--without-http_ssi_module)', 77 | userid => '(?s)^(?!.*--without-http_userid_module)', 78 | access => '(?s)^(?!.*--without-http_access_module)', 79 | auth_basic 80 | => '(?s)^(?!.*--without-http_auth_basic_module)', 81 | autoindex 82 | => '(?s)^(?!.*--without-http_autoindex_module)', 83 | geo => '(?s)^(?!.*--without-http_geo_module)', 84 | map => '(?s)^(?!.*--without-http_map_module)', 85 | referer => '(?s)^(?!.*--without-http_referer_module)', 86 | rewrite => '(?s)^(?!.*--without-http_rewrite_module)', 87 | proxy => '(?s)^(?!.*--without-http_proxy_module)', 88 | fastcgi => '(?s)^(?!.*--without-http_fastcgi_module)', 89 | uwsgi => '(?s)^(?!.*--without-http_uwsgi_module)', 90 | scgi => '(?s)^(?!.*--without-http_scgi_module)', 91 | memcached 92 | => '(?s)^(?!.*--without-http_memcached_module)', 93 | limit_zone 94 | => '(?s)^(?!.*--without-http_limit_zone_module)', 95 | limit_req 96 | => '(?s)^(?!.*--without-http_limit_req_module)', 97 | empty_gif 98 | => '(?s)^(?!.*--without-http_empty_gif_module)', 99 | browser => '(?s)^(?!.*--without-http_browser_module)', 100 | upstream_ip_hash 101 | => '(?s)^(?!.*--without-http_upstream_ip_hash_module)', 102 | http => '(?s)^(?!.*--without-http(?!\S))', 103 | cache => '(?s)^(?!.*--without-http-cache)', 104 | pop3 => '(?s)^(?!.*--without-mail_pop3_module)', 105 | imap => '(?s)^(?!.*--without-mail_imap_module)', 106 | smtp => '(?s)^(?!.*--without-mail_smtp_module)', 107 | pcre => '(?s)^(?!.*--without-pcre)', 108 | ); 109 | 110 | my $re = $regex{$feature}; 111 | $re = $feature if !defined $re; 112 | 113 | $self->{_configure_args} = `$NGINX -V 2>&1` 114 | if !defined $self->{_configure_args}; 115 | 116 | return ($self->{_configure_args} =~ $re) ? 1 : 0; 117 | } 118 | 119 | sub has_daemon($) { 120 | my ($self, $daemon) = @_; 121 | 122 | Test::More::plan(skip_all => "$daemon not found") 123 | unless `which $daemon`; 124 | 125 | return $self; 126 | } 127 | 128 | sub plan($) { 129 | my ($self, $plan) = @_; 130 | 131 | Test::More::plan(tests => $plan); 132 | 133 | return $self; 134 | } 135 | 136 | sub run(;$) { 137 | my ($self, $conf) = @_; 138 | 139 | my $testdir = $self->{_testdir}; 140 | 141 | if (defined $conf) { 142 | my $c = `cat $conf`; 143 | $self->write_file_expand('nginx.conf', $c); 144 | } 145 | 146 | my $pid = fork(); 147 | die "Unable to fork(): $!\n" unless defined $pid; 148 | 149 | if ($pid == 0) { 150 | my @globals = $self->{_test_globals} ? 151 | () : ('-g', "pid $testdir/nginx.pid; " 152 | . "error_log $testdir/error.log debug;"); 153 | exec($NGINX, '-c', "$testdir/nginx.conf", @globals) 154 | or die "Unable to exec(): $!\n"; 155 | } 156 | 157 | # wait for nginx to start 158 | 159 | $self->waitforfile("$testdir/nginx.pid") 160 | or die "Can't start nginx"; 161 | 162 | $self->{_started} = 1; 163 | return $self; 164 | } 165 | 166 | sub waitforfile($) { 167 | my ($self, $file) = @_; 168 | 169 | # wait for file to appear 170 | 171 | for (1 .. 30) { 172 | return 1 if -e $file; 173 | select undef, undef, undef, 0.1; 174 | } 175 | 176 | return undef; 177 | } 178 | 179 | sub waitforsocket($) { 180 | my ($self, $peer) = @_; 181 | 182 | # wait for socket to accept connections 183 | 184 | for (1 .. 30) { 185 | my $s = IO::Socket::INET->new( 186 | Proto => 'tcp', 187 | PeerAddr => $peer 188 | ); 189 | 190 | return 1 if defined $s; 191 | 192 | select undef, undef, undef, 0.1; 193 | } 194 | 195 | return undef; 196 | } 197 | 198 | sub stop() { 199 | my ($self) = @_; 200 | 201 | return $self unless $self->{_started}; 202 | 203 | kill 'QUIT', `cat $self->{_testdir}/nginx.pid`; 204 | wait; 205 | 206 | $self->{_started} = 0; 207 | 208 | return $self; 209 | } 210 | 211 | sub stop_daemons() { 212 | my ($self) = @_; 213 | 214 | while ($self->{_daemons} && scalar @{$self->{_daemons}}) { 215 | my $p = shift @{$self->{_daemons}}; 216 | kill 'TERM', $p; 217 | wait; 218 | } 219 | 220 | return $self; 221 | } 222 | 223 | sub write_file($$) { 224 | my ($self, $name, $content) = @_; 225 | 226 | open F, '>' . $self->{_testdir} . '/' . $name 227 | or die "Can't create $name: $!"; 228 | print F $content; 229 | close F; 230 | 231 | return $self; 232 | } 233 | 234 | sub write_file_expand($$) { 235 | my ($self, $name, $content) = @_; 236 | 237 | $content =~ s/%%TEST_GLOBALS%%/$self->test_globals()/gmse; 238 | $content =~ s/%%TEST_GLOBALS_HTTP%%/$self->test_globals_http()/gmse; 239 | $content =~ s/%%TESTDIR%%/$self->{_testdir}/gms; 240 | 241 | return $self->write_file($name, $content); 242 | } 243 | 244 | sub run_daemon($;@) { 245 | my ($self, $code, @args) = @_; 246 | 247 | my $pid = fork(); 248 | die "Can't fork daemon: $!\n" unless defined $pid; 249 | 250 | if ($pid == 0) { 251 | if (ref($code) eq 'CODE') { 252 | $code->(@args); 253 | exit 0; 254 | } else { 255 | exec($code, @args); 256 | } 257 | } 258 | 259 | $self->{_daemons} = [] unless defined $self->{_daemons}; 260 | push @{$self->{_daemons}}, $pid; 261 | 262 | return $self; 263 | } 264 | 265 | sub testdir() { 266 | my ($self) = @_; 267 | return $self->{_testdir}; 268 | } 269 | 270 | sub test_globals() { 271 | my ($self) = @_; 272 | 273 | return $self->{_test_globals} 274 | if defined $self->{_test_globals}; 275 | 276 | my $s = ''; 277 | 278 | $s .= "pid $self->{_testdir}/nginx.pid;\n"; 279 | $s .= "error_log $self->{_testdir}/error.log debug;\n"; 280 | 281 | $self->{_test_globals} = $s; 282 | } 283 | 284 | sub test_globals_http() { 285 | my ($self) = @_; 286 | 287 | return $self->{_test_globals_http} 288 | if defined $self->{_test_globals_http}; 289 | 290 | my $s = ''; 291 | 292 | $s .= "root $self->{_testdir};\n"; 293 | $s .= "access_log $self->{_testdir}/access.log;\n"; 294 | $s .= "client_body_temp_path $self->{_testdir}/client_body_temp;\n"; 295 | 296 | $s .= "fastcgi_temp_path $self->{_testdir}/fastcgi_temp;\n" 297 | if $self->has_module('fastcgi'); 298 | 299 | $s .= "proxy_temp_path $self->{_testdir}/proxy_temp;\n" 300 | if $self->has_module('proxy'); 301 | 302 | $s .= "uwsgi_temp_path $self->{_testdir}/uwsgi_temp;\n" 303 | if $self->has_module('uwsgi'); 304 | 305 | $s .= "scgi_temp_path $self->{_testdir}/scgi_temp;\n" 306 | if $self->has_module('scgi'); 307 | 308 | $self->{_test_globals_http} = $s; 309 | } 310 | 311 | ############################################################################### 312 | 313 | sub log_core { 314 | return unless $ENV{TEST_NGINX_VERBOSE}; 315 | my ($prefix, $msg) = @_; 316 | ($prefix, $msg) = ('', $prefix) unless defined $msg; 317 | $prefix .= ' ' if length($prefix) > 0; 318 | 319 | if (length($msg) > 4096) { 320 | $msg = substr($msg, 0, 4096) 321 | . "(...logged only 4096 of " . length($msg) 322 | . " bytes)"; 323 | } 324 | 325 | $msg =~ s/^/# $prefix/gm; 326 | $msg =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1)) . (($1 eq "\n") ? "\n" : '')/gmxe; 327 | $msg .= "\n" unless $msg =~ /\n\Z/; 328 | print $msg; 329 | } 330 | 331 | sub log_out { 332 | log_core('>>', @_); 333 | } 334 | 335 | sub log_in { 336 | log_core('<<', @_); 337 | } 338 | 339 | ############################################################################### 340 | 341 | sub http_get($;%) { 342 | my ($url, %extra) = @_; 343 | return http(<new( 367 | Proto => 'tcp', 368 | PeerAddr => '127.0.0.1:8080' 369 | ); 370 | log_out($request); 371 | $s->print($request); 372 | local $/; 373 | select undef, undef, undef, $extra{sleep} if $extra{sleep}; 374 | return '' if $extra{aborted}; 375 | $reply = $s->getline(); 376 | alarm(0); 377 | }; 378 | alarm(0); 379 | if ($@) { 380 | log_in("died: $@"); 381 | return undef; 382 | } else { 383 | log_in($reply); 384 | } 385 | return $reply; 386 | } 387 | 388 | ############################################################################### 389 | 390 | sub http_gzip_request { 391 | my ($url) = @_; 392 | my $r = http(<builder->skip( 432 | "IO::Uncompress::Gunzip not installed", 1) if $@; 433 | 434 | my $in = http_content($text); 435 | my $out; 436 | 437 | IO::Uncompress::Gunzip::gunzip(\$in => \$out); 438 | 439 | Test::More->builder->like($out, $re, $name); 440 | } 441 | } 442 | 443 | ############################################################################### 444 | 445 | 1; 446 | 447 | ############################################################################### 448 | -------------------------------------------------------------------------------- /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 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | typedef struct { 17 | ngx_http_upstream_conf_t upstream; 18 | ngx_int_t index; 19 | ngx_int_t db; 20 | ngx_uint_t gzip_flag; 21 | } ngx_http_redis_loc_conf_t; 22 | 23 | 24 | typedef struct { 25 | size_t rest; 26 | ngx_http_request_t *request; 27 | ngx_str_t key; 28 | } ngx_http_redis_ctx_t; 29 | 30 | 31 | static ngx_int_t ngx_http_redis_create_request(ngx_http_request_t *r); 32 | static ngx_int_t ngx_http_redis_reinit_request(ngx_http_request_t *r); 33 | static ngx_int_t ngx_http_redis_process_header(ngx_http_request_t *r); 34 | static ngx_int_t ngx_http_redis_filter_init(void *data); 35 | static ngx_int_t ngx_http_redis_filter(void *data, ssize_t bytes); 36 | static void ngx_http_redis_abort_request(ngx_http_request_t *r); 37 | static void ngx_http_redis_finalize_request(ngx_http_request_t *r, 38 | ngx_int_t rc); 39 | 40 | static ngx_int_t ngx_http_redis_add_variables(ngx_conf_t *cf); 41 | static void *ngx_http_redis_create_loc_conf(ngx_conf_t *cf); 42 | static char *ngx_http_redis_merge_loc_conf(ngx_conf_t *cf, 43 | void *parent, void *child); 44 | 45 | static char *ngx_http_redis_pass(ngx_conf_t *cf, ngx_command_t *cmd, 46 | void *conf); 47 | 48 | static ngx_conf_bitmask_t ngx_http_redis_next_upstream_masks[] = { 49 | { ngx_string("error"), NGX_HTTP_UPSTREAM_FT_ERROR }, 50 | { ngx_string("timeout"), NGX_HTTP_UPSTREAM_FT_TIMEOUT }, 51 | { ngx_string("invalid_response"), NGX_HTTP_UPSTREAM_FT_INVALID_HEADER }, 52 | { ngx_string("not_found"), NGX_HTTP_UPSTREAM_FT_HTTP_404 }, 53 | { ngx_string("off"), NGX_HTTP_UPSTREAM_FT_OFF }, 54 | { ngx_null_string, 0 } 55 | }; 56 | 57 | 58 | static ngx_command_t ngx_http_redis_commands[] = { 59 | 60 | { ngx_string("redis_pass"), 61 | NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1, 62 | ngx_http_redis_pass, 63 | NGX_HTTP_LOC_CONF_OFFSET, 64 | 0, 65 | NULL }, 66 | 67 | #if defined nginx_version && nginx_version >= 8022 68 | { ngx_string("redis_bind"), 69 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 70 | ngx_http_upstream_bind_set_slot, 71 | NGX_HTTP_LOC_CONF_OFFSET, 72 | offsetof(ngx_http_redis_loc_conf_t, upstream.local), 73 | NULL }, 74 | #endif 75 | 76 | { ngx_string("redis_connect_timeout"), 77 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 78 | ngx_conf_set_msec_slot, 79 | NGX_HTTP_LOC_CONF_OFFSET, 80 | offsetof(ngx_http_redis_loc_conf_t, upstream.connect_timeout), 81 | NULL }, 82 | 83 | { ngx_string("redis_send_timeout"), 84 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 85 | ngx_conf_set_msec_slot, 86 | NGX_HTTP_LOC_CONF_OFFSET, 87 | offsetof(ngx_http_redis_loc_conf_t, upstream.send_timeout), 88 | NULL }, 89 | 90 | { ngx_string("redis_buffer_size"), 91 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 92 | ngx_conf_set_size_slot, 93 | NGX_HTTP_LOC_CONF_OFFSET, 94 | offsetof(ngx_http_redis_loc_conf_t, upstream.buffer_size), 95 | NULL }, 96 | 97 | { ngx_string("redis_read_timeout"), 98 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 99 | ngx_conf_set_msec_slot, 100 | NGX_HTTP_LOC_CONF_OFFSET, 101 | offsetof(ngx_http_redis_loc_conf_t, upstream.read_timeout), 102 | NULL }, 103 | 104 | { ngx_string("redis_next_upstream"), 105 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, 106 | ngx_conf_set_bitmask_slot, 107 | NGX_HTTP_LOC_CONF_OFFSET, 108 | offsetof(ngx_http_redis_loc_conf_t, upstream.next_upstream), 109 | &ngx_http_redis_next_upstream_masks }, 110 | 111 | { ngx_string("redis_gzip_flag"), 112 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 113 | ngx_conf_set_num_slot, 114 | NGX_HTTP_LOC_CONF_OFFSET, 115 | offsetof(ngx_http_redis_loc_conf_t, gzip_flag), 116 | NULL }, 117 | 118 | ngx_null_command 119 | }; 120 | 121 | 122 | static ngx_http_module_t ngx_http_redis_module_ctx = { 123 | ngx_http_redis_add_variables, /* preconfiguration */ 124 | NULL, /* postconfiguration */ 125 | 126 | NULL, /* create main configuration */ 127 | NULL, /* init main configuration */ 128 | 129 | NULL, /* create server configuration */ 130 | NULL, /* merge server configuration */ 131 | 132 | ngx_http_redis_create_loc_conf, /* create location configration */ 133 | ngx_http_redis_merge_loc_conf /* merge location configration */ 134 | }; 135 | 136 | 137 | ngx_module_t ngx_http_redis_module = { 138 | NGX_MODULE_V1, 139 | &ngx_http_redis_module_ctx, /* module context */ 140 | ngx_http_redis_commands, /* module directives */ 141 | NGX_HTTP_MODULE, /* module type */ 142 | NULL, /* init master */ 143 | NULL, /* init module */ 144 | NULL, /* init process */ 145 | NULL, /* init thread */ 146 | NULL, /* exit thread */ 147 | NULL, /* exit process */ 148 | NULL, /* exit master */ 149 | NGX_MODULE_V1_PADDING 150 | }; 151 | 152 | /* Initialize additional var for hide "Content-Enconding: gzip" header */ 153 | static ngx_str_t ngx_http_redis_hide_headers[] = { 154 | ngx_null_string 155 | }; 156 | 157 | static ngx_str_t ngx_http_redis_key = ngx_string("redis_key"); 158 | static ngx_str_t ngx_http_redis_db = ngx_string("redis_db"); 159 | static ngx_uint_t ngx_http_redis_db_index; 160 | 161 | 162 | #define NGX_HTTP_REDIS_END (sizeof(ngx_http_redis_end) - 1) 163 | static u_char ngx_http_redis_end[] = CRLF; 164 | 165 | 166 | static ngx_int_t 167 | ngx_http_redis_handler(ngx_http_request_t *r) 168 | { 169 | ngx_int_t rc; 170 | ngx_http_upstream_t *u; 171 | ngx_http_redis_ctx_t *ctx; 172 | ngx_http_redis_loc_conf_t *rlcf; 173 | 174 | if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) { 175 | return NGX_HTTP_NOT_ALLOWED; 176 | } 177 | 178 | rc = ngx_http_discard_request_body(r); 179 | 180 | if (rc != NGX_OK) { 181 | return rc; 182 | } 183 | 184 | if (ngx_http_set_content_type(r) != NGX_OK) { 185 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 186 | } 187 | 188 | #if defined nginx_version && nginx_version >= 8011 189 | if (ngx_http_upstream_create(r) != NGX_OK) { 190 | #else 191 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 192 | 193 | u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); 194 | if (u == NULL) { 195 | #endif 196 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 197 | } 198 | 199 | #if defined nginx_version && nginx_version >= 8011 200 | u = r->upstream; 201 | #endif 202 | 203 | #if defined nginx_version && nginx_version >= 8037 204 | ngx_str_set(&u->schema, "redis://"); 205 | #else 206 | u->schema.len = sizeof("redis://") - 1; 207 | u->schema.data = (u_char *) "redis://"; 208 | #endif 209 | 210 | #if defined nginx_version && nginx_version >= 8011 211 | u->output.tag = (ngx_buf_tag_t) &ngx_http_redis_module; 212 | #else 213 | u->peer.log = r->connection->log; 214 | u->peer.log_error = NGX_ERROR_ERR; 215 | #endif 216 | 217 | #if defined nginx_version && nginx_version >= 8011 218 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 219 | #else 220 | u->output.tag = (ngx_buf_tag_t) &ngx_http_redis_module; 221 | #endif 222 | 223 | u->conf = &rlcf->upstream; 224 | 225 | u->create_request = ngx_http_redis_create_request; 226 | u->reinit_request = ngx_http_redis_reinit_request; 227 | u->process_header = ngx_http_redis_process_header; 228 | u->abort_request = ngx_http_redis_abort_request; 229 | u->finalize_request = ngx_http_redis_finalize_request; 230 | 231 | #if defined nginx_version && nginx_version < 8011 232 | r->upstream = u; 233 | #endif 234 | 235 | ctx = ngx_palloc(r->pool, sizeof(ngx_http_redis_ctx_t)); 236 | if (ctx == NULL) { 237 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 238 | } 239 | 240 | ctx->rest = NGX_HTTP_REDIS_END; 241 | ctx->request = r; 242 | 243 | ngx_http_set_ctx(r, ctx, ngx_http_redis_module); 244 | 245 | u->input_filter_init = ngx_http_redis_filter_init; 246 | u->input_filter = ngx_http_redis_filter; 247 | u->input_filter_ctx = ctx; 248 | 249 | #if defined nginx_version && nginx_version >= 8011 250 | r->main->count++; 251 | #endif 252 | 253 | ngx_http_upstream_init(r); 254 | 255 | return NGX_DONE; 256 | } 257 | 258 | 259 | static ngx_int_t 260 | ngx_http_redis_create_request(ngx_http_request_t *r) 261 | { 262 | size_t len, get_len, select_len; 263 | uintptr_t escape; 264 | ngx_buf_t *b; 265 | ngx_chain_t *cl; 266 | ngx_http_redis_ctx_t *ctx; 267 | ngx_http_variable_value_t *vv[2]; 268 | ngx_http_redis_loc_conf_t *rlcf; 269 | 270 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 271 | 272 | vv[0] = ngx_http_get_indexed_variable(r, ngx_http_redis_db_index); 273 | 274 | /* 275 | * If user do not select redis database in nginx.conf by redis_db 276 | * variable, just add size of "select 0" to request. This is add 277 | * some overhead in talk with redis, but this way simplify parsing 278 | * the redis answer in ngx_http_redis_process_header(). 279 | */ 280 | if (vv[0] == NULL || vv[0]->not_found || vv[0]->len == 0) { 281 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 282 | "select 0 redis database" ); 283 | select_len = sizeof("*2\r\n$6\r\nselect\r\n$1\r\n0\r\n") - 1; 284 | } else { 285 | /* We have to pass the lengths of strings for the Redis protocol */ 286 | int db_len = (vv[0]->len > 99 ? 3 : 2); 287 | select_len = sizeof("*2\r\n$6\r\nselect\r\n$\r\n\r\n") - 1 + vv[0]->len + db_len; 288 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 289 | "select %s redis database", vv[0]->data); 290 | } 291 | 292 | len = select_len; 293 | 294 | vv[1] = ngx_http_get_indexed_variable(r, rlcf->index); 295 | 296 | /* If nginx.conf have no redis_key return error. */ 297 | if (vv[1] == NULL || vv[1]->not_found || vv[1]->len == 0) { 298 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 299 | "the \"$redis_key\" variable is not set"); 300 | return NGX_ERROR; 301 | } 302 | 303 | /* Count have space required escape symbols. */ 304 | escape = 2 * ngx_escape_uri(NULL, vv[1]->data, vv[1]->len, NGX_ESCAPE_REDIS); 305 | 306 | /* We need to include the command length and the length 307 | of the command length in bytes. Redis keys can be up to 308 | 2^31 which is ten digits in length so we just allocate that. */ 309 | get_len = sizeof("*2\r\n$3\r\nget\r\n$\r\n\r\n") - 1 + escape + 10 + vv[1]->len; 310 | 311 | len += get_len; 312 | 313 | ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, 314 | "ngx_http_redis allocating %d", len 315 | ); 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 "select " for request. */ 334 | b->last = ngx_snprintf( 335 | b->last, select_len, "*2\r\n$6\r\nselect\r\n$%d\r\n", 336 | vv[0]->len == 0 ? 1 : vv[0]->len 337 | ); 338 | 339 | /* Get context redis_db from configuration file. */ 340 | ctx = ngx_http_get_module_ctx(r, ngx_http_redis_module); 341 | 342 | ctx->key.data = b->last; 343 | 344 | /* 345 | * Add "0" as redis number db to request if redis_db undefined, 346 | * othervise add real number from context. 347 | */ 348 | if (vv[0] == NULL || vv[0]->not_found || vv[0]->len == 0) { 349 | ngx_log_debug0(NGX_LOG_DEBUG, r->connection->log, 0, 350 | "select 0 redis database" ); 351 | *b->last++ = '0'; 352 | } else { 353 | b->last = ngx_copy(b->last, vv[0]->data, vv[0]->len); 354 | ctx->key.len = b->last - ctx->key.data; 355 | ngx_log_debug1(NGX_LOG_DEBUG, r->connection->log, 0, 356 | "select %V redis database", &ctx->key); 357 | } 358 | 359 | /* Add "\r\n". */ 360 | *b->last++ = CR; *b->last++ = LF; 361 | 362 | 363 | /* Add "get" command with appropriate protocol information. */ 364 | b->last = ngx_snprintf(b->last, get_len, "*2\r\n$3\r\nget\r\n$%d\r\n\r\n", vv[1]->len); 365 | 366 | /* Get context redis_key from nginx.conf. */ 367 | ctx = ngx_http_get_module_ctx(r, ngx_http_redis_module); 368 | 369 | ctx->key.data = b->last; 370 | 371 | /* 372 | * If no escape symbols then copy data as is, othervise use 373 | * escape-copy function. 374 | */ 375 | 376 | if (escape == 0) { 377 | b->last = ngx_copy(b->last, vv[1]->data, vv[1]->len); 378 | 379 | } else { 380 | b->last = (u_char *) ngx_escape_uri(b->last, vv[1]->data, vv[1]->len, 381 | NGX_ESCAPE_REDIS); 382 | } 383 | 384 | ctx->key.len = vv[1]->len; 385 | 386 | /* 387 | * Summary, the request looks like this: 388 | * "select $redis_db\r\nget $redis_key\r\n", where 389 | * $redis_db and $redis_key are variable's values. 390 | */ 391 | 392 | return NGX_OK; 393 | } 394 | 395 | 396 | static ngx_int_t 397 | ngx_http_redis_reinit_request(ngx_http_request_t *r) 398 | { 399 | return NGX_OK; 400 | } 401 | 402 | 403 | static ngx_int_t 404 | ngx_http_redis_process_header(ngx_http_request_t *r) 405 | { 406 | u_char *p, *len; 407 | u_int c, try; 408 | ngx_str_t line; 409 | ngx_table_elt_t *h; 410 | ngx_http_upstream_t *u; 411 | ngx_http_redis_ctx_t *ctx; 412 | ngx_http_redis_loc_conf_t *rlcf; 413 | 414 | c = try = 0; 415 | 416 | u = r->upstream; 417 | 418 | p = u->buffer.pos; 419 | 420 | /* 421 | * Good answer from redis should looks like this: 422 | * "+OK\r\n$8\r\n12345678\r\n" 423 | * 424 | * Here is: 425 | * "+OK" is answer for first command "select 0". 426 | * Next two strings are answer for command "get $redis_key", where 427 | * 428 | * "$8" is length of following next string and 429 | * "12345678" is value of $redis_key, the string. 430 | * 431 | * So, if the first symbol is: 432 | * "+" (good answer) - try to find 2 strings; 433 | * "-" (bad answer) - try to find 1 string; 434 | * othervise answer is invalid. 435 | */ 436 | if (*p == '+') { 437 | try = 2; 438 | } else if (*p == '-') { 439 | try = 1; 440 | } else { 441 | goto no_valid; 442 | } 443 | 444 | for (p = u->buffer.pos; p < u->buffer.last; p++) { 445 | if (*p == LF) { 446 | c++; 447 | if (c == try) { 448 | goto found; 449 | } 450 | } 451 | } 452 | 453 | return NGX_AGAIN; 454 | 455 | found: 456 | 457 | *p = '\0'; 458 | 459 | line.len = p - u->buffer.pos - 1; 460 | line.data = u->buffer.pos; 461 | 462 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 463 | "redis: \"%V\"", &line); 464 | 465 | p = u->buffer.pos; 466 | 467 | /* Get context of redis_key for future error messages, i.e. ctx->key */ 468 | ctx = ngx_http_get_module_ctx(r, ngx_http_redis_module); 469 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_redis_module); 470 | 471 | /* Compare pointer and error message, if yes set 502 and return */ 472 | if (ngx_strncmp(p, "-ERR", sizeof("-ERR") - 1) == 0) { 473 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 474 | "redis sent error in response \"%V\" " 475 | "for key \"%V\"", 476 | &line, &ctx->key); 477 | 478 | u->headers_in.status_n = 502; 479 | u->state->status = 502; 480 | 481 | return NGX_OK; 482 | } 483 | 484 | /* Compare pointer and good message, if yes move on the pointer */ 485 | if (ngx_strncmp(p, "+OK\r\n", sizeof("+OK\r\n") - 1) == 0) { 486 | p += sizeof("+OK\r\n") - 1; 487 | } 488 | 489 | /* 490 | * Compare pointer and "get" answer. As said before, "$" means, that 491 | * next symbols are length for upcoming key, "-1" means no key. 492 | * Set 404 and return. 493 | */ 494 | if (ngx_strncmp(p, "$-1", sizeof("$-1") - 1) == 0) { 495 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 496 | "key: \"%V\" was not found by redis", &ctx->key); 497 | 498 | u->headers_in.status_n = 404; 499 | u->state->status = 404; 500 | #if defined nginx_version && nginx_version >= 1001004 501 | u->keepalive = 1; 502 | #endif 503 | 504 | return NGX_OK; 505 | } 506 | 507 | /* Compare pointer and "get" answer, if "$"... */ 508 | if (ngx_strncmp(p, "$", sizeof("$") - 1) == 0) { 509 | 510 | /* move on pointer */ 511 | p += sizeof("$") - 1; 512 | 513 | /* set len to pointer */ 514 | len = p; 515 | 516 | /* if defined gzip_flag... */ 517 | if (rlcf->gzip_flag) { 518 | /* hash init */ 519 | h = ngx_list_push(&r->upstream->headers_in.headers); 520 | if (h == NULL) { 521 | return NGX_ERROR; 522 | } 523 | 524 | /* 525 | * add Content-Encoding header for future gunzipping 526 | * with ngx_http_gunzip_filter module 527 | */ 528 | h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash( 529 | ngx_hash(ngx_hash(ngx_hash( 530 | ngx_hash(ngx_hash(ngx_hash( 531 | ngx_hash(ngx_hash(ngx_hash( 532 | ngx_hash(ngx_hash('c', 'o'), 'n'), 't'), 'e'), 533 | 'n'), 't'), '-'), 'e'), 'n'), 'c'), 'o'), 534 | 'd'), 'i'), 'n'), 'g'); 535 | ngx_str_set(&h->key, "Content-Encoding"); 536 | ngx_str_set(&h->value, "gzip"); 537 | h->lowcase_key = (u_char*) "content-encoding"; 538 | #if (NGX_HTTP_GZIP) 539 | u->headers_in.content_encoding = h; 540 | #endif 541 | } 542 | 543 | /* try to find end of string */ 544 | while (*p && *p++ != CR) { /* void */ } 545 | 546 | /* 547 | * get the length of upcoming redis_key value, convert from ascii 548 | * if the length is empty, return 549 | */ 550 | #if defined nginx_version && nginx_version < 1001004 551 | r->headers_out.content_length_n = ngx_atoof(len, p - len - 1); 552 | if (r->headers_out.content_length_n == -1) { 553 | #else 554 | u->headers_in.content_length_n = ngx_atoof(len, p - len - 1); 555 | if (u->headers_in.content_length_n == -1) { 556 | #endif 557 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 558 | "redis sent invalid length in response \"%V\" " 559 | "for key \"%V\"", 560 | &line, &ctx->key); 561 | return NGX_HTTP_UPSTREAM_INVALID_HEADER; 562 | } 563 | 564 | /* The length of answer is not empty, set 200 */ 565 | u->headers_in.status_n = 200; 566 | u->state->status = 200; 567 | /* Set position to the first symbol of data and return */ 568 | u->buffer.pos = p + 1; 569 | 570 | return NGX_OK; 571 | } 572 | 573 | no_valid: 574 | 575 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 576 | "redis sent invalid response: \"%V\"", &line); 577 | 578 | return NGX_HTTP_UPSTREAM_INVALID_HEADER; 579 | } 580 | 581 | 582 | static ngx_int_t 583 | ngx_http_redis_filter_init(void *data) 584 | { 585 | ngx_http_redis_ctx_t *ctx = data; 586 | 587 | ngx_http_upstream_t *u; 588 | 589 | u = ctx->request->upstream; 590 | 591 | #if defined nginx_version && nginx_version < 1005003 592 | u->length += NGX_HTTP_REDIS_END; 593 | #else 594 | if (u->headers_in.status_n != 404) { 595 | u->length = u->headers_in.content_length_n + NGX_HTTP_REDIS_END; 596 | ctx->rest = NGX_HTTP_REDIS_END; 597 | 598 | } else { 599 | u->length = 0; 600 | } 601 | #endif 602 | 603 | return NGX_OK; 604 | } 605 | 606 | 607 | static ngx_int_t 608 | ngx_http_redis_filter(void *data, ssize_t bytes) 609 | { 610 | ngx_http_redis_ctx_t *ctx = data; 611 | 612 | u_char *last; 613 | ngx_buf_t *b; 614 | ngx_chain_t *cl, **ll; 615 | ngx_http_upstream_t *u; 616 | 617 | u = ctx->request->upstream; 618 | b = &u->buffer; 619 | 620 | #if defined nginx_version && nginx_version < 1001004 621 | if (u->length == ctx->rest) { 622 | #else 623 | if (u->length == (ssize_t) ctx->rest) { 624 | #endif 625 | 626 | if (ngx_strncmp(b->last, 627 | ngx_http_redis_end + NGX_HTTP_REDIS_END - ctx->rest, 628 | bytes) 629 | != 0) 630 | { 631 | ngx_log_error(NGX_LOG_ERR, ctx->request->connection->log, 0, 632 | "redis sent invalid trailer"); 633 | 634 | u->length = 0; 635 | ctx->rest = 0; 636 | 637 | return NGX_OK; 638 | } 639 | 640 | u->length -= bytes; 641 | ctx->rest -= bytes; 642 | 643 | #if defined nginx_version && nginx_version >= 1001004 644 | if (u->length == 0) { 645 | u->keepalive = 1; 646 | } 647 | #endif 648 | 649 | return NGX_OK; 650 | } 651 | 652 | for (cl = u->out_bufs, ll = &u->out_bufs; cl; cl = cl->next) { 653 | ll = &cl->next; 654 | } 655 | 656 | cl = ngx_chain_get_free_buf(ctx->request->pool, &u->free_bufs); 657 | if (cl == NULL) { 658 | return NGX_ERROR; 659 | } 660 | 661 | cl->buf->flush = 1; 662 | cl->buf->memory = 1; 663 | 664 | *ll = cl; 665 | 666 | last = b->last; 667 | cl->buf->pos = last; 668 | b->last += bytes; 669 | cl->buf->last = b->last; 670 | cl->buf->tag = u->output.tag; 671 | 672 | ngx_log_debug4(NGX_LOG_DEBUG_HTTP, ctx->request->connection->log, 0, 673 | "redis filter bytes:%z size:%z length:%z rest:%z", 674 | bytes, b->last - b->pos, u->length, ctx->rest); 675 | 676 | if (bytes <= (ssize_t) (u->length - NGX_HTTP_REDIS_END)) { 677 | u->length -= bytes; 678 | return NGX_OK; 679 | } 680 | 681 | last += (size_t) (u->length - NGX_HTTP_REDIS_END); 682 | 683 | if (ngx_strncmp(last, ngx_http_redis_end, b->last - last) != 0) { 684 | ngx_log_error(NGX_LOG_ERR, ctx->request->connection->log, 0, 685 | "redis sent invalid trailer"); 686 | 687 | #if defined nginx_version && nginx_version >= 1001004 688 | b->last = last; 689 | cl->buf->last = last; 690 | u->length = 0; 691 | ctx->rest = 0; 692 | 693 | return NGX_OK; 694 | #endif 695 | } 696 | 697 | ctx->rest -= b->last - last; 698 | b->last = last; 699 | cl->buf->last = last; 700 | u->length = ctx->rest; 701 | 702 | #if defined nginx_version && nginx_version >= 1001004 703 | if (u->length == 0) { 704 | u->keepalive = 1; 705 | } 706 | #endif 707 | 708 | return NGX_OK; 709 | } 710 | 711 | 712 | static void 713 | ngx_http_redis_abort_request(ngx_http_request_t *r) 714 | { 715 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 716 | "abort http redis request"); 717 | return; 718 | } 719 | 720 | 721 | static void 722 | ngx_http_redis_finalize_request(ngx_http_request_t *r, ngx_int_t rc) 723 | { 724 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 725 | "finalize http redis request"); 726 | return; 727 | } 728 | 729 | 730 | static void * 731 | ngx_http_redis_create_loc_conf(ngx_conf_t *cf) 732 | { 733 | ngx_http_redis_loc_conf_t *conf; 734 | 735 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_redis_loc_conf_t)); 736 | if (conf == NULL) { 737 | #if defined nginx_version && nginx_version >= 8011 738 | return NULL; 739 | #else 740 | return NGX_CONF_ERROR; 741 | #endif 742 | } 743 | 744 | /* 745 | * set by ngx_pcalloc(): 746 | * 747 | * conf->upstream.bufs.num = 0; 748 | * conf->upstream.next_upstream = 0; 749 | * conf->upstream.temp_path = NULL; 750 | * conf->upstream.uri = { 0, NULL }; 751 | * conf->upstream.location = NULL; 752 | */ 753 | 754 | conf->upstream.connect_timeout = NGX_CONF_UNSET_MSEC; 755 | conf->upstream.send_timeout = NGX_CONF_UNSET_MSEC; 756 | conf->upstream.read_timeout = NGX_CONF_UNSET_MSEC; 757 | 758 | conf->upstream.buffer_size = NGX_CONF_UNSET_SIZE; 759 | 760 | /* the hardcoded values */ 761 | conf->upstream.cyclic_temp_file = 0; 762 | conf->upstream.buffering = 0; 763 | conf->upstream.ignore_client_abort = 0; 764 | conf->upstream.send_lowat = 0; 765 | conf->upstream.bufs.num = 0; 766 | conf->upstream.busy_buffers_size = 0; 767 | conf->upstream.max_temp_file_size = 0; 768 | conf->upstream.temp_file_write_size = 0; 769 | conf->upstream.intercept_errors = 1; 770 | conf->upstream.intercept_404 = 1; 771 | conf->upstream.pass_request_headers = 0; 772 | conf->upstream.pass_request_body = 0; 773 | 774 | /* 775 | * initialize additional parameters for hide 776 | * "Content-Encoding: gzip" header 777 | */ 778 | conf->upstream.hide_headers = NGX_CONF_UNSET_PTR; 779 | conf->upstream.pass_headers = NGX_CONF_UNSET_PTR; 780 | 781 | conf->index = NGX_CONF_UNSET; 782 | conf->db = NGX_CONF_UNSET; 783 | conf->gzip_flag = NGX_CONF_UNSET_UINT; 784 | 785 | return conf; 786 | } 787 | 788 | 789 | static char * 790 | ngx_http_redis_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 791 | { 792 | ngx_hash_init_t hash; 793 | 794 | ngx_http_redis_loc_conf_t *prev = parent; 795 | ngx_http_redis_loc_conf_t *conf = child; 796 | 797 | ngx_conf_merge_msec_value(conf->upstream.connect_timeout, 798 | prev->upstream.connect_timeout, 60000); 799 | 800 | ngx_conf_merge_msec_value(conf->upstream.send_timeout, 801 | prev->upstream.send_timeout, 60000); 802 | 803 | ngx_conf_merge_msec_value(conf->upstream.read_timeout, 804 | prev->upstream.read_timeout, 60000); 805 | 806 | ngx_conf_merge_size_value(conf->upstream.buffer_size, 807 | prev->upstream.buffer_size, 808 | (size_t) ngx_pagesize); 809 | 810 | ngx_conf_merge_bitmask_value(conf->upstream.next_upstream, 811 | prev->upstream.next_upstream, 812 | (NGX_CONF_BITMASK_SET 813 | |NGX_HTTP_UPSTREAM_FT_ERROR 814 | |NGX_HTTP_UPSTREAM_FT_TIMEOUT)); 815 | 816 | if (conf->upstream.next_upstream & NGX_HTTP_UPSTREAM_FT_OFF) { 817 | conf->upstream.next_upstream = NGX_CONF_BITMASK_SET 818 | |NGX_HTTP_UPSTREAM_FT_OFF; 819 | } 820 | 821 | /* Initialize hash for hide "Content-Encoding" header */ 822 | hash.max_size = 512; 823 | hash.bucket_size = ngx_align(64, ngx_cacheline_size); 824 | hash.name = "redis_hide_headers_hash"; 825 | 826 | if (ngx_http_upstream_hide_headers_hash(cf, &conf->upstream, 827 | &prev->upstream, ngx_http_redis_hide_headers, &hash) 828 | != NGX_OK) 829 | { 830 | return NGX_CONF_ERROR; 831 | } 832 | 833 | if (conf->upstream.upstream == NULL) { 834 | conf->upstream.upstream = prev->upstream.upstream; 835 | } 836 | 837 | if (conf->index == NGX_CONF_UNSET) { 838 | conf->index = prev->index; 839 | } 840 | 841 | if (conf->db == NGX_CONF_UNSET) { 842 | conf->db = prev->db; 843 | } 844 | 845 | ngx_conf_merge_uint_value(conf->gzip_flag, prev->gzip_flag, 0); 846 | 847 | return NGX_CONF_OK; 848 | } 849 | 850 | 851 | static char * 852 | ngx_http_redis_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 853 | { 854 | ngx_http_redis_loc_conf_t *rlcf = conf; 855 | 856 | ngx_str_t *value; 857 | ngx_url_t u; 858 | ngx_http_core_loc_conf_t *clcf; 859 | 860 | if (rlcf->upstream.upstream) { 861 | return "is duplicate"; 862 | } 863 | 864 | value = cf->args->elts; 865 | 866 | ngx_memzero(&u, sizeof(ngx_url_t)); 867 | 868 | u.url = value[1]; 869 | u.no_resolve = 1; 870 | 871 | rlcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0); 872 | if (rlcf->upstream.upstream == NULL) { 873 | return NGX_CONF_ERROR; 874 | } 875 | 876 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 877 | 878 | clcf->handler = ngx_http_redis_handler; 879 | 880 | if (clcf->name.data[clcf->name.len - 1] == '/') { 881 | clcf->auto_redirect = 1; 882 | } 883 | 884 | rlcf->index = ngx_http_get_variable_index(cf, &ngx_http_redis_key); 885 | 886 | if (rlcf->index == NGX_ERROR) { 887 | return NGX_CONF_ERROR; 888 | } 889 | 890 | rlcf->db = ngx_http_get_variable_index(cf, &ngx_http_redis_db); 891 | 892 | return NGX_CONF_OK; 893 | } 894 | 895 | 896 | static ngx_int_t 897 | ngx_http_redis_reset_variable(ngx_http_request_t *r, 898 | ngx_http_variable_value_t *v, uintptr_t data) 899 | { 900 | *v = ngx_http_variable_null_value; 901 | 902 | return NGX_OK; 903 | } 904 | 905 | 906 | static ngx_int_t 907 | ngx_http_redis_add_variables(ngx_conf_t *cf) 908 | { 909 | ngx_int_t n; 910 | ngx_http_variable_t *var; 911 | 912 | var = ngx_http_add_variable(cf, &ngx_http_redis_db, 913 | NGX_HTTP_VAR_CHANGEABLE); 914 | if (var == NULL) { 915 | return NGX_ERROR; 916 | } 917 | 918 | var->get_handler = ngx_http_redis_reset_variable; 919 | 920 | n = ngx_http_get_variable_index(cf, &ngx_http_redis_db); 921 | if (n == NGX_ERROR) { 922 | return NGX_ERROR; 923 | } 924 | 925 | ngx_http_redis_db_index = n; 926 | 927 | return NGX_OK; 928 | } 929 | --------------------------------------------------------------------------------