├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── config ├── src ├── ngx_http_rate_limit_handler.c ├── ngx_http_rate_limit_handler.h ├── ngx_http_rate_limit_module.c ├── ngx_http_rate_limit_module.h ├── ngx_http_rate_limit_reply.c ├── ngx_http_rate_limit_reply.h ├── ngx_http_rate_limit_upstream.c ├── ngx_http_rate_limit_upstream.h ├── ngx_http_rate_limit_util.c └── ngx_http_rate_limit_util.h └── t └── sanity.t /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | ColumnLimit: 80 5 | AlignConsecutiveDeclarations: true 6 | AlwaysBreakAfterReturnType: AllDefinitions 7 | BreakBeforeBraces: Custom 8 | BraceWrapping: 9 | AfterClass: false 10 | AfterControlStatement: false 11 | AfterEnum: false 12 | AfterFunction: true 13 | AfterNamespace: true 14 | AfterObjCDeclaration: false 15 | AfterStruct: false 16 | AfterUnion: false 17 | BeforeCatch: false 18 | BeforeElse: false 19 | IndentBraces: false 20 | Cpp11BracedListStyle: false 21 | PointerAlignment: Right 22 | SpaceAfterCStyleCast: true 23 | ... 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | permissions: {} 6 | 7 | jobs: 8 | CI: 9 | runs-on: ${{ matrix.os }} 10 | permissions: 11 | contents: read 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | include: 16 | - os: ubuntu-24.04 17 | nginx-version: 1.27.3 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Install dependencies 24 | run: | 25 | # for Test::Nginx 26 | curl -fsSL https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg 27 | echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] https://openresty.org/package/ubuntu $(lsb_release -sc) main" | \ 28 | sudo tee /etc/apt/sources.list.d/openresty.list > /dev/null 29 | sudo apt-get update 30 | sudo apt-get install --no-install-recommends libtest-nginx-perl redis-server 31 | 32 | - name: Install nginx 33 | env: 34 | NGINX_VERSION: ${{ matrix.nginx-version }} 35 | working-directory: ${{ runner.temp }} 36 | run: | 37 | mkdir nginx 38 | curl -Ls https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz | \ 39 | tar xzC nginx --strip-components=1 40 | cd nginx 41 | ./configure --prefix="$HOME/nginx" --add-module=${{ github.workspace }} 42 | make -j$(nproc) 43 | make install 44 | 45 | - name: Install Redis rate limiter module 46 | working-directory: ${{ runner.temp }} 47 | run: | 48 | git clone https://github.com/onsigntv/redis-rate-limiter.git 49 | cd redis-rate-limiter 50 | make -j$(nproc) USE_MONOTONIC_CLOCK=1 51 | sudo install -D -t /usr/lib/redis/modules ratelimit.so 52 | 53 | - name: Load Redis rate limiter module 54 | run: | 55 | # Redis < 7 56 | # redis-cli MODULE LOAD /usr/lib/redis/modules/ratelimit.so 57 | # Redis >= 7 (due to `enable-module-command no` restriction) 58 | echo "loadmodule /usr/lib/redis/modules/ratelimit.so" | sudo tee -a /etc/redis/redis.conf 59 | sudo service redis-server restart 60 | 61 | - name: Prepare environment 62 | run: echo "$HOME/nginx/sbin" >> $GITHUB_PATH 63 | 64 | - name: Run integration tests 65 | run: prove -r t 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### C ### 2 | # Prerequisites 3 | *.d 4 | 5 | # Object files 6 | *.o 7 | *.ko 8 | *.obj 9 | *.elf 10 | 11 | # Linker output 12 | *.ilk 13 | *.map 14 | *.exp 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | 26 | # Shared objects 27 | *.so 28 | *.so.* 29 | 30 | ### CLion ### 31 | .idea/ 32 | 33 | ### Test::Nginx ### 34 | t/servroot/ 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018-present, Andries Louw Wolthuizen and Kleis Auke Wolthuizen. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of wsrv.nl and images.weserv.nl nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rate-limit-nginx-module 2 | 3 | [![CI status](https://github.com/weserv/rate-limit-nginx-module/workflows/CI/badge.svg)](https://github.com/weserv/rate-limit-nginx-module/actions) 4 | 5 | A Redis backed rate limit module for Nginx web servers. 6 | 7 | This implementation is based on the following [Redis module](https://redis.io/topics/modules-intro): 8 | 9 | * [redis-rate-limiter](https://github.com/onsigntv/redis-rate-limiter) 10 | 11 | Which offers a straightforward implementation of the fairly sophisticated [generic cell rate algorithm](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm), in 130 lines of C, without external dependencies. 12 | 13 | *This module is not distributed with the Nginx source.* 14 | 15 | ## Status 16 | 17 | This module is production ready. 18 | 19 | ## Synopsis 20 | 21 | ```nginx 22 | upstream redis { 23 | server 127.0.0.1:6379; 24 | 25 | # Or: server unix:/var/run/redis/redis.sock; 26 | 27 | # a pool with at most 1024 connections 28 | keepalive 1024; 29 | } 30 | 31 | geo $limit { 32 | default 1; 33 | 10.0.0.0/8 0; 34 | 192.168.0.0/24 0; 35 | } 36 | 37 | map $limit $limit_key { 38 | 0 ""; 39 | 1 $remote_addr; 40 | } 41 | 42 | rate_limit_status 429; 43 | 44 | location = /limit { 45 | rate_limit $limit_key requests=15 period=1m burst=20; 46 | rate_limit_pass redis; 47 | } 48 | 49 | location = /limit_b { 50 | rate_limit $limit_key requests=20 period=1m burst=25; 51 | rate_limit_prefix b; 52 | rate_limit_pass redis; 53 | } 54 | 55 | location = /quota { 56 | rate_limit $limit_key requests=15 period=1m burst=20; 57 | rate_limit_quantity 0; 58 | rate_limit_pass redis; 59 | rate_limit_headers on; 60 | } 61 | ``` 62 | 63 | ## Installation 64 | 65 | *Note: You will need to install the Redis module first, see the install instructions [here](https://github.com/onsigntv/redis-rate-limiter#install).* 66 | 67 | You can install this module manually by recompiling the standard Nginx core as follows: 68 | 69 | 1. Grab the nginx source code from [nginx.org](https://nginx.org) (this module is tested on version 1.27.3). 70 | 2. Clone this repository into a newly created directory (for e.g. `./rate-limit-nginx-module`) 71 | 3. Build the nginx source with this module: 72 | ```bash 73 | wget https://nginx.org/download/nginx-1.27.3.tar.gz 74 | tar -xzvf nginx-1.27.3.tar.gz 75 | cd nginx-1.27.3/ 76 | 77 | git clone https://github.com/weserv/rate-limit-nginx-module rate-limit-nginx-module 78 | 79 | # Here we assume you would install you nginx under /opt/nginx/. 80 | ./configure --prefix=/opt/nginx \ 81 | --add-module=rate-limit-nginx-module/ 82 | 83 | make -j$(nproc) 84 | make install 85 | ``` 86 | 87 | ## Test suite 88 | 89 | The following dependencies are required to run the test suite: 90 | 91 | * Nginx version >= 1.9.11 92 | 93 | * Perl modules: 94 | * [Test::Nginx](https://metacpan.org/pod/Test::Nginx::Socket) 95 | 96 | * Nginx modules: 97 | * ngx_http_rate_limit_module (i.e., this module) 98 | 99 | * Redis modules: 100 | * [redis-rate-limiter](https://github.com/onsigntv/redis-rate-limiter) 101 | 102 | * Applications: 103 | * redis: listening on the default port, 6379. 104 | 105 | To run the whole test suite in the default testing mode: 106 | ```bash 107 | cd /path/to/rate-limit-nginx-module 108 | export PATH=/path/to/your/nginx/sbin:$PATH 109 | prove -I/path/to/test-nginx/lib -r t 110 | ``` 111 | 112 | To run specific test files: 113 | ```bash 114 | cd /path/to/rate-limit-nginx-module 115 | export PATH=/path/to/your/nginx/sbin:$PATH 116 | prove -I/path/to/test-nginx/lib t/sanity.t 117 | ``` 118 | 119 | To run a specific test block in a particular test file, add the line 120 | `--- ONLY` to the test block you want to run, and then use the `prove` 121 | utility to run that `.t` file. 122 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | if [ -z "$ngx_module_link" ]; then 2 | cat << END 3 | 4 | $0: error: rate limit module requires recent version of NGINX (1.9.11+). 5 | 6 | END 7 | exit 1 8 | fi 9 | 10 | ngx_addon_name=ngx_http_rate_limit_module 11 | ngx_module_type=HTTP 12 | ngx_module_name=$ngx_addon_name 13 | ngx_module_deps=" \ 14 | $ngx_addon_dir/src/ngx_http_rate_limit_module.h \ 15 | $ngx_addon_dir/src/ngx_http_rate_limit_handler.h \ 16 | $ngx_addon_dir/src/ngx_http_rate_limit_upstream.h \ 17 | $ngx_addon_dir/src/ngx_http_rate_limit_reply.h \ 18 | $ngx_addon_dir/src/ngx_http_rate_limit_util.h \ 19 | " 20 | ngx_module_srcs=" \ 21 | $ngx_addon_dir/src/ngx_http_rate_limit_module.c \ 22 | $ngx_addon_dir/src/ngx_http_rate_limit_handler.c \ 23 | $ngx_addon_dir/src/ngx_http_rate_limit_upstream.c \ 24 | $ngx_addon_dir/src/ngx_http_rate_limit_reply.c \ 25 | $ngx_addon_dir/src/ngx_http_rate_limit_util.c \ 26 | " 27 | 28 | . auto/module 29 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_handler.c: -------------------------------------------------------------------------------- 1 | #include "ngx_http_rate_limit_handler.h" 2 | #include "ngx_http_rate_limit_reply.h" 3 | #include "ngx_http_rate_limit_upstream.h" 4 | #include "ngx_http_rate_limit_util.h" 5 | 6 | static ngx_int_t ngx_http_rate_limit_create_request(ngx_http_request_t *r); 7 | static ngx_int_t ngx_http_rate_limit_reinit_request(ngx_http_request_t *r); 8 | static ngx_int_t ngx_http_rate_limit_process_header(ngx_http_request_t *r); 9 | static ngx_int_t ngx_http_rate_limit_filter_init(void *data); 10 | static ngx_int_t ngx_http_rate_limit_filter(void *data, ssize_t bytes); 11 | static void ngx_http_rate_limit_abort_request(ngx_http_request_t *r); 12 | static void ngx_http_rate_limit_finalize_request(ngx_http_request_t *r, 13 | ngx_int_t rc); 14 | 15 | static ngx_str_t x_limit_header = ngx_string("X-RateLimit-Limit"); 16 | static ngx_str_t x_remaining_header = ngx_string("X-RateLimit-Remaining"); 17 | static ngx_str_t x_reset_header = ngx_string("X-RateLimit-Reset"); 18 | static ngx_str_t x_retry_after_header = ngx_string("Retry-After"); 19 | 20 | ngx_int_t 21 | ngx_http_rate_limit_handler(ngx_http_request_t *r) 22 | { 23 | ngx_http_upstream_t *u; 24 | ngx_http_rate_limit_ctx_t *ctx; 25 | ngx_http_rate_limit_loc_conf_t *rlcf; 26 | size_t len; 27 | u_char *p, *n; 28 | ngx_uint_t status; 29 | ngx_str_t target; 30 | ngx_url_t url; 31 | 32 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rate_limit_module); 33 | 34 | if (!rlcf->configured) { 35 | return NGX_DECLINED; 36 | } 37 | 38 | ctx = ngx_http_get_module_ctx(r, ngx_http_rate_limit_module); 39 | 40 | if (ctx != NULL) { 41 | if (!ctx->finalized) { 42 | return NGX_AGAIN; 43 | } 44 | 45 | status = r->upstream->state->status; 46 | 47 | /* Return appropriate status */ 48 | 49 | if (status == NGX_HTTP_TOO_MANY_REQUESTS) { 50 | ngx_log_error(rlcf->limit_log_level, r->connection->log, 0, 51 | "rate limit exceeded for key \"%V\"", &ctx->key); 52 | 53 | return rlcf->status_code; 54 | } 55 | 56 | if (status == NGX_HTTP_OK) { 57 | return NGX_OK; 58 | } 59 | 60 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 61 | "rate limit unexpected status: %ui", status); 62 | 63 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 64 | } 65 | 66 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_rate_limit_ctx_t)); 67 | if (ctx == NULL) { 68 | return NGX_ERROR; 69 | } 70 | 71 | if (ngx_http_complex_value(r, &rlcf->key, &ctx->key) != NGX_OK) { 72 | return NGX_ERROR; 73 | } 74 | 75 | if (ctx->key.len == 0) { 76 | return NGX_DECLINED; 77 | } 78 | 79 | len = rlcf->prefix.len; 80 | 81 | if (len > 0) { 82 | n = ngx_pnalloc(r->pool, len + ctx->key.len + 2); 83 | if (n == NULL) { 84 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 85 | } 86 | 87 | p = ngx_cpymem(n, rlcf->prefix.data, len); 88 | p = ngx_cpymem(p, "_", 1); 89 | ngx_cpystrn(p, ctx->key.data, ctx->key.len + 2); 90 | 91 | ctx->key.len += len + 1; 92 | ctx->key.data = n; 93 | } 94 | 95 | if (ctx->key.len > 65535) { 96 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 97 | "the value of the \"%V\" key " 98 | "is more than 65535 bytes: \"%V\"", 99 | &rlcf->key.value, &ctx->key); 100 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 101 | } 102 | 103 | ctx->request = r; 104 | 105 | if (ngx_http_upstream_create(r) != NGX_OK) { 106 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 107 | } 108 | 109 | u = r->upstream; 110 | 111 | if (rlcf->complex_target) { 112 | /* Variables used in the rate_limit_pass directive */ 113 | 114 | if (ngx_http_complex_value(r, rlcf->complex_target, &target) != 115 | NGX_OK) { 116 | return NGX_ERROR; 117 | } 118 | 119 | if (target.len == 0) { 120 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 121 | "handler: empty \"rate_limit_pass\" target"); 122 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 123 | } 124 | 125 | url.host = target; 126 | url.port = 0; 127 | url.no_resolve = 1; 128 | 129 | rlcf->upstream.upstream = ngx_http_rate_limit_upstream_add(r, &url); 130 | 131 | if (rlcf->upstream.upstream == NULL) { 132 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 133 | "rate limit: upstream \"%V\" not found", &target); 134 | 135 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 136 | } 137 | } 138 | 139 | ngx_str_set(&u->schema, "redis2://"); 140 | u->output.tag = (ngx_buf_tag_t) &ngx_http_rate_limit_module; 141 | 142 | u->conf = &rlcf->upstream; 143 | 144 | u->create_request = ngx_http_rate_limit_create_request; 145 | u->reinit_request = ngx_http_rate_limit_reinit_request; 146 | u->process_header = ngx_http_rate_limit_process_header; 147 | u->abort_request = ngx_http_rate_limit_abort_request; 148 | u->finalize_request = ngx_http_rate_limit_finalize_request; 149 | 150 | ngx_http_set_ctx(r, ctx, ngx_http_rate_limit_module); 151 | 152 | u->input_filter_init = ngx_http_rate_limit_filter_init; 153 | u->input_filter = ngx_http_rate_limit_filter; 154 | u->input_filter_ctx = ctx; 155 | 156 | r->main->count++; 157 | 158 | /* Initiate the upstream connection by calling NGINX upstream. */ 159 | ngx_http_upstream_init(r); 160 | 161 | /* Override the read event handler to our own */ 162 | u->read_event_handler = ngx_http_rate_limit_rev_handler; 163 | 164 | return NGX_AGAIN; 165 | } 166 | 167 | static ngx_int_t 168 | ngx_http_rate_limit_create_request(ngx_http_request_t *r) 169 | { 170 | ngx_int_t rc; 171 | ngx_buf_t *b; 172 | ngx_chain_t *cl; 173 | 174 | rc = ngx_http_rate_limit_build_command(r, &b); 175 | if (rc != NGX_OK) { 176 | return rc; 177 | } 178 | 179 | /* Allocate a buffer chain for NGINX. */ 180 | cl = ngx_alloc_chain_link(r->pool); 181 | if (cl == NULL) { 182 | return NGX_ERROR; 183 | } 184 | 185 | cl->buf = b; 186 | cl->next = NULL; 187 | 188 | /* We are only sending one buffer. */ 189 | b->last_buf = 1; 190 | 191 | /* Attach the buffer to the request. */ 192 | r->upstream->request_bufs = cl; 193 | 194 | return NGX_OK; 195 | } 196 | 197 | static ngx_int_t 198 | ngx_http_rate_limit_reinit_request(ngx_http_request_t *r) 199 | { 200 | ngx_http_upstream_t *u; 201 | 202 | u = r->upstream; 203 | 204 | /* Override the read event handler to our own */ 205 | u->read_event_handler = ngx_http_rate_limit_rev_handler; 206 | 207 | return NGX_OK; 208 | } 209 | 210 | static ngx_int_t 211 | ngx_http_rate_limit_process_header(ngx_http_request_t *r) 212 | { 213 | ngx_http_upstream_t *u; 214 | ngx_http_rate_limit_ctx_t *ctx; 215 | ngx_buf_t *b; 216 | u_char chr; 217 | ngx_str_t buf; 218 | 219 | u = r->upstream; 220 | b = &u->buffer; 221 | 222 | if (b->last - b->pos < (ssize_t) sizeof(u_char)) { 223 | return NGX_AGAIN; 224 | } 225 | 226 | ctx = ngx_http_get_module_ctx(r, ngx_http_rate_limit_module); 227 | if (ctx == NULL) { 228 | return NGX_ERROR; 229 | } 230 | 231 | /* the first char is the response header */ 232 | chr = *b->pos; 233 | 234 | /* we are always expecting a multi bulk reply */ 235 | if (chr != '*') { 236 | buf.data = b->pos; 237 | buf.len = b->last - b->pos; 238 | 239 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 240 | "rate limit: redis sent invalid response: \"%V\"", &buf); 241 | 242 | return NGX_HTTP_UPSTREAM_INVALID_HEADER; 243 | } 244 | 245 | ++b->pos; 246 | 247 | u->state->status = NGX_HTTP_OK; 248 | 249 | return NGX_OK; 250 | } 251 | 252 | static ngx_int_t 253 | ngx_http_rate_limit_filter_init(void *data) 254 | { 255 | return NGX_OK; 256 | } 257 | 258 | static ngx_int_t 259 | ngx_http_rate_limit_filter(void *data, ssize_t bytes) 260 | { 261 | ngx_http_rate_limit_ctx_t *ctx = data; 262 | 263 | return ngx_http_rate_limit_process_reply(ctx, bytes); 264 | } 265 | 266 | static void 267 | ngx_http_rate_limit_abort_request(ngx_http_request_t *r) 268 | { 269 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 270 | "abort http rate limit request"); 271 | return; 272 | } 273 | 274 | static void 275 | ngx_http_rate_limit_finalize_request(ngx_http_request_t *r, ngx_int_t rc) 276 | { 277 | ngx_http_rate_limit_ctx_t *ctx; 278 | ngx_http_rate_limit_loc_conf_t *rlcf; 279 | 280 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 281 | "finalize http rate limit request"); 282 | 283 | ctx = ngx_http_get_module_ctx(r, ngx_http_rate_limit_module); 284 | if (ctx == NULL) { 285 | return; 286 | } 287 | 288 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rate_limit_module); 289 | 290 | if (r->upstream->state->status == NGX_HTTP_TOO_MANY_REQUESTS || 291 | rlcf->enable_headers) { 292 | /* X-RateLimit-Limit HTTP header */ 293 | (void) ngx_set_custom_header(r, &x_limit_header, ctx->limit); 294 | 295 | /* X-RateLimit-Remaining HTTP header */ 296 | (void) ngx_set_custom_header(r, &x_remaining_header, ctx->remaining); 297 | 298 | /* X-RateLimit-Reset */ 299 | (void) ngx_set_custom_header(r, &x_reset_header, ctx->reset); 300 | 301 | /* Retry-After (always -1 if the action was allowed) */ 302 | if (ctx->retry_after != -1) { 303 | (void) ngx_set_custom_header(r, &x_retry_after_header, 304 | (ngx_uint_t) ctx->retry_after); 305 | } 306 | } 307 | 308 | ctx->finalized = 1; 309 | } 310 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef NGX_HTTP_RATE_LIMIT_HANDLER_H 2 | #define NGX_HTTP_RATE_LIMIT_HANDLER_H 3 | 4 | #include "ngx_http_rate_limit_module.h" 5 | 6 | ngx_int_t ngx_http_rate_limit_handler(ngx_http_request_t *r); 7 | 8 | #endif /* NGX_HTTP_RATE_LIMIT_HANDLER_H */ 9 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_module.c: -------------------------------------------------------------------------------- 1 | #include "ngx_http_rate_limit_module.h" 2 | #include "ngx_http_rate_limit_handler.h" 3 | #include "ngx_http_rate_limit_util.h" 4 | 5 | static ngx_int_t ngx_http_rate_limit_init(ngx_conf_t *cf); 6 | static void *ngx_http_rate_limit_create_loc_conf(ngx_conf_t *cf); 7 | static char *ngx_http_rate_limit_merge_loc_conf(ngx_conf_t *cf, void *parent, 8 | void *child); 9 | static char *ngx_http_rate_limit(ngx_conf_t *cf, ngx_command_t *cmd, 10 | void *conf); 11 | static char *ngx_http_rate_limit_pass(ngx_conf_t *cf, ngx_command_t *cmd, 12 | void *conf); 13 | 14 | static ngx_conf_enum_t ngx_http_rate_limit_log_levels[] = { 15 | { ngx_string("info"), NGX_LOG_INFO }, 16 | { ngx_string("notice"), NGX_LOG_NOTICE }, 17 | { ngx_string("warn"), NGX_LOG_WARN }, 18 | { ngx_string("error"), NGX_LOG_ERR }, 19 | { ngx_null_string, 0 } 20 | }; 21 | 22 | static ngx_conf_num_bounds_t ngx_http_rate_limit_status_bounds = { 23 | ngx_conf_check_num_bounds, 400, 599 24 | }; 25 | 26 | static ngx_command_t ngx_http_rate_limit_commands[] = { 27 | 28 | { ngx_string("rate_limit"), 29 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 30 | NGX_CONF_TAKE1234, 31 | ngx_http_rate_limit, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, 32 | 33 | { ngx_string("rate_limit_prefix"), 34 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 35 | NGX_CONF_TAKE1, 36 | ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, 37 | offsetof(ngx_http_rate_limit_loc_conf_t, prefix), NULL }, 38 | 39 | { ngx_string("rate_limit_quantity"), 40 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 41 | NGX_CONF_TAKE1, 42 | ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, 43 | offsetof(ngx_http_rate_limit_loc_conf_t, quantity), NULL }, 44 | 45 | { ngx_string("rate_limit_pass"), 46 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 47 | NGX_CONF_TAKE1, 48 | ngx_http_rate_limit_pass, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, 49 | 50 | { ngx_string("rate_limit_headers"), 51 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 52 | NGX_CONF_FLAG, 53 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 54 | offsetof(ngx_http_rate_limit_loc_conf_t, enable_headers), NULL }, 55 | 56 | { ngx_string("rate_limit_log_level"), 57 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 58 | NGX_CONF_TAKE1, 59 | ngx_conf_set_enum_slot, NGX_HTTP_LOC_CONF_OFFSET, 60 | offsetof(ngx_http_rate_limit_loc_conf_t, limit_log_level), 61 | &ngx_http_rate_limit_log_levels }, 62 | 63 | { ngx_string("rate_limit_status"), 64 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 65 | NGX_CONF_TAKE1, 66 | ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, 67 | offsetof(ngx_http_rate_limit_loc_conf_t, status_code), 68 | &ngx_http_rate_limit_status_bounds }, 69 | 70 | { ngx_string("rate_limit_buffer_size"), 71 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 72 | NGX_CONF_TAKE1, 73 | ngx_conf_set_size_slot, NGX_HTTP_LOC_CONF_OFFSET, 74 | offsetof(ngx_http_rate_limit_loc_conf_t, upstream.buffer_size), NULL }, 75 | 76 | { ngx_string("rate_limit_connect_timeout"), 77 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 78 | NGX_CONF_TAKE1, 79 | ngx_conf_set_msec_slot, NGX_HTTP_LOC_CONF_OFFSET, 80 | offsetof(ngx_http_rate_limit_loc_conf_t, upstream.connect_timeout), 81 | NULL }, 82 | 83 | { ngx_string("rate_limit_send_timeout"), 84 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 85 | NGX_CONF_TAKE1, 86 | ngx_conf_set_msec_slot, NGX_HTTP_LOC_CONF_OFFSET, 87 | offsetof(ngx_http_rate_limit_loc_conf_t, upstream.send_timeout), NULL }, 88 | 89 | { ngx_string("rate_limit_read_timeout"), 90 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | 91 | NGX_CONF_TAKE1, 92 | ngx_conf_set_msec_slot, NGX_HTTP_LOC_CONF_OFFSET, 93 | offsetof(ngx_http_rate_limit_loc_conf_t, upstream.read_timeout), NULL }, 94 | 95 | ngx_null_command 96 | }; 97 | 98 | static ngx_http_module_t ngx_http_rate_limit_module_ctx = { 99 | NULL, /* preconfiguration */ 100 | ngx_http_rate_limit_init, /* postconfiguration */ 101 | 102 | NULL, /* create main configuration */ 103 | NULL, /* init main configuration */ 104 | 105 | NULL, /* create server configuration */ 106 | NULL, /* merge server configuration */ 107 | 108 | ngx_http_rate_limit_create_loc_conf, /* create location configration */ 109 | ngx_http_rate_limit_merge_loc_conf /* merge location configration */ 110 | }; 111 | 112 | ngx_module_t ngx_http_rate_limit_module = { 113 | NGX_MODULE_V1, 114 | &ngx_http_rate_limit_module_ctx, /* module context */ 115 | ngx_http_rate_limit_commands, /* module directives */ 116 | NGX_HTTP_MODULE, /* module type */ 117 | NULL, /* init master */ 118 | NULL, /* init module */ 119 | NULL, /* init process */ 120 | NULL, /* init thread */ 121 | NULL, /* exit thread */ 122 | NULL, /* exit process */ 123 | NULL, /* exit master */ 124 | NGX_MODULE_V1_PADDING 125 | }; 126 | 127 | static void * 128 | ngx_http_rate_limit_create_loc_conf(ngx_conf_t *cf) 129 | { 130 | ngx_http_rate_limit_loc_conf_t *conf; 131 | 132 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_rate_limit_loc_conf_t)); 133 | if (conf == NULL) { 134 | return NULL; 135 | } 136 | 137 | /* 138 | * set by ngx_pcalloc(): 139 | * 140 | * conf->upstream.bufs.num = 0; 141 | * conf->upstream.next_upstream = 0; 142 | * conf->upstream.temp_path = NULL; 143 | * conf->upstream.uri = { 0, NULL }; 144 | * conf->upstream.location = NULL; 145 | * 146 | * conf->prefix = { 0, NULL }; 147 | */ 148 | 149 | conf->upstream.connect_timeout = NGX_CONF_UNSET_MSEC; 150 | conf->upstream.send_timeout = NGX_CONF_UNSET_MSEC; 151 | conf->upstream.read_timeout = NGX_CONF_UNSET_MSEC; 152 | 153 | conf->upstream.buffer_size = NGX_CONF_UNSET_SIZE; 154 | 155 | /* the hardcoded values */ 156 | conf->upstream.cyclic_temp_file = 0; 157 | conf->upstream.buffering = 0; 158 | conf->upstream.ignore_client_abort = 1; 159 | conf->upstream.send_lowat = 0; 160 | conf->upstream.bufs.num = 0; 161 | conf->upstream.busy_buffers_size = 0; 162 | conf->upstream.max_temp_file_size = 0; 163 | conf->upstream.temp_file_write_size = 0; 164 | conf->upstream.intercept_errors = 1; 165 | conf->upstream.intercept_404 = 1; 166 | conf->upstream.pass_request_headers = 0; 167 | conf->upstream.pass_request_body = 0; 168 | 169 | conf->enable_headers = NGX_CONF_UNSET; 170 | conf->status_code = NGX_CONF_UNSET_UINT; 171 | conf->limit_log_level = NGX_CONF_UNSET_UINT; 172 | 173 | conf->quantity = NGX_CONF_UNSET_UINT; 174 | 175 | return conf; 176 | } 177 | 178 | static char * 179 | ngx_http_rate_limit_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 180 | { 181 | ngx_http_rate_limit_loc_conf_t *prev = parent; 182 | ngx_http_rate_limit_loc_conf_t *conf = child; 183 | 184 | ngx_conf_merge_msec_value(conf->upstream.connect_timeout, 185 | prev->upstream.connect_timeout, 60000); 186 | 187 | ngx_conf_merge_msec_value(conf->upstream.send_timeout, 188 | prev->upstream.send_timeout, 60000); 189 | 190 | ngx_conf_merge_msec_value(conf->upstream.read_timeout, 191 | prev->upstream.read_timeout, 60000); 192 | 193 | ngx_conf_merge_size_value(conf->upstream.buffer_size, 194 | prev->upstream.buffer_size, 195 | (size_t) ngx_pagesize); 196 | 197 | if (conf->upstream.upstream == NULL) { 198 | conf->upstream.upstream = prev->upstream.upstream; 199 | } 200 | 201 | ngx_conf_merge_value(conf->enable_headers, prev->enable_headers, 0); 202 | ngx_conf_merge_uint_value(conf->status_code, prev->status_code, 203 | NGX_HTTP_TOO_MANY_REQUESTS); 204 | ngx_conf_merge_uint_value(conf->limit_log_level, prev->limit_log_level, 205 | NGX_LOG_ERR); 206 | 207 | ngx_conf_merge_str_value(conf->prefix, prev->prefix, ""); 208 | ngx_conf_merge_uint_value(conf->quantity, prev->quantity, 1); 209 | 210 | return NGX_CONF_OK; 211 | } 212 | 213 | static char * 214 | ngx_http_rate_limit(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 215 | { 216 | ngx_http_rate_limit_loc_conf_t *lrcf = conf; 217 | 218 | ngx_str_t *value, s; 219 | ngx_int_t requests, period, burst; 220 | ngx_uint_t i; 221 | ngx_http_compile_complex_value_t ccv; 222 | 223 | value = cf->args->elts; 224 | 225 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 226 | 227 | ccv.cf = cf; 228 | ccv.value = &value[1]; 229 | ccv.complex_value = &lrcf->key; 230 | 231 | if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { 232 | return NGX_CONF_ERROR; 233 | } 234 | 235 | requests = 1; 236 | period = 60; 237 | burst = 0; 238 | 239 | for (i = 2; i < cf->args->nelts; i++) { 240 | 241 | if (ngx_strncmp(value[i].data, "requests=", 9) == 0) { 242 | 243 | requests = ngx_atoi(value[i].data + 9, value[i].len - 9); 244 | if (requests <= 0) { 245 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 246 | "invalid requests value \"%V\"", &value[i]); 247 | return NGX_CONF_ERROR; 248 | } 249 | 250 | continue; 251 | } 252 | 253 | if (ngx_strncmp(value[i].data, "period=", 7) == 0) { 254 | 255 | s.len = value[i].len - 7; 256 | s.data = value[i].data + 7; 257 | 258 | period = ngx_parse_time(&s, 1); 259 | if (period <= 0) { 260 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 261 | "invalid period time \"%V\"", &value[i]); 262 | return NGX_CONF_ERROR; 263 | } 264 | 265 | continue; 266 | } 267 | 268 | if (ngx_strncmp(value[i].data, "burst=", 6) == 0) { 269 | 270 | burst = ngx_atoi(value[i].data + 6, value[i].len - 6); 271 | if (burst < 0) { 272 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 273 | "invalid burst value \"%V\"", &value[i]); 274 | return NGX_CONF_ERROR; 275 | } 276 | 277 | continue; 278 | } 279 | 280 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"", 281 | &value[i]); 282 | return NGX_CONF_ERROR; 283 | } 284 | 285 | lrcf->requests = requests; 286 | lrcf->period = period; 287 | lrcf->burst = burst; 288 | 289 | return NGX_CONF_OK; 290 | } 291 | 292 | static char * 293 | ngx_http_rate_limit_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 294 | { 295 | ngx_http_rate_limit_loc_conf_t *rlcf = conf; 296 | 297 | ngx_str_t *value; 298 | ngx_uint_t n; 299 | ngx_url_t url; 300 | 301 | ngx_http_compile_complex_value_t ccv; 302 | 303 | if (rlcf->upstream.upstream) { 304 | return "is duplicate"; 305 | } 306 | 307 | value = cf->args->elts; 308 | 309 | n = ngx_http_script_variables_count(&value[1]); 310 | if (n) { 311 | rlcf->complex_target = 312 | ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); 313 | 314 | if (rlcf->complex_target == NULL) { 315 | return NGX_CONF_ERROR; 316 | } 317 | 318 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 319 | ccv.cf = cf; 320 | ccv.value = &value[1]; 321 | ccv.complex_value = rlcf->complex_target; 322 | 323 | if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { 324 | return NGX_CONF_ERROR; 325 | } 326 | 327 | rlcf->configured = 1; 328 | 329 | return NGX_CONF_OK; 330 | } 331 | 332 | rlcf->complex_target = NULL; 333 | 334 | ngx_memzero(&url, sizeof(ngx_url_t)); 335 | 336 | url.url = value[1]; 337 | url.no_resolve = 1; 338 | 339 | rlcf->upstream.upstream = ngx_http_upstream_add(cf, &url, 0); 340 | if (rlcf->upstream.upstream == NULL) { 341 | return NGX_CONF_ERROR; 342 | } 343 | 344 | rlcf->configured = 1; 345 | 346 | return NGX_CONF_OK; 347 | } 348 | 349 | static ngx_int_t 350 | ngx_http_rate_limit_init(ngx_conf_t *cf) 351 | { 352 | ngx_http_handler_pt *h; 353 | ngx_http_core_main_conf_t *cmcf; 354 | 355 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 356 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers); 357 | 358 | if (h == NULL) { 359 | return NGX_ERROR; 360 | } 361 | 362 | *h = ngx_http_rate_limit_handler; 363 | 364 | return NGX_OK; 365 | } 366 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_module.h: -------------------------------------------------------------------------------- 1 | #ifndef NGX_HTTP_RATE_LIMIT_MODULE_H 2 | #define NGX_HTTP_RATE_LIMIT_MODULE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | extern ngx_module_t ngx_http_rate_limit_module; 9 | 10 | typedef struct { 11 | ngx_flag_t configured; 12 | ngx_http_complex_value_t key; 13 | 14 | ngx_http_upstream_conf_t upstream; 15 | ngx_http_complex_value_t *complex_target; /* for rate_limit_pass */ 16 | 17 | ngx_flag_t enable_headers; 18 | ngx_uint_t status_code; 19 | ngx_uint_t limit_log_level; 20 | 21 | ngx_str_t prefix; 22 | ngx_uint_t requests; 23 | ngx_uint_t period; 24 | ngx_uint_t burst; 25 | ngx_uint_t quantity; 26 | } ngx_http_rate_limit_loc_conf_t; 27 | 28 | typedef struct { 29 | ngx_str_t key; 30 | 31 | ngx_http_request_t *request; 32 | 33 | /* used to parse the redis response */ 34 | ngx_uint_t state; 35 | 36 | /* flag indicating whether the rate limit has been finalized */ 37 | ngx_flag_t finalized; 38 | 39 | /* parsed variables from the redis response */ 40 | ngx_uint_t limit; 41 | ngx_uint_t remaining; 42 | ngx_uint_t reset; 43 | ngx_int_t retry_after; 44 | } ngx_http_rate_limit_ctx_t; 45 | 46 | #endif /* NGX_HTTP_RATE_LIMIT_MODULE_H */ 47 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_reply.c: -------------------------------------------------------------------------------- 1 | #include "ngx_http_rate_limit_reply.h" 2 | 3 | ngx_int_t 4 | ngx_http_rate_limit_process_reply(ngx_http_rate_limit_ctx_t *ctx, ssize_t bytes) 5 | { 6 | ngx_buf_t *b; 7 | ngx_http_upstream_t *u; 8 | u_char ch, *p; 9 | 10 | u = ctx->request->upstream; 11 | b = &u->buffer; 12 | 13 | enum { 14 | sw_start = 0, 15 | sw_CRLF1, 16 | sw_ARG1, 17 | sw_CRLF2, 18 | sw_ARG2, 19 | sw_LF1, 20 | sw_ARG3, 21 | sw_LF2, 22 | sw_ARG4, 23 | sw_ALLOWED, 24 | sw_LF3, 25 | sw_ARG5, 26 | sw_almost_done 27 | } state; 28 | 29 | state = ctx->state; 30 | 31 | b->pos = b->last; 32 | b->last += bytes; 33 | 34 | /* Example response: 35 | * "5\r\n:0\r\n:16\r\n:15\r\n:-1\r\n:2\r\n" 36 | * Note: the first multi bulk reply byte (`*`) is 37 | * checked within `u->process_header`. 38 | */ 39 | 40 | for (p = b->pos; p < b->last; p++) { 41 | ch = *p; 42 | 43 | switch (state) { 44 | 45 | case sw_start: 46 | /* our bulk length must always be 5 */ 47 | switch (ch) { 48 | case '5': 49 | state = sw_CRLF1; 50 | break; 51 | default: 52 | return NGX_ERROR; 53 | } 54 | break; 55 | 56 | case sw_CRLF1: 57 | switch (ch) { 58 | case CR: 59 | break; 60 | case LF: 61 | state = sw_ARG1; 62 | break; 63 | default: 64 | return NGX_ERROR; 65 | } 66 | break; 67 | 68 | case sw_ARG1: 69 | /* 0 indicates the action is allowed 70 | * 1 indicates that the action was limited/blocked */ 71 | switch (ch) { 72 | case ':': 73 | break; 74 | case '0': 75 | u->state->status = NGX_HTTP_OK; 76 | state = sw_CRLF2; 77 | break; 78 | case '1': 79 | u->state->status = NGX_HTTP_TOO_MANY_REQUESTS; 80 | state = sw_CRLF2; 81 | break; 82 | default: 83 | return NGX_ERROR; 84 | } 85 | break; 86 | 87 | case sw_CRLF2: 88 | switch (ch) { 89 | case CR: 90 | break; 91 | case LF: 92 | state = sw_ARG2; 93 | break; 94 | default: 95 | return NGX_ERROR; 96 | } 97 | break; 98 | 99 | case sw_ARG2: 100 | /* X-RateLimit-Limit HTTP header */ 101 | if (ch == ':') { 102 | break; 103 | } 104 | 105 | if (ch == CR) { 106 | state = sw_LF1; 107 | break; 108 | } 109 | 110 | if (ch < '0' || ch > '9') { 111 | return NGX_ERROR; 112 | } 113 | 114 | ctx->limit = ctx->limit * 10 + (ch - '0'); 115 | 116 | break; 117 | 118 | case sw_LF1: 119 | switch (ch) { 120 | case LF: 121 | state = sw_ARG3; 122 | break; 123 | default: 124 | return NGX_ERROR; 125 | } 126 | break; 127 | 128 | case sw_ARG3: 129 | /* X-RateLimit-Remaining HTTP header */ 130 | if (ch == ':') { 131 | break; 132 | } 133 | 134 | if (ch == CR) { 135 | state = sw_LF2; 136 | break; 137 | } 138 | 139 | if (ch < '0' || ch > '9') { 140 | return NGX_ERROR; 141 | } 142 | 143 | ctx->remaining = ctx->remaining * 10 + (ch - '0'); 144 | 145 | break; 146 | 147 | case sw_LF2: 148 | switch (ch) { 149 | case LF: 150 | state = sw_ARG4; 151 | break; 152 | default: 153 | return NGX_ERROR; 154 | } 155 | break; 156 | 157 | case sw_ARG4: 158 | /* The number of seconds until the user should retry, 159 | * and always -1 if the action was allowed. */ 160 | if (ch == ':') { 161 | break; 162 | } 163 | 164 | if (ch == '-') { 165 | state = sw_ALLOWED; 166 | break; 167 | } 168 | 169 | if (ch == CR) { 170 | state = sw_LF3; 171 | break; 172 | } 173 | 174 | if (ch < '0' || ch > '9') { 175 | return NGX_ERROR; 176 | } 177 | 178 | ctx->retry_after = ctx->retry_after * 10 + (ch - '0'); 179 | 180 | break; 181 | 182 | case sw_ALLOWED: 183 | switch (ch) { 184 | case '1': 185 | ctx->retry_after = -1; 186 | break; 187 | case CR: 188 | state = sw_LF3; 189 | break; 190 | default: 191 | return NGX_ERROR; 192 | } 193 | break; 194 | 195 | case sw_LF3: 196 | switch (ch) { 197 | case LF: 198 | state = sw_ARG5; 199 | break; 200 | default: 201 | return NGX_ERROR; 202 | } 203 | break; 204 | 205 | case sw_ARG5: 206 | /* X-RateLimit-Reset HTTP header */ 207 | if (ch == ':') { 208 | break; 209 | } 210 | 211 | if (ch == CR) { 212 | state = sw_almost_done; 213 | break; 214 | } 215 | 216 | if (ch < '0' || ch > '9') { 217 | return NGX_ERROR; 218 | } 219 | 220 | ctx->reset = ctx->reset * 10 + (ch - '0'); 221 | 222 | break; 223 | 224 | case sw_almost_done: 225 | /* End of redis response */ 226 | switch (ch) { 227 | case LF: 228 | goto done; 229 | default: 230 | return NGX_ERROR; 231 | } 232 | } 233 | } 234 | 235 | b->pos = p; 236 | ctx->state = state; 237 | 238 | return NGX_AGAIN; 239 | 240 | done: 241 | 242 | b->pos = p + 1; 243 | 244 | u->keepalive = 1; 245 | u->length = 0; 246 | 247 | return NGX_OK; 248 | } 249 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_reply.h: -------------------------------------------------------------------------------- 1 | #ifndef NGX_HTTP_RATE_LIMIT_REPLY_H 2 | #define NGX_HTTP_RATE_LIMIT_REPLY_H 3 | 4 | #include "ngx_http_rate_limit_module.h" 5 | 6 | ngx_int_t ngx_http_rate_limit_process_reply(ngx_http_rate_limit_ctx_t *ctx, 7 | ssize_t bytes); 8 | 9 | #endif /* NGX_HTTP_RATE_LIMIT_REPLY_H */ 10 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_upstream.c: -------------------------------------------------------------------------------- 1 | #include "ngx_http_rate_limit_upstream.h" 2 | 3 | /* Reference: ngx_http_upstream_finalize_request */ 4 | void 5 | ngx_http_rate_limit_finalize_upstream_request(ngx_http_request_t *r, 6 | ngx_http_upstream_t *u, 7 | ngx_int_t rc) 8 | { 9 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 10 | "finalize http rate limit request: %i", rc); 11 | 12 | if (u->cleanup == NULL) { 13 | /* the request was already finalized */ 14 | ngx_http_finalize_request(r, NGX_DONE); 15 | return; 16 | } 17 | 18 | *u->cleanup = NULL; 19 | u->cleanup = NULL; 20 | 21 | if (u->resolved && u->resolved->ctx) { 22 | ngx_resolve_name_done(u->resolved->ctx); 23 | u->resolved->ctx = NULL; 24 | } 25 | 26 | if (u->state && u->state->response_time == (ngx_msec_t) -1) { 27 | u->state->response_time = ngx_current_msec - u->start_time; 28 | 29 | if (u->pipe && u->pipe->read_length) { 30 | u->state->bytes_received += 31 | u->pipe->read_length - u->pipe->preread_size; 32 | u->state->response_length = u->pipe->read_length; 33 | } 34 | 35 | if (u->peer.connection) { 36 | u->state->bytes_sent = u->peer.connection->sent; 37 | } 38 | } 39 | 40 | u->finalize_request(r, rc); 41 | 42 | if (u->peer.free && u->peer.sockaddr) { 43 | u->peer.free(&u->peer, u->peer.data, 0); 44 | u->peer.sockaddr = NULL; 45 | } 46 | 47 | if (u->peer.connection) { 48 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 49 | "close redis connection: %d", u->peer.connection->fd); 50 | 51 | if (u->peer.connection->pool) { 52 | ngx_destroy_pool(u->peer.connection->pool); 53 | } 54 | 55 | ngx_close_connection(u->peer.connection); 56 | } 57 | 58 | u->peer.connection = NULL; 59 | 60 | r->read_event_handler = ngx_http_block_reading; 61 | r->write_event_handler = ngx_http_core_run_phases; 62 | 63 | if (rc == NGX_DECLINED) { 64 | return; 65 | } 66 | 67 | r->connection->log->action = "sending to client"; 68 | 69 | ngx_http_finalize_request(r, rc); 70 | 71 | ngx_http_core_run_phases(r); 72 | } 73 | 74 | /* Reference: ngx_http_upstream_test_connect */ 75 | static ngx_int_t 76 | ngx_http_rate_limit_test_connect(ngx_connection_t *c) 77 | { 78 | int err; 79 | socklen_t len; 80 | 81 | #if (NGX_HAVE_KQUEUE) 82 | 83 | if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) { 84 | if (c->write->pending_eof || c->read->pending_eof) { 85 | if (c->write->pending_eof) { 86 | err = c->write->kq_errno; 87 | 88 | } else { 89 | err = c->read->kq_errno; 90 | } 91 | 92 | c->log->action = "connecting to redis"; 93 | (void) ngx_connection_error( 94 | c, err, "kevent() reported that connect() failed"); 95 | return NGX_ERROR; 96 | } 97 | 98 | } else 99 | #endif 100 | { 101 | err = 0; 102 | len = sizeof(int); 103 | 104 | /* 105 | * BSDs and Linux return 0 and set a pending error in err 106 | * Solaris returns -1 and sets errno 107 | */ 108 | 109 | if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, (void *) &err, &len) == 110 | -1) { 111 | err = ngx_socket_errno; 112 | } 113 | 114 | if (err) { 115 | c->log->action = "connecting to redis"; 116 | (void) ngx_connection_error(c, err, "connect() failed"); 117 | return NGX_ERROR; 118 | } 119 | } 120 | 121 | return NGX_OK; 122 | } 123 | 124 | /* Reference: ngx_http_upstream_process_non_buffered_request */ 125 | static void 126 | ngx_http_rate_limit_process_redis_response(ngx_http_request_t *r, 127 | ngx_uint_t do_write) 128 | { 129 | size_t size; 130 | ssize_t n; 131 | ngx_buf_t *b; 132 | ngx_connection_t *upstream; 133 | ngx_http_upstream_t *u; 134 | 135 | u = r->upstream; 136 | upstream = u->peer.connection; 137 | 138 | b = &u->buffer; 139 | 140 | do_write = do_write || u->length == 0; 141 | 142 | for (;;) { 143 | 144 | if (do_write) { 145 | 146 | if (u->out_bufs || u->busy_bufs) { 147 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_DONE); 148 | } 149 | 150 | if (u->busy_bufs == NULL) { 151 | 152 | if (u->length == 0 || 153 | (upstream->read->eof && u->length == -1)) { 154 | ngx_http_rate_limit_finalize_upstream_request(r, u, 0); 155 | return; 156 | } 157 | 158 | if (upstream->read->eof) { 159 | ngx_log_error(NGX_LOG_ERR, upstream->log, 0, 160 | "redis prematurely closed connection"); 161 | 162 | ngx_http_rate_limit_finalize_upstream_request( 163 | r, u, NGX_HTTP_BAD_GATEWAY); 164 | return; 165 | } 166 | 167 | if (upstream->read->error) { 168 | ngx_http_rate_limit_finalize_upstream_request( 169 | r, u, NGX_HTTP_BAD_GATEWAY); 170 | return; 171 | } 172 | 173 | b->pos = b->start; 174 | b->last = b->start; 175 | } 176 | } 177 | 178 | size = b->end - b->last; 179 | 180 | if (size && upstream->read->ready) { 181 | 182 | n = upstream->recv(upstream, b->last, size); 183 | 184 | if (n == NGX_AGAIN) { 185 | break; 186 | } 187 | 188 | if (n > 0) { 189 | u->state->bytes_received += n; 190 | u->state->response_length += n; 191 | 192 | if (u->input_filter(u->input_filter_ctx, n) == NGX_ERROR) { 193 | ngx_http_rate_limit_finalize_upstream_request(r, u, 194 | NGX_ERROR); 195 | return; 196 | } 197 | } 198 | 199 | do_write = 1; 200 | 201 | continue; 202 | } 203 | 204 | break; 205 | } 206 | 207 | if (ngx_handle_read_event(upstream->read, 0) != NGX_OK) { 208 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_ERROR); 209 | return; 210 | } 211 | 212 | if (upstream->read->active && !upstream->read->ready) { 213 | ngx_add_timer(upstream->read, u->conf->read_timeout); 214 | 215 | } else if (upstream->read->timer_set) { 216 | ngx_del_timer(upstream->read); 217 | } 218 | } 219 | 220 | /* Reference: ngx_http_upstream_process_non_buffered_upstream */ 221 | static void 222 | ngx_http_rate_limit_redis_rev_handler(ngx_http_request_t *r, 223 | ngx_http_upstream_t *u) 224 | { 225 | ngx_connection_t *c; 226 | 227 | c = u->peer.connection; 228 | 229 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, 230 | "http rate limit process redis response"); 231 | 232 | c->log->action = "reading from redis"; 233 | 234 | if (c->read->timedout) { 235 | ngx_connection_error(c, NGX_ETIMEDOUT, "redis timed out"); 236 | ngx_http_rate_limit_finalize_upstream_request( 237 | r, u, NGX_HTTP_GATEWAY_TIME_OUT); 238 | return; 239 | } 240 | 241 | ngx_http_rate_limit_process_redis_response(r, 0); 242 | } 243 | 244 | /* Reference: ngx_http_upstream_dummy_handler */ 245 | static void 246 | ngx_http_rate_limit_dummy_handler(ngx_http_request_t *r, ngx_http_upstream_t *u) 247 | { 248 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 249 | "rate_limit: ngx_http_rate_limit_dummy_handler should not" 250 | " be called by the upstream"); 251 | } 252 | 253 | /* Reference: ngx_http_upstream_send_response */ 254 | static void 255 | ngx_http_rate_limit_process_response(ngx_http_request_t *r, 256 | ngx_http_upstream_t *u) 257 | { 258 | ssize_t n; 259 | ngx_connection_t *c; 260 | ngx_http_core_loc_conf_t *clcf; 261 | 262 | /* Not necessary, we don't send anything to the client */ 263 | /*u->header_sent = 1;*/ 264 | 265 | c = r->connection; 266 | 267 | clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 268 | 269 | /* We are always processing a non buffered response */ 270 | /*if (!u->buffering) {*/ 271 | 272 | /* Input filter is always set */ 273 | /*if (u->input_filter == NULL) { 274 | u->input_filter_init = ngx_http_upstream_non_buffered_filter_init; 275 | u->input_filter = ngx_http_upstream_non_buffered_filter; 276 | u->input_filter_ctx = r; 277 | }*/ 278 | 279 | u->read_event_handler = ngx_http_rate_limit_redis_rev_handler; 280 | 281 | /* Set write_event_handler to the dummy handler 282 | * to make sure we don't send anything */ 283 | u->write_event_handler = ngx_http_rate_limit_dummy_handler; 284 | 285 | /* Not needed */ 286 | /*r->limit_rate = 0; 287 | r->limit_rate_set = 1;*/ 288 | 289 | if (u->input_filter_init(u->input_filter_ctx) == NGX_ERROR) { 290 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_ERROR); 291 | return; 292 | } 293 | 294 | if (clcf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK) { 295 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_ERROR); 296 | return; 297 | } 298 | 299 | n = u->buffer.last - u->buffer.pos; 300 | 301 | if (n) { 302 | u->buffer.last = u->buffer.pos; 303 | 304 | u->state->response_length += n; 305 | 306 | if (u->input_filter(u->input_filter_ctx, n) == NGX_ERROR) { 307 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_ERROR); 308 | return; 309 | } 310 | 311 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_DONE); 312 | } else { 313 | u->buffer.pos = u->buffer.start; 314 | u->buffer.last = u->buffer.start; 315 | 316 | if (ngx_http_send_special(r, NGX_HTTP_FLUSH) == NGX_ERROR) { 317 | ngx_http_rate_limit_finalize_upstream_request(r, u, NGX_ERROR); 318 | return; 319 | } 320 | 321 | ngx_http_rate_limit_redis_rev_handler(r, u); 322 | } 323 | } 324 | 325 | /* Reference: ngx_http_upstream_process_header */ 326 | void 327 | ngx_http_rate_limit_rev_handler(ngx_http_request_t *r, ngx_http_upstream_t *u) 328 | { 329 | ssize_t n; 330 | ngx_int_t rc; 331 | ngx_connection_t *c; 332 | 333 | c = u->peer.connection; 334 | 335 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, 336 | "http rate limit rev handler"); 337 | 338 | c->log->action = "reading response header from redis"; 339 | 340 | if (c->read->timedout) { 341 | /*ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);*/ 342 | ngx_http_rate_limit_finalize_upstream_request( 343 | r, u, NGX_HTTP_GATEWAY_TIME_OUT); 344 | return; 345 | } 346 | 347 | if (!u->request_sent && ngx_http_rate_limit_test_connect(c) != NGX_OK) { 348 | /* Ensure u->reinit_request always gets called for upstream_next */ 349 | /*u->request_sent = 1; 350 | 351 | ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);*/ 352 | ngx_http_rate_limit_finalize_upstream_request( 353 | r, u, NGX_HTTP_SERVICE_UNAVAILABLE); 354 | return; 355 | } 356 | 357 | if (u->buffer.start == NULL) { 358 | u->buffer.start = ngx_palloc(r->pool, u->conf->buffer_size); 359 | if (u->buffer.start == NULL) { 360 | ngx_http_rate_limit_finalize_upstream_request( 361 | r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); 362 | return; 363 | } 364 | 365 | u->buffer.pos = u->buffer.start; 366 | u->buffer.last = u->buffer.start; 367 | u->buffer.end = u->buffer.start + u->conf->buffer_size; 368 | u->buffer.temporary = 1; 369 | 370 | u->buffer.tag = u->output.tag; 371 | 372 | /* No need to init u->headers_in.headers and u->headers_in.trailers */ 373 | } 374 | 375 | for (;;) { 376 | 377 | n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last); 378 | 379 | if (n == NGX_AGAIN) { 380 | if (ngx_handle_read_event(c->read, 0) != NGX_OK) { 381 | ngx_http_rate_limit_finalize_upstream_request( 382 | r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); 383 | 384 | return; 385 | } 386 | 387 | return; 388 | } 389 | 390 | if (n == 0) { 391 | ngx_log_error(NGX_LOG_ERR, c->log, 0, 392 | "redis prematurely closed connection"); 393 | } 394 | 395 | if (n == NGX_ERROR || n == 0) { 396 | /*ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);*/ 397 | ngx_http_rate_limit_finalize_upstream_request( 398 | r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); 399 | return; 400 | } 401 | 402 | u->state->bytes_received += n; 403 | 404 | u->buffer.last += n; 405 | 406 | rc = u->process_header(r); 407 | 408 | if (rc == NGX_AGAIN) { 409 | 410 | if (u->buffer.last == u->buffer.end) { 411 | ngx_log_error(NGX_LOG_ERR, c->log, 0, 412 | "redis sent too big header"); 413 | 414 | /*ngx_http_upstream_next(r, u, 415 | NGX_HTTP_UPSTREAM_FT_INVALID_HEADER);*/ 416 | 417 | ngx_http_rate_limit_finalize_upstream_request( 418 | r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); 419 | return; 420 | } 421 | 422 | continue; 423 | } 424 | 425 | break; 426 | } 427 | 428 | if (rc == NGX_HTTP_UPSTREAM_INVALID_HEADER) { 429 | /*ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_INVALID_HEADER);*/ 430 | ngx_http_rate_limit_finalize_upstream_request( 431 | r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); 432 | return; 433 | } 434 | 435 | if (rc == NGX_ERROR) { 436 | ngx_http_rate_limit_finalize_upstream_request( 437 | r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); 438 | return; 439 | } 440 | 441 | /* rc == NGX_OK */ 442 | 443 | u->state->header_time = ngx_current_msec - u->start_time; 444 | 445 | u->length = -1; 446 | 447 | ngx_http_rate_limit_process_response(r, u); 448 | } 449 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_upstream.h: -------------------------------------------------------------------------------- 1 | #ifndef NGX_HTTP_RATE_LIMIT_UPSTREAM_H 2 | #define NGX_HTTP_RATE_LIMIT_UPSTREAM_H 3 | 4 | #include 5 | 6 | void ngx_http_rate_limit_rev_handler(ngx_http_request_t *r, 7 | ngx_http_upstream_t *u); 8 | 9 | #endif /* NGX_HTTP_RATE_LIMIT_UPSTREAM_H */ 10 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_util.c: -------------------------------------------------------------------------------- 1 | #include "ngx_http_rate_limit_util.h" 2 | 3 | static size_t ngx_get_num_size(uint64_t i); 4 | 5 | ngx_http_upstream_srv_conf_t * 6 | ngx_http_rate_limit_upstream_add(ngx_http_request_t *r, ngx_url_t *url) 7 | { 8 | ngx_http_upstream_main_conf_t *umcf; 9 | ngx_http_upstream_srv_conf_t **uscfp; 10 | ngx_uint_t i; 11 | 12 | umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module); 13 | 14 | uscfp = umcf->upstreams.elts; 15 | 16 | for (i = 0; i < umcf->upstreams.nelts; i++) { 17 | 18 | if (uscfp[i]->host.len != url->host.len || 19 | ngx_strncasecmp(uscfp[i]->host.data, url->host.data, 20 | url->host.len) != 0) { 21 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 22 | "upstream_add: host not match"); 23 | continue; 24 | } 25 | 26 | if (uscfp[i]->port != url->port) { 27 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 28 | "upstream_add: port not match: %d != %d", 29 | (int) uscfp[i]->port, (int) url->port); 30 | continue; 31 | } 32 | 33 | #if defined(nginx_version) && nginx_version < 1011006 34 | if (uscfp[i]->default_port && url->default_port && 35 | uscfp[i]->default_port != url->default_port) { 36 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 37 | "upstream_add: default_port not match"); 38 | continue; 39 | } 40 | #endif 41 | 42 | return uscfp[i]; 43 | } 44 | 45 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 46 | "no upstream found: %V", &url->host); 47 | 48 | return NULL; 49 | } 50 | 51 | static size_t 52 | ngx_get_num_size(uint64_t i) 53 | { 54 | size_t n = 0; 55 | 56 | do { 57 | i = i / 10; 58 | n++; 59 | } while (i > 0); 60 | 61 | return n; 62 | } 63 | 64 | ngx_int_t 65 | ngx_http_rate_limit_build_command(ngx_http_request_t *r, ngx_buf_t **b) 66 | { 67 | size_t len, arg_len; 68 | u_char *p; 69 | ngx_http_rate_limit_ctx_t *ctx; 70 | ngx_http_rate_limit_loc_conf_t *rlcf; 71 | 72 | rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rate_limit_module); 73 | 74 | ctx = ngx_http_get_module_ctx(r, ngx_http_rate_limit_module); 75 | if (ctx == NULL) { 76 | return NGX_ERROR; 77 | } 78 | 79 | /* Accumulate buffer size. */ 80 | len = 0; 81 | 82 | /* Example command: 83 | * "*5\r\n$11\r\nRATER.LIMIT\r\n$7\r\nuser123\r\n$2\r\n15\r\n$2\r\n30\r\n$2\r\n60\r\n" 84 | */ 85 | 86 | /*The arity of the command */ 87 | len += sizeof("*6") - 1; 88 | len += sizeof("\r\n") - 1; 89 | 90 | /* The length of the first argument in bytes */ 91 | len += sizeof("$11") - 1; 92 | len += sizeof("\r\n") - 1; 93 | 94 | /* Command name */ 95 | len += sizeof("RATER.LIMIT") - 1; 96 | len += sizeof("\r\n") - 1; 97 | 98 | /* */ 99 | len += sizeof("$") - 1; 100 | len += ngx_get_num_size(ctx->key.len); 101 | len += sizeof("\r\n") - 1; 102 | len += ctx->key.len; 103 | len += sizeof("\r\n") - 1; 104 | 105 | /* */ 106 | arg_len = ngx_get_num_size(rlcf->burst); 107 | len += sizeof("$") - 1; 108 | len += ngx_get_num_size(arg_len); 109 | len += sizeof("\r\n") - 1; 110 | len += arg_len; 111 | len += sizeof("\r\n") - 1; 112 | 113 | /* */ 114 | arg_len = ngx_get_num_size(rlcf->requests); 115 | len += sizeof("$") - 1; 116 | len += ngx_get_num_size(arg_len); 117 | len += sizeof("\r\n") - 1; 118 | len += arg_len; 119 | len += sizeof("\r\n") - 1; 120 | 121 | /* */ 122 | arg_len = ngx_get_num_size(rlcf->period); 123 | len += sizeof("$") - 1; 124 | len += ngx_get_num_size(arg_len); 125 | len += sizeof("\r\n") - 1; 126 | len += arg_len; 127 | len += sizeof("\r\n") - 1; 128 | 129 | /* [] */ 130 | if (rlcf->quantity != 1) { 131 | arg_len = ngx_get_num_size(rlcf->quantity); 132 | len += sizeof("$") - 1; 133 | len += ngx_get_num_size(arg_len); 134 | len += sizeof("\r\n") - 1; 135 | len += arg_len; 136 | len += sizeof("\r\n") - 1; 137 | } 138 | 139 | *b = ngx_create_temp_buf(r->pool, len); 140 | if (*b == NULL) { 141 | return NGX_ERROR; 142 | } 143 | 144 | p = (*b)->last; 145 | 146 | *p++ = '*'; 147 | *p++ = rlcf->quantity != 1 ? '6' : '5'; 148 | *p++ = '\r'; 149 | *p++ = '\n'; 150 | 151 | *p++ = '$'; 152 | *p++ = '1'; 153 | *p++ = '1'; 154 | *p++ = '\r'; 155 | *p++ = '\n'; 156 | p = ngx_cpymem(p, "RATER.LIMIT", sizeof("RATER.LIMIT") - 1); 157 | *p++ = '\r'; 158 | *p++ = '\n'; 159 | 160 | *p++ = '$'; 161 | p = ngx_sprintf(p, "%uz", ctx->key.len); 162 | *p++ = '\r'; 163 | *p++ = '\n'; 164 | p = ngx_copy(p, ctx->key.data, ctx->key.len); 165 | *p++ = '\r'; 166 | *p++ = '\n'; 167 | 168 | *p++ = '$'; 169 | p = ngx_sprintf(p, "%uz", ngx_get_num_size(rlcf->burst)); 170 | *p++ = '\r'; 171 | *p++ = '\n'; 172 | p = ngx_sprintf(p, "%d", rlcf->burst); 173 | *p++ = '\r'; 174 | *p++ = '\n'; 175 | 176 | *p++ = '$'; 177 | p = ngx_sprintf(p, "%uz", ngx_get_num_size(rlcf->requests)); 178 | *p++ = '\r'; 179 | *p++ = '\n'; 180 | p = ngx_sprintf(p, "%d", rlcf->requests); 181 | *p++ = '\r'; 182 | *p++ = '\n'; 183 | 184 | *p++ = '$'; 185 | p = ngx_sprintf(p, "%uz", ngx_get_num_size(rlcf->period)); 186 | *p++ = '\r'; 187 | *p++ = '\n'; 188 | p = ngx_sprintf(p, "%d", rlcf->period); 189 | *p++ = '\r'; 190 | *p++ = '\n'; 191 | 192 | if (rlcf->quantity != 1) { 193 | *p++ = '$'; 194 | p = ngx_sprintf(p, "%uz", ngx_get_num_size(rlcf->quantity)); 195 | *p++ = '\r'; 196 | *p++ = '\n'; 197 | p = ngx_sprintf(p, "%d", rlcf->quantity); 198 | *p++ = '\r'; 199 | *p++ = '\n'; 200 | } 201 | 202 | if (p - (*b)->pos != (ssize_t) len) { 203 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 204 | "rate limit: buffer error %uz != %uz", 205 | (size_t)(p - (*b)->pos), len); 206 | 207 | return NGX_ERROR; 208 | } 209 | 210 | (*b)->last = p; 211 | 212 | return NGX_OK; 213 | } 214 | 215 | ngx_int_t 216 | ngx_set_custom_header(ngx_http_request_t *r, ngx_str_t *key, ngx_uint_t value) 217 | { 218 | ngx_table_elt_t *h; 219 | 220 | h = ngx_list_push(&r->headers_out.headers); 221 | if (h == NULL) { 222 | return NGX_ERROR; 223 | } 224 | 225 | /* Mark the header as not deleted. */ 226 | h->hash = 1; 227 | h->key = *key; 228 | 229 | h->value.data = ngx_pnalloc(r->pool, ngx_get_num_size(value)); 230 | if (h->value.data == NULL) { 231 | h->hash = 0; 232 | return NGX_ERROR; 233 | } 234 | 235 | h->value.len = ngx_sprintf(h->value.data, "%ui", value) - h->value.data; 236 | 237 | h->lowcase_key = ngx_pnalloc(r->pool, h->key.len); 238 | if (h->lowcase_key == NULL) { 239 | return NGX_ERROR; 240 | } 241 | 242 | ngx_strlow(h->lowcase_key, h->key.data, h->key.len); 243 | 244 | return NGX_OK; 245 | } 246 | -------------------------------------------------------------------------------- /src/ngx_http_rate_limit_util.h: -------------------------------------------------------------------------------- 1 | #ifndef NGX_HTTP_RATE_LIMIT_UTIL_H 2 | #define NGX_HTTP_RATE_LIMIT_UTIL_H 3 | 4 | #include "ngx_http_rate_limit_module.h" 5 | 6 | ngx_http_upstream_srv_conf_t *ngx_http_rate_limit_upstream_add( 7 | ngx_http_request_t *r, ngx_url_t *url); 8 | ngx_int_t ngx_http_rate_limit_build_command(ngx_http_request_t *r, 9 | ngx_buf_t **b); 10 | ngx_int_t ngx_set_custom_header(ngx_http_request_t *r, ngx_str_t *key, 11 | ngx_uint_t value); 12 | 13 | #endif /* NGX_HTTP_RATE_LIMIT_UTIL_H */ 14 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use Test::Nginx::Socket; 4 | 5 | plan tests => repeat_each() * (blocks() * 10 - 2); 6 | 7 | $ENV{TEST_NGINX_REDIS_PORT} ||= 6379; 8 | 9 | our $HttpConfig = qq{ 10 | upstream redis { 11 | server 127.0.0.1:$ENV{TEST_NGINX_REDIS_PORT}; 12 | 13 | # a pool with at most 1024 connections 14 | keepalive 1024; 15 | } 16 | 17 | error_log logs/error.log debug; 18 | }; 19 | 20 | no_long_string(); 21 | #no_diff(); 22 | 23 | run_tests(); 24 | 25 | __DATA__ 26 | 27 | === TEST 1: headers 28 | --- http_config eval: $::HttpConfig 29 | --- config 30 | location /quota { 31 | rate_limit $remote_addr requests=700 period=3m burst=699; 32 | rate_limit_quantity 0; 33 | rate_limit_pass redis; 34 | rate_limit_headers on; 35 | 36 | error_page 404 =200 @quota; 37 | } 38 | 39 | location @quota { 40 | default_type application/json; 41 | return 200 '{"X-RateLimit-Limit":$sent_http_x_ratelimit_limit, "X-RateLimit-Remaining":$sent_http_x_ratelimit_remaining, "X-RateLimit-Reset":$sent_http_x_ratelimit_reset}'; 42 | } 43 | --- request 44 | GET /quota 45 | --- response_headers 46 | X-RateLimit-Limit: 700 47 | X-RateLimit-Remaining: 700 48 | X-RateLimit-Reset: 0 49 | !Retry-After 50 | --- response_body: {"X-RateLimit-Limit":700, "X-RateLimit-Remaining":700, "X-RateLimit-Reset":0} 51 | 52 | === TEST 2: too many requests 53 | --- http_config eval: $::HttpConfig 54 | --- config 55 | location /hit { 56 | rate_limit $remote_addr requests=4 period=5s burst=3; 57 | rate_limit_prefix a; 58 | rate_limit_pass redis; 59 | rate_limit_headers on; 60 | 61 | error_page 404 =200 @hit; 62 | } 63 | 64 | location @hit { 65 | default_type text/plain; 66 | return 200 "200 OK\n"; 67 | } 68 | --- request eval 69 | ['GET /hit', 'GET /hit', 'GET /hit', 'GET /hit', 'GET /hit'] 70 | --- response_headers eval 71 | ['X-RateLimit-Remaining: 3', 'X-RateLimit-Remaining: 2', 'X-RateLimit-Remaining: 1', 'X-RateLimit-Remaining: 0', 'Retry-After: 1'] 72 | --- response_body_like eval 73 | ['200 OK', '200 OK', '200 OK', '200 OK', '429 Too Many Requests'] 74 | --- error_code eval 75 | [200, 200, 200, 200, 429] 76 | 77 | === TEST 3: configurable quantity 78 | --- http_config eval: $::HttpConfig 79 | --- config 80 | location /hit { 81 | rate_limit $remote_addr requests=4 period=5s burst=3; 82 | rate_limit_prefix b; 83 | rate_limit_quantity 5; 84 | rate_limit_pass redis; 85 | rate_limit_headers on; 86 | 87 | error_page 404 =200 @hit; 88 | } 89 | 90 | location @hit { 91 | default_type text/plain; 92 | return 200 "200 OK\n"; 93 | } 94 | --- request 95 | GET /hit 96 | --- response_headers 97 | X-RateLimit-Limit: 4 98 | X-RateLimit-Remaining: 4 99 | X-RateLimit-Reset: 0 100 | !Retry-After 101 | --- response_body_like: 429 Too Many Requests 102 | --- error_code: 429 103 | --- error_log: rate limit exceeded for key "b_127.0.0.1" 104 | --------------------------------------------------------------------------------