├── LICENSE ├── PATCHES.md ├── README ├── config ├── docs ├── sticky.pdf └── sticky.vsd ├── ngx_http_sticky_misc.c ├── ngx_http_sticky_misc.h ├── ngx_http_sticky_module.c ├── patches ├── nginx-sticky-module-issue-24.patch ├── nginx-sticky-module-issue-31.patch └── nginx-sticky-module-issue-33.patch └── t └── basic.t /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions 6 | * are met: 7 | * 1. Redistributions of source code must retain the above copyright 8 | * notice, this list of conditions and the following disclaimer. 9 | * 2. Redistributions in binary form must reproduce the above copyright 10 | * notice, this list of conditions and the following disclaimer in the 11 | * documentation and/or other materials provided with the distribution. 12 | * 13 | * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 17 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | * SUCH DAMAGE. 24 | */ 25 | -------------------------------------------------------------------------------- /PATCHES.md: -------------------------------------------------------------------------------- 1 | This is a copy of the repo hosted here: 2 | 3 | https://code.google.com/p/nginx-sticky-module/ 4 | 5 | The following patches are applied: 6 | 7 | - Issue 24: Secure cookies (https://code.google.com/p/nginx-sticky-module/issues/detail?id=24) 8 | - Issue 31: Max-Age vs. Expires (https://code.google.com/p/nginx-sticky-module/issues/detail?id=31) 9 | - Issue 33: ngx_sock_ntop was changed in 1.5.X (https://code.google.com/p/nginx-sticky-module/issues/detail?id=33) 10 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Nginx Sticky Module 2 | -- 3 | 4 | *See PATCHES.md for applied patches* 5 | 6 | Description: 7 | A nginx module to add a sticky cookie to be always forwarded the the same 8 | upstream server. 9 | 10 | When dealing with several backend servers, it's sometimes useful that one 11 | client (browser) is always served by the same backend server 12 | (for session persistance for example). 13 | 14 | Using a persistance by IP (with the ip_hash upstream module) is maybe not 15 | a good idea because there could be situations where a lot of different 16 | browsers are coming with the same IP address (behind proxies)and the load 17 | balancing system won't be fair. 18 | 19 | Using a cookie to track the upstream server makes each browser unique. 20 | 21 | When the sticky module can't apply, it switchs back to the classic Round Robin 22 | Upstream or returns a "Bad Gateway" (depending on the no_fallback flag). 23 | 24 | Sticky module can't apply when cookies are not supported by the browser 25 | 26 | * Sticky module is based on a "best effort" algorithm. Its aim is not to handle 27 | * security somehow. It's been made to ensure that normal users are always 28 | * redirected to the same backend server: that's all! 29 | 30 | Installation 31 | 32 | You'll need to re-compile Nginx from source to include this module. 33 | Modify your compile of Nginx by adding the following directive 34 | (modified to suit your path of course): 35 | 36 | ./configure ... --add-module=/absolute/path/to/nginx-sticky-module 37 | make 38 | make install 39 | 40 | Usage 41 | upstream { 42 | sticky; 43 | server 127.0.0.1:9000; 44 | server 127.0.0.1:9001; 45 | server 127.0.0.1:9002; 46 | } 47 | 48 | sticky [name=route] [domain=.foo.bar] [path=/] [expires=1h] [hash=index|md5|sha1] [no_fallback]; 49 | - name: the name of the cookies used to track the persistant upstream srv 50 | default: route 51 | 52 | - domain: the domain in which the cookie will be valid 53 | default: nothing. Let the browser handle this. 54 | 55 | - path: the path in which the cookie will be valid 56 | default: nothing. Let the browser handle this. 57 | 58 | - expires: the validity duration of the cookie 59 | default: nothing. It's a session cookie. 60 | restriction: must be a duration greater than one second 61 | 62 | - hash: the hash mechanism to encode upstream server. It cant' be used 63 | with hmac. 64 | md5|sha1: well known hash 65 | index: it's not hashed, an in-memory index is used instead 66 | it's quicker and the overhead is shorter 67 | Warning: the matching against upstream servers list 68 | is inconsistent. So, at reload, if upstreams servers 69 | has changed, index values are not guaranted to 70 | correspond to the same server as before! 71 | USE IT WITH CAUTION and only if you need to! 72 | default: md5 73 | 74 | - hmac: the HMAC hash mechanism to encode upstream server 75 | It's like the hash mechanism but it uses hmac_key 76 | to secure the hashing. It can't be used with hash. 77 | md5|sha1: well known hash 78 | default: none. see hash. 79 | 80 | -hmac_key: the key to use with hmac. It's mandatory when hmac is set 81 | default: nothing. 82 | 83 | -no_fallback: when this flag is set, nginx will return a 502 (Bad Gateway or 84 | Proxy Error) if a request comes with a cookie and the 85 | corresponding backend is unavailable. 86 | 87 | Detail Mechanism 88 | see docs/sticky.{vsd,pdf} 89 | 90 | Warnings: 91 | - sticky module does not work with the "backup" option of the "server" configuration item. 92 | - sticky module does not work with the nginx_http_upstream_check_module. 93 | - sticky module may require to configure nginx with SSL support. 94 | 95 | Contributing 96 | http://code.google.com/p/nginx-sticky-module/ 97 | 98 | TODO 99 | Stress 100 | Code review 101 | 102 | Author 103 | Jerome Loyet 104 | 105 | Copyright & License 106 | This module is licenced under the BSD license. 107 | 108 | Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) 109 | 110 | Redistribution and use in source and binary forms, with or without 111 | modification, are permitted provided that the following conditions 112 | are met: 113 | 114 | 1. Redistributions of source code must retain the above copyright 115 | notice, this list of conditions and the following disclaimer. 116 | 117 | 2. Redistributions in binary form must reproduce the above copyright 118 | notice, this list of conditions and the following disclaimer in the 119 | documentation and/or other materials provided with the distribution. 120 | 121 | THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 122 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 123 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 124 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 125 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 126 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 127 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 128 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 129 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 130 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 131 | SUCH DAMAGE. 132 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_sticky_module 2 | HTTP_MODULES="$HTTP_MODULES ngx_http_sticky_module" 3 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_sticky_module.c $ngx_addon_dir/ngx_http_sticky_misc.c" 4 | NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_addon_dir/ngx_http_sticky_misc.h" 5 | USE_MD5=YES 6 | USE_SHA1=YES 7 | -------------------------------------------------------------------------------- /docs/sticky.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusis/nginx-sticky-module/dca9c94f86c4e4c38d4d492a6fef7704861bf05d/docs/sticky.pdf -------------------------------------------------------------------------------- /docs/sticky.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusis/nginx-sticky-module/dca9c94f86c4e4c38d4d492a6fef7704861bf05d/docs/sticky.vsd -------------------------------------------------------------------------------- /ngx_http_sticky_misc.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ngx_http_sticky_misc.h" 14 | 15 | #ifndef ngx_str_set 16 | #define ngx_str_set(str, text) (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text 17 | #endif 18 | 19 | static cookie_expires(char *str, size_t size, time_t t) 20 | { 21 | char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; 22 | char *wdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; 23 | struct tm e; 24 | gmtime_r(&t, &e); 25 | return snprintf(str, size, "%s, %02d-%s-%04d %02d:%02d:%02d GMT", 26 | wdays[e.tm_wday], e.tm_mday, months[e.tm_mon], e.tm_year + 1900, e.tm_hour,e.tm_min,e.tm_sec); 27 | } 28 | 29 | ngx_int_t ngx_http_sticky_misc_set_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires, unsigned secure, unsigned httponly) 30 | { 31 | u_char *cookie, *p; 32 | size_t len; 33 | ngx_table_elt_t *set_cookie, *elt; 34 | ngx_str_t remove; 35 | ngx_list_part_t *part; 36 | ngx_uint_t i; 37 | char expires_str[80]; 38 | int expires_len; 39 | 40 | if (value == NULL) { 41 | ngx_str_set(&remove, "_remove_"); 42 | value = &remove; 43 | } 44 | 45 | /* name = value */ 46 | len = name->len + 1 + value->len; 47 | 48 | /*; Domain= */ 49 | if (domain->len > 0) { 50 | len += sizeof("; Domain=") - 1 + domain->len; 51 | } 52 | 53 | /*; Expires= */ 54 | if (expires != NGX_CONF_UNSET) { 55 | expires_len = cookie_expires(expires_str, sizeof(expires_str), time(NULL) + expires); 56 | len += sizeof("; Expires=") - 1 + expires_len; 57 | } 58 | 59 | /* ; Path= */ 60 | if (path->len > 0) { 61 | len += sizeof("; Path=") - 1 + path->len; 62 | } 63 | 64 | /* ; Secure */ 65 | if (secure) { 66 | len += sizeof("; Secure") - 1; 67 | } 68 | 69 | /* ; HttpOnly */ 70 | if (httponly) { 71 | len += sizeof("; HttpOnly") - 1; 72 | } 73 | 74 | cookie = ngx_pnalloc(r->pool, len); 75 | if (cookie == NULL) { 76 | return NGX_ERROR; 77 | } 78 | 79 | p = ngx_copy(cookie, name->data, name->len); 80 | *p++ = '='; 81 | p = ngx_copy(p, value->data, value->len); 82 | 83 | if (domain->len > 0) { 84 | p = ngx_copy(p, "; Domain=", sizeof("; Domain=") - 1); 85 | p = ngx_copy(p, domain->data, domain->len); 86 | } 87 | 88 | if (expires != NGX_CONF_UNSET) { 89 | p = ngx_copy(p, "; Expires=", sizeof("; Expires=") - 1); 90 | p = ngx_copy(p, expires_str, expires_len); 91 | } 92 | 93 | if (path->len > 0) { 94 | p = ngx_copy(p, "; Path=", sizeof("; Path=") - 1); 95 | p = ngx_copy(p, path->data, path->len); 96 | } 97 | 98 | if (secure) { 99 | p = ngx_copy(p, "; Secure", sizeof("; Secure") - 1); 100 | } 101 | 102 | if (httponly) { 103 | p = ngx_copy(p, "; HttpOnly", sizeof("; HttpOnly") - 1); 104 | } 105 | 106 | part = &r->headers_out.headers.part; 107 | elt = part->elts; 108 | set_cookie = NULL; 109 | 110 | for (i=0 ;; i++) { 111 | if (part->nelts > 1 || i >= part->nelts) { 112 | if (part->next == NULL) { 113 | break; 114 | } 115 | part = part->next; 116 | elt = part->elts; 117 | i = 0; 118 | } 119 | /* ... */ 120 | if (ngx_strncmp(elt->value.data, name->data, name->len) == 0) { 121 | set_cookie = elt; 122 | break; 123 | } 124 | } 125 | 126 | /* found a Set-Cookie header with the same name: replace it */ 127 | if (set_cookie != NULL) { 128 | set_cookie->value.len = p - cookie; 129 | set_cookie->value.data = cookie; 130 | return NGX_OK; 131 | } 132 | 133 | set_cookie = ngx_list_push(&r->headers_out.headers); 134 | if (set_cookie == NULL) { 135 | return NGX_ERROR; 136 | } 137 | set_cookie->hash = 1; 138 | ngx_str_set(&set_cookie->key, "Set-Cookie"); 139 | set_cookie->value.len = p - cookie; 140 | set_cookie->value.data = cookie; 141 | 142 | return NGX_OK; 143 | } 144 | 145 | ngx_int_t ngx_http_sticky_misc_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest) 146 | { 147 | ngx_md5_t md5; 148 | u_char hash[MD5_DIGEST_LENGTH]; 149 | 150 | digest->data = ngx_pcalloc(pool, MD5_DIGEST_LENGTH * 2); 151 | if (digest->data == NULL) { 152 | return NGX_ERROR; 153 | } 154 | 155 | digest->len = MD5_DIGEST_LENGTH * 2; 156 | ngx_md5_init(&md5); 157 | ngx_md5_update(&md5, in, len); 158 | ngx_md5_final(hash, &md5); 159 | 160 | ngx_hex_dump(digest->data, hash, MD5_DIGEST_LENGTH); 161 | return NGX_OK; 162 | } 163 | 164 | ngx_int_t ngx_http_sticky_misc_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest) 165 | { 166 | ngx_sha1_t sha1; 167 | u_char hash[SHA_DIGEST_LENGTH]; 168 | 169 | digest->data = ngx_pcalloc(pool, SHA_DIGEST_LENGTH * 2); 170 | if (digest->data == NULL) { 171 | return NGX_ERROR; 172 | } 173 | 174 | digest->len = SHA_DIGEST_LENGTH * 2; 175 | ngx_sha1_init(&sha1); 176 | ngx_sha1_update(&sha1, in, len); 177 | ngx_sha1_final(hash, &sha1); 178 | 179 | ngx_hex_dump(digest->data, hash, SHA_DIGEST_LENGTH); 180 | return NGX_OK; 181 | } 182 | 183 | ngx_int_t ngx_http_sticky_misc_hmac_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest) 184 | { 185 | u_char hash[MD5_DIGEST_LENGTH]; 186 | u_char k[MD5_CBLOCK]; 187 | ngx_md5_t md5; 188 | u_int i; 189 | 190 | digest->data = ngx_pcalloc(pool, MD5_DIGEST_LENGTH * 2); 191 | if (digest->data == NULL) { 192 | return NGX_ERROR; 193 | } 194 | digest->len = MD5_DIGEST_LENGTH * 2; 195 | 196 | ngx_memzero(k, sizeof(k)); 197 | 198 | if (key->len > MD5_CBLOCK) { 199 | ngx_md5_init(&md5); 200 | ngx_md5_update(&md5, key->data, key->len); 201 | ngx_md5_final(k, &md5); 202 | } else { 203 | ngx_memcpy(k, key->data, key->len); 204 | } 205 | 206 | /* XOR ipad */ 207 | for (i=0; i < MD5_CBLOCK; i++) { 208 | k[i] ^= 0x36; 209 | } 210 | 211 | ngx_md5_init(&md5); 212 | ngx_md5_update(&md5, k, MD5_CBLOCK); 213 | ngx_md5_update(&md5, in, len); 214 | ngx_md5_final(hash, &md5); 215 | 216 | /* Convert k to opad -- 0x6A = 0x36 ^ 0x5C */ 217 | for (i=0; i < MD5_CBLOCK; i++) { 218 | k[i] ^= 0x6a; 219 | } 220 | 221 | ngx_md5_init(&md5); 222 | ngx_md5_update(&md5, k, MD5_CBLOCK); 223 | ngx_md5_update(&md5, hash, MD5_DIGEST_LENGTH); 224 | ngx_md5_final(hash, &md5); 225 | 226 | ngx_hex_dump(digest->data, hash, MD5_DIGEST_LENGTH); 227 | 228 | return NGX_OK; 229 | } 230 | 231 | ngx_int_t ngx_http_sticky_misc_hmac_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest) 232 | { 233 | u_char hash[SHA_DIGEST_LENGTH]; 234 | u_char k[SHA_CBLOCK]; 235 | ngx_sha1_t sha1; 236 | u_int i; 237 | 238 | digest->data = ngx_pcalloc(pool, SHA_DIGEST_LENGTH * 2); 239 | if (digest->data == NULL) { 240 | return NGX_ERROR; 241 | } 242 | digest->len = SHA_DIGEST_LENGTH * 2; 243 | 244 | ngx_memzero(k, sizeof(k)); 245 | 246 | if (key->len > SHA_CBLOCK) { 247 | ngx_sha1_init(&sha1); 248 | ngx_sha1_update(&sha1, key->data, key->len); 249 | ngx_sha1_final(k, &sha1); 250 | } else { 251 | ngx_memcpy(k, key->data, key->len); 252 | } 253 | 254 | /* XOR ipad */ 255 | for (i=0; i < SHA_CBLOCK; i++) { 256 | k[i] ^= 0x36; 257 | } 258 | 259 | ngx_sha1_init(&sha1); 260 | ngx_sha1_update(&sha1, k, SHA_CBLOCK); 261 | ngx_sha1_update(&sha1, in, len); 262 | ngx_sha1_final(hash, &sha1); 263 | 264 | /* Convert k to opad -- 0x6A = 0x36 ^ 0x5C */ 265 | for (i=0; i < SHA_CBLOCK; i++) { 266 | k[i] ^= 0x6a; 267 | } 268 | 269 | ngx_sha1_init(&sha1); 270 | ngx_sha1_update(&sha1, k, SHA_CBLOCK); 271 | ngx_sha1_update(&sha1, hash, SHA_DIGEST_LENGTH); 272 | ngx_sha1_final(hash, &sha1); 273 | 274 | ngx_hex_dump(digest->data, hash, SHA_DIGEST_LENGTH); 275 | 276 | return NGX_OK; 277 | } 278 | 279 | ngx_int_t ngx_http_sticky_misc_text_raw(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest) 280 | { 281 | size_t len; 282 | if (!in) { 283 | return NGX_ERROR; 284 | } 285 | 286 | switch (in->sa_family) { 287 | case AF_INET: 288 | len = NGX_INET_ADDRSTRLEN + sizeof(":65535") - 1; 289 | break; 290 | 291 | #if (NGX_HAVE_INET6) 292 | case AF_INET6: 293 | len = NGX_INET6_ADDRSTRLEN + sizeof(":65535") - 1; 294 | break; 295 | #endif 296 | 297 | #if (NGX_HAVE_UNIX_DOMAIN) 298 | case AF_UNIX: 299 | len = sizeof("unix:") - 1 + NGX_UNIX_ADDRSTRLEN; 300 | break; 301 | #endif 302 | 303 | default: 304 | return NGX_ERROR; 305 | } 306 | 307 | 308 | digest->data = ngx_pnalloc(pool, len); 309 | if (digest->data == NULL) { 310 | return NGX_ERROR; 311 | } 312 | #if defined(nginx_version) && nginx_version >= 1005003 313 | digest->len = ngx_sock_ntop(in, sizeof(struct sockaddr_in), digest->data, len, 1); 314 | #else 315 | digest->len = ngx_sock_ntop(in, digest->data, len, 1); 316 | return NGX_OK; 317 | #endif 318 | return NGX_OK; 319 | } 320 | 321 | ngx_int_t ngx_http_sticky_misc_text_md5(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest) 322 | { 323 | ngx_str_t str; 324 | if (ngx_http_sticky_misc_text_raw(pool, in, &str) != NGX_OK) { 325 | return NGX_ERROR; 326 | } 327 | 328 | if (ngx_http_sticky_misc_md5(pool, (void *)str.data, str.len, digest) != NGX_OK) { 329 | ngx_pfree(pool, &str); 330 | return NGX_ERROR; 331 | } 332 | 333 | return ngx_pfree(pool, &str); 334 | } 335 | 336 | ngx_int_t ngx_http_sticky_misc_text_sha1(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest) 337 | { 338 | ngx_str_t str; 339 | if (ngx_http_sticky_misc_text_raw(pool, in, &str) != NGX_OK) { 340 | return NGX_ERROR; 341 | } 342 | 343 | if (ngx_http_sticky_misc_sha1(pool, (void *)str.data, str.len, digest) != NGX_OK) { 344 | ngx_pfree(pool, &str); 345 | return NGX_ERROR; 346 | } 347 | 348 | return ngx_pfree(pool, &str); 349 | } 350 | 351 | -------------------------------------------------------------------------------- /ngx_http_sticky_misc.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) 4 | */ 5 | 6 | #ifndef _NGX_HTTP_STICKY_MISC_H_INCLUDED_ 7 | #define _NGX_HTTP_STICKY_MISC_H_INCLUDED_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | typedef ngx_int_t (*ngx_http_sticky_misc_hash_pt)(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); 15 | typedef ngx_int_t (*ngx_http_sticky_misc_hmac_pt)(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); 16 | typedef ngx_int_t (*ngx_http_sticky_misc_text_pt)(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); 17 | 18 | ngx_int_t ngx_http_sticky_misc_set_cookie (ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires, unsigned secure, unsigned httponly); 19 | ngx_int_t ngx_http_sticky_misc_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); 20 | ngx_int_t ngx_http_sticky_misc_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); 21 | ngx_int_t ngx_http_sticky_misc_hmac_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); 22 | ngx_int_t ngx_http_sticky_misc_hmac_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); 23 | 24 | ngx_int_t ngx_http_sticky_misc_text_raw(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); 25 | ngx_int_t ngx_http_sticky_misc_text_md5(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); 26 | ngx_int_t ngx_http_sticky_misc_text_sha1(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); 27 | 28 | #endif /* _NGX_HTTP_STICKY_MISC_H_INCLUDED_ */ 29 | -------------------------------------------------------------------------------- /ngx_http_sticky_module.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (C) Jerome Loyet 4 | */ 5 | 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "ngx_http_sticky_misc.h" 12 | 13 | /* define a peer */ 14 | typedef struct { 15 | ngx_http_upstream_rr_peer_t *rr_peer; 16 | ngx_str_t digest; 17 | } ngx_http_sticky_peer_t; 18 | 19 | /* the configuration structure */ 20 | typedef struct { 21 | ngx_http_upstream_srv_conf_t uscf; 22 | ngx_str_t cookie_name; 23 | ngx_str_t cookie_domain; 24 | ngx_str_t cookie_path; 25 | time_t cookie_expires; 26 | unsigned cookie_secure:1; 27 | unsigned cookie_httponly:1; 28 | ngx_str_t hmac_key; 29 | ngx_http_sticky_misc_hash_pt hash; 30 | ngx_http_sticky_misc_hmac_pt hmac; 31 | ngx_http_sticky_misc_text_pt text; 32 | ngx_uint_t no_fallback; 33 | ngx_http_sticky_peer_t *peers; 34 | } ngx_http_sticky_srv_conf_t; 35 | 36 | 37 | /* the custom sticky struct used on each request */ 38 | typedef struct { 39 | /* the round robin data must be first */ 40 | ngx_http_upstream_rr_peer_data_t rrp; 41 | ngx_event_get_peer_pt get_rr_peer; 42 | int selected_peer; 43 | int no_fallback; 44 | ngx_http_sticky_srv_conf_t *sticky_conf; 45 | ngx_http_request_t *request; 46 | } ngx_http_sticky_peer_data_t; 47 | 48 | 49 | static ngx_int_t ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us); 50 | static ngx_int_t ngx_http_get_sticky_peer(ngx_peer_connection_t *pc, void *data); 51 | static char *ngx_http_sticky_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 52 | static void *ngx_http_sticky_create_conf(ngx_conf_t *cf); 53 | 54 | 55 | static ngx_command_t ngx_http_sticky_commands[] = { 56 | 57 | { ngx_string("sticky"), 58 | NGX_HTTP_UPS_CONF|NGX_CONF_ANY, 59 | ngx_http_sticky_set, 60 | 0, 61 | 0, 62 | NULL }, 63 | 64 | ngx_null_command 65 | }; 66 | 67 | 68 | static ngx_http_module_t ngx_http_sticky_module_ctx = { 69 | NULL, /* preconfiguration */ 70 | NULL, /* postconfiguration */ 71 | 72 | NULL, /* create main configuration */ 73 | NULL, /* init main configuration */ 74 | 75 | ngx_http_sticky_create_conf, /* create server configuration */ 76 | NULL, /* merge server configuration */ 77 | 78 | NULL, /* create location configuration */ 79 | NULL /* merge location configuration */ 80 | }; 81 | 82 | 83 | ngx_module_t ngx_http_sticky_module = { 84 | NGX_MODULE_V1, 85 | &ngx_http_sticky_module_ctx, /* module context */ 86 | ngx_http_sticky_commands, /* module directives */ 87 | NGX_HTTP_MODULE, /* module type */ 88 | NULL, /* init master */ 89 | NULL, /* init module */ 90 | NULL, /* init process */ 91 | NULL, /* init thread */ 92 | NULL, /* exit thread */ 93 | NULL, /* exit process */ 94 | NULL, /* exit master */ 95 | NGX_MODULE_V1_PADDING 96 | }; 97 | 98 | 99 | /* 100 | * function called by the upstream module to init itself 101 | * it's called once per instance 102 | */ 103 | ngx_int_t ngx_http_init_upstream_sticky(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) 104 | { 105 | ngx_http_upstream_rr_peers_t *rr_peers; 106 | ngx_http_sticky_srv_conf_t *conf; 107 | ngx_uint_t i; 108 | 109 | /* call the rr module on wich the sticky module is based on */ 110 | if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) { 111 | return NGX_ERROR; 112 | } 113 | 114 | /* calculate each peer digest once and save */ 115 | rr_peers = us->peer.data; 116 | 117 | /* do nothing there's only one peer */ 118 | if (rr_peers->number <= 1 || rr_peers->single) { 119 | return NGX_OK; 120 | } 121 | 122 | /* tell the upstream module to call ngx_http_init_sticky_peer when it inits peer */ 123 | us->peer.init = ngx_http_init_sticky_peer; 124 | 125 | conf = ngx_http_conf_upstream_srv_conf(us, ngx_http_sticky_module); 126 | 127 | /* if 'index', no need to alloc and generate digest */ 128 | if (!conf->hash && !conf->hmac && !conf->text) { 129 | conf->peers = NULL; 130 | return NGX_OK; 131 | } 132 | 133 | /* create our own upstream indexes */ 134 | conf->peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_sticky_peer_t) * rr_peers->number); 135 | if (conf->peers == NULL) { 136 | return NGX_ERROR; 137 | } 138 | 139 | /* parse each peer and generate digest if necessary */ 140 | for (i = 0; i < rr_peers->number; i++) { 141 | conf->peers[i].rr_peer = &rr_peers->peer[i]; 142 | 143 | if (conf->hmac) { 144 | /* generate hmac */ 145 | conf->hmac(cf->pool, rr_peers->peer[i].sockaddr, rr_peers->peer[i].socklen, &conf->hmac_key, &conf->peers[i].digest); 146 | 147 | } else if (conf->text) { 148 | /* generate text */ 149 | conf->text(cf->pool, rr_peers->peer[i].sockaddr, &conf->peers[i].digest); 150 | 151 | } else { 152 | /* generate hash */ 153 | conf->hash(cf->pool, rr_peers->peer[i].sockaddr, rr_peers->peer[i].socklen, &conf->peers[i].digest); 154 | } 155 | 156 | #if 0 157 | /* FIXME: is it possible to log to debug level when at configuration stage ? */ 158 | ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "[sticky/ngx_http_init_upstream_sticky] generated digest \"%V\" for upstream at index %d", &conf->peers[i].digest, i); 159 | #endif 160 | 161 | } 162 | 163 | return NGX_OK; 164 | } 165 | 166 | /* 167 | * function called by the upstream module when it inits each peer 168 | * it's called once per request 169 | */ 170 | static ngx_int_t ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) 171 | { 172 | ngx_http_sticky_peer_data_t *iphp; 173 | ngx_str_t route; 174 | ngx_uint_t i; 175 | ngx_int_t n; 176 | 177 | /* alloc custom sticky struct */ 178 | iphp = ngx_palloc(r->pool, sizeof(ngx_http_sticky_peer_data_t)); 179 | if (iphp == NULL) { 180 | return NGX_ERROR; 181 | } 182 | 183 | /* attach it to the request upstream data */ 184 | r->upstream->peer.data = &iphp->rrp; 185 | 186 | /* call the rr module on which the sticky is based on */ 187 | if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) { 188 | return NGX_ERROR; 189 | } 190 | 191 | /* set the callback to select the next peer to use */ 192 | r->upstream->peer.get = ngx_http_get_sticky_peer; 193 | 194 | /* init the custom sticky struct */ 195 | iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; 196 | iphp->selected_peer = -1; 197 | iphp->no_fallback = 0; 198 | iphp->sticky_conf = ngx_http_conf_upstream_srv_conf(us, ngx_http_sticky_module); 199 | iphp->request = r; 200 | 201 | /* check weather a cookie is present or not and save it */ 202 | if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &iphp->sticky_conf->cookie_name, &route) != NGX_DECLINED) { 203 | /* a route cookie has been found. Let's give it a try */ 204 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] got cookie route=%V, let's try to find a matching peer", &route); 205 | 206 | /* hash, hmac or text, just compare digest */ 207 | if (iphp->sticky_conf->hash || iphp->sticky_conf->hmac || iphp->sticky_conf->text) { 208 | 209 | /* check internal struct has been set */ 210 | if (!iphp->sticky_conf->peers) { 211 | /* log a warning, as it will continue without the sticky */ 212 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "[sticky/init_sticky_peer] internal peers struct has not been set"); 213 | return NGX_OK; /* return OK, in order to continue */ 214 | } 215 | 216 | /* search the digest found in the cookie in the peer digest list */ 217 | for (i = 0; i < iphp->rrp.peers->number; i++) { 218 | 219 | /* ensure the both len are equal and > 0 */ 220 | if (iphp->sticky_conf->peers[i].digest.len != route.len || route.len <= 0) { 221 | continue; 222 | } 223 | 224 | if (!ngx_strncmp(iphp->sticky_conf->peers[i].digest.data, route.data, route.len)) { 225 | /* we found a match */ 226 | iphp->selected_peer = i; 227 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] the route \"%V\" matches peer at index %ui", &route, i); 228 | return NGX_OK; 229 | } 230 | } 231 | 232 | } else { 233 | 234 | /* switch back to index, just convert to integer and ensure it corresponds to a valid peer */ 235 | n = ngx_atoi(route.data, route.len); 236 | if (n == NGX_ERROR) { 237 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "[sticky/init_sticky_peer] unable to convert the route \"%V\" to an integer value", &route); 238 | } else if (n >= 0 && n < (ngx_int_t)iphp->rrp.peers->number) { 239 | /* found one */ 240 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] the route \"%V\" matches peer at index %i", &route, n); 241 | iphp->selected_peer = n; 242 | return NGX_OK; 243 | } 244 | } 245 | 246 | /* nothing was found, just continue with rr */ 247 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] the route \"%V\" does not match any peer. Just ignoring it ...", &route); 248 | return NGX_OK; 249 | } 250 | 251 | /* nothing found */ 252 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] route cookie not found", &route); 253 | return NGX_OK; /* return OK, in order to continue */ 254 | } 255 | 256 | /* 257 | * function called by the upstream module to choose the next peer to use 258 | * called at least one time per request 259 | */ 260 | static ngx_int_t ngx_http_get_sticky_peer(ngx_peer_connection_t *pc, void *data) 261 | { 262 | ngx_http_sticky_peer_data_t *iphp = data; 263 | ngx_http_sticky_srv_conf_t *conf = iphp->sticky_conf; 264 | ngx_int_t selected_peer = -1; 265 | time_t now = ngx_time(); 266 | uintptr_t m; 267 | ngx_uint_t n, i; 268 | ngx_http_upstream_rr_peer_t *peer = NULL; 269 | 270 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] get sticky peer, try: %ui, n_peers: %ui, no_fallback: %ui/%ui", pc->tries, iphp->rrp.peers->number, conf->no_fallback, iphp->no_fallback); 271 | 272 | /* TODO: cached */ 273 | 274 | /* has the sticky module already choosen a peer to connect to and is it a valid peer */ 275 | /* is there more than one peer (otherwise, no choices to make) */ 276 | if (iphp->selected_peer >= 0 && iphp->selected_peer < (ngx_int_t)iphp->rrp.peers->number && !iphp->rrp.peers->single) { 277 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] let's try the selected peer (%i)", iphp->selected_peer); 278 | 279 | n = iphp->selected_peer / (8 * sizeof(uintptr_t)); 280 | m = (uintptr_t) 1 << iphp->selected_peer % (8 * sizeof(uintptr_t)); 281 | 282 | /* has the peer not already been tried ? */ 283 | if (!(iphp->rrp.tried[n] & m)) { 284 | peer = &iphp->rrp.peers->peer[iphp->selected_peer]; 285 | 286 | /* if the no_fallback flag is set */ 287 | if (conf->no_fallback) { 288 | 289 | iphp->no_fallback = 1; 290 | 291 | /* if peer is down */ 292 | if (peer->down) { 293 | ngx_log_error(NGX_LOG_NOTICE, pc->log, 0, "[sticky/get_sticky_peer] the selected peer is down and no_fallback is flagged"); 294 | return NGX_BUSY; 295 | } 296 | 297 | /* if it's been ignored for long enought (fail_timeout), reset timeout */ 298 | /* do this check before testing peer->fails ! :) */ 299 | if (now - peer->accessed > peer->fail_timeout) { 300 | peer->fails = 0; 301 | } 302 | 303 | /* if peer is failed */ 304 | if (peer->max_fails > 0 && peer->fails >= peer->max_fails) { 305 | ngx_log_error(NGX_LOG_NOTICE, pc->log, 0, "[sticky/get_sticky_peer] the selected peer is maked as failed and no_fallback is flagged"); 306 | return NGX_BUSY; 307 | } 308 | } 309 | 310 | /* ensure the peer is not marked as down */ 311 | if (!peer->down) { 312 | 313 | /* if it's not failedi, use it */ 314 | if (peer->max_fails == 0 || peer->fails < peer->max_fails) { 315 | selected_peer = (ngx_int_t)n; 316 | 317 | /* if it's been ignored for long enought (fail_timeout), reset timeout and use it */ 318 | } else if (now - peer->accessed > peer->fail_timeout) { 319 | peer->fails = 0; 320 | selected_peer = (ngx_int_t)n; 321 | 322 | /* it's failed or timeout did not expire yet */ 323 | } else { 324 | /* mark the peer as tried */ 325 | iphp->rrp.tried[n] |= m; 326 | } 327 | } 328 | } 329 | } 330 | 331 | /* we have a valid peer, tell the upstream module to use it */ 332 | if (peer && selected_peer >= 0) { 333 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] peer found at index %i", selected_peer); 334 | 335 | iphp->rrp.current = iphp->selected_peer; 336 | pc->cached = 0; 337 | pc->connection = NULL; 338 | pc->sockaddr = peer->sockaddr; 339 | pc->socklen = peer->socklen; 340 | pc->name = &peer->name; 341 | 342 | iphp->rrp.tried[n] |= m; 343 | 344 | } else { 345 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] no sticky peer selected, switch back to classic rr"); 346 | 347 | if (iphp->no_fallback) { 348 | ngx_log_error(NGX_LOG_NOTICE, pc->log, 0, "[sticky/get_sticky_peer] No fallback in action !"); 349 | return NGX_BUSY; 350 | } 351 | 352 | ngx_int_t ret = iphp->get_rr_peer(pc, &iphp->rrp); 353 | if (ret != NGX_OK) { 354 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] ngx_http_upstream_get_round_robin_peer returned %i", ret); 355 | return ret; 356 | } 357 | 358 | /* search for the choosen peer in order to set the cookie */ 359 | for (i = 0; i < iphp->rrp.peers->number; i++) { 360 | 361 | if (iphp->rrp.peers->peer[i].sockaddr == pc->sockaddr && iphp->rrp.peers->peer[i].socklen == pc->socklen) { 362 | if (conf->hash || conf->hmac || conf->text) { 363 | ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &conf->peers[i].digest, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires, conf->cookie_secure, conf->cookie_httponly); 364 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] set cookie \"%V\" value=\"%V\" index=%ui", &conf->cookie_name, &conf->peers[i].digest, i); 365 | } else { 366 | ngx_str_t route; 367 | ngx_uint_t tmp = i; 368 | route.len = 0; 369 | do { 370 | route.len++; 371 | } while (tmp /= 10); 372 | route.data = ngx_pcalloc(iphp->request->pool, sizeof(u_char) * (route.len + 1)); 373 | if (route.data == NULL) { 374 | break; 375 | } 376 | ngx_snprintf(route.data, route.len, "%d", i); 377 | route.len = ngx_strlen(route.data); 378 | ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &route, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires, conf->cookie_secure, conf->cookie_httponly); 379 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] set cookie \"%V\" value=\"%V\" index=%ui", &conf->cookie_name, &tmp, i); 380 | } 381 | break; /* found and hopefully the cookie have been set */ 382 | } 383 | } 384 | } 385 | 386 | /* reset the selection in order to bypass the sticky module when the upstream module will try another peers if necessary */ 387 | iphp->selected_peer = -1; 388 | 389 | return NGX_OK; 390 | } 391 | 392 | /* 393 | * Function called when the sticky command is parsed on the conf file 394 | */ 395 | static char *ngx_http_sticky_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 396 | { 397 | ngx_http_upstream_srv_conf_t *upstream_conf; 398 | ngx_http_sticky_srv_conf_t *sticky_conf; 399 | ngx_uint_t i; 400 | ngx_str_t tmp; 401 | ngx_str_t name = ngx_string("route"); 402 | ngx_str_t domain = ngx_string(""); 403 | ngx_str_t path = ngx_string(""); 404 | ngx_str_t hmac_key = ngx_string(""); 405 | time_t expires = NGX_CONF_UNSET; 406 | unsigned secure = 0; 407 | unsigned httponly = 0; 408 | ngx_http_sticky_misc_hash_pt hash = NGX_CONF_UNSET_PTR; 409 | ngx_http_sticky_misc_hmac_pt hmac = NULL; 410 | ngx_http_sticky_misc_text_pt text = NULL; 411 | ngx_uint_t no_fallback = 0; 412 | 413 | /* parse all elements */ 414 | for (i = 1; i < cf->args->nelts; i++) { 415 | ngx_str_t *value = cf->args->elts; 416 | 417 | /* is "name=" is starting the argument ? */ 418 | if ((u_char *)ngx_strstr(value[i].data, "name=") == value[i].data) { 419 | 420 | /* do we have at least on char after "name=" ? */ 421 | if (value[i].len <= sizeof("name=") - 1) { 422 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"name=\""); 423 | return NGX_CONF_ERROR; 424 | } 425 | 426 | /* save what's after "name=" */ 427 | name.len = value[i].len - ngx_strlen("name="); 428 | name.data = (u_char *)(value[i].data + sizeof("name=") - 1); 429 | continue; 430 | } 431 | 432 | /* is "domain=" is starting the argument ? */ 433 | if ((u_char *)ngx_strstr(value[i].data, "domain=") == value[i].data) { 434 | 435 | /* do we have at least on char after "domain=" ? */ 436 | if (value[i].len <= ngx_strlen("domain=")) { 437 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"domain=\""); 438 | return NGX_CONF_ERROR; 439 | } 440 | 441 | /* save what's after "domain=" */ 442 | domain.len = value[i].len - ngx_strlen("domain="); 443 | domain.data = (u_char *)(value[i].data + sizeof("domain=") - 1); 444 | continue; 445 | } 446 | 447 | /* is "path=" is starting the argument ? */ 448 | if ((u_char *)ngx_strstr(value[i].data, "path=") == value[i].data) { 449 | 450 | /* do we have at least on char after "path=" ? */ 451 | if (value[i].len <= ngx_strlen("path=")) { 452 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"path=\""); 453 | return NGX_CONF_ERROR; 454 | } 455 | 456 | /* save what's after "domain=" */ 457 | path.len = value[i].len - ngx_strlen("path="); 458 | path.data = (u_char *)(value[i].data + sizeof("path=") - 1); 459 | continue; 460 | } 461 | 462 | /* is "expires=" is starting the argument ? */ 463 | if ((u_char *)ngx_strstr(value[i].data, "expires=") == value[i].data) { 464 | 465 | /* do we have at least on char after "expires=" ? */ 466 | if (value[i].len <= sizeof("expires=") - 1) { 467 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"expires=\""); 468 | return NGX_CONF_ERROR; 469 | } 470 | 471 | /* extract value */ 472 | tmp.len = value[i].len - ngx_strlen("expires="); 473 | tmp.data = (u_char *)(value[i].data + sizeof("expires=") - 1); 474 | 475 | /* convert to time, save and validate */ 476 | expires = ngx_parse_time(&tmp, 1); 477 | if (expires == NGX_ERROR || expires < 1) { 478 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value for \"expires=\""); 479 | return NGX_CONF_ERROR; 480 | } 481 | continue; 482 | } 483 | 484 | if (ngx_strncmp(value[i].data, "secure", 6) == 0 && value[i].len == 6) { 485 | secure = 1; 486 | continue; 487 | } 488 | 489 | if (ngx_strncmp(value[i].data, "httponly", 8) == 0 && value[i].len == 8) { 490 | httponly = 1; 491 | continue; 492 | } 493 | 494 | /* is "text=" is starting the argument ? */ 495 | if ((u_char *)ngx_strstr(value[i].data, "text=") == value[i].data) { 496 | 497 | /* only hash or hmac can be used, not both */ 498 | if (hmac || hash != NGX_CONF_UNSET_PTR) { 499 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please choose between \"hash=\", \"hmac=\" and \"text\""); 500 | return NGX_CONF_ERROR; 501 | } 502 | 503 | /* do we have at least on char after "name=" ? */ 504 | if (value[i].len <= sizeof("text=") - 1) { 505 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"text=\""); 506 | return NGX_CONF_ERROR; 507 | } 508 | 509 | /* extract value to temp */ 510 | tmp.len = value[i].len - ngx_strlen("text="); 511 | tmp.data = (u_char *)(value[i].data + sizeof("text=") - 1); 512 | 513 | /* is name=raw */ 514 | if (ngx_strncmp(tmp.data, "raw", sizeof("raw") - 1) == 0 ) { 515 | text = ngx_http_sticky_misc_text_raw; 516 | continue; 517 | } 518 | 519 | /* is name=md5 */ 520 | if (ngx_strncmp(tmp.data, "md5", sizeof("md5") - 1) == 0 ) { 521 | text = ngx_http_sticky_misc_text_md5; 522 | continue; 523 | } 524 | 525 | /* is name=sha1 */ 526 | if (ngx_strncmp(tmp.data, "sha1", sizeof("sha1") - 1) == 0 ) { 527 | text = ngx_http_sticky_misc_text_sha1; 528 | continue; 529 | } 530 | 531 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "wrong value for \"text=\": raw, md5 or sha1"); 532 | return NGX_CONF_ERROR; 533 | } 534 | 535 | /* is "hash=" is starting the argument ? */ 536 | if ((u_char *)ngx_strstr(value[i].data, "hash=") == value[i].data) { 537 | 538 | /* only hash or hmac can be used, not both */ 539 | if (hmac || text) { 540 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please choose between \"hash=\", \"hmac=\" and \"text=\""); 541 | return NGX_CONF_ERROR; 542 | } 543 | 544 | /* do we have at least on char after "hash=" ? */ 545 | if (value[i].len <= sizeof("hash=") - 1) { 546 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"hash=\""); 547 | return NGX_CONF_ERROR; 548 | } 549 | 550 | /* extract value to temp */ 551 | tmp.len = value[i].len - ngx_strlen("hash="); 552 | tmp.data = (u_char *)(value[i].data + sizeof("hash=") - 1); 553 | 554 | /* is hash=index */ 555 | if (ngx_strncmp(tmp.data, "index", sizeof("index") - 1) == 0 ) { 556 | hash = NULL; 557 | continue; 558 | } 559 | 560 | /* is hash=md5 */ 561 | if (ngx_strncmp(tmp.data, "md5", sizeof("md5") - 1) == 0 ) { 562 | hash = ngx_http_sticky_misc_md5; 563 | continue; 564 | } 565 | 566 | /* is hash=sha1 */ 567 | if (ngx_strncmp(tmp.data, "sha1", sizeof("sha1") - 1) == 0 ) { 568 | hash = ngx_http_sticky_misc_sha1; 569 | continue; 570 | } 571 | 572 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "wrong value for \"hash=\": index, md5 or sha1"); 573 | return NGX_CONF_ERROR; 574 | } 575 | 576 | /* is "hmac=" is starting the argument ? */ 577 | if ((u_char *)ngx_strstr(value[i].data, "hmac=") == value[i].data) { 578 | 579 | /* only hash or hmac can be used, not both */ 580 | if (hash != NGX_CONF_UNSET_PTR || text) { 581 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please choose between \"hash=\", \"hmac=\" and \"text\""); 582 | return NGX_CONF_ERROR; 583 | } 584 | 585 | /* do we have at least on char after "hmac=" ? */ 586 | if (value[i].len <= sizeof("hmac=") - 1) { 587 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"hmac=\""); 588 | return NGX_CONF_ERROR; 589 | } 590 | 591 | /* extract value */ 592 | tmp.len = value[i].len - ngx_strlen("hmac="); 593 | tmp.data = (u_char *)(value[i].data + sizeof("hmac=") - 1); 594 | 595 | /* is hmac=md5 ? */ 596 | if (ngx_strncmp(tmp.data, "md5", sizeof("md5") - 1) == 0 ) { 597 | hmac = ngx_http_sticky_misc_hmac_md5; 598 | continue; 599 | } 600 | 601 | /* is hmac=sha1 ? */ 602 | if (ngx_strncmp(tmp.data, "sha1", sizeof("sha1") - 1) == 0 ) { 603 | hmac = ngx_http_sticky_misc_hmac_sha1; 604 | continue; 605 | } 606 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "wrong value for \"hmac=\": md5 or sha1"); 607 | return NGX_CONF_ERROR; 608 | } 609 | 610 | /* is "hmac_key=" is starting the argument ? */ 611 | if ((u_char *)ngx_strstr(value[i].data, "hmac_key=") == value[i].data) { 612 | 613 | /* do we have at least on char after "hmac_key=" ? */ 614 | if (value[i].len <= ngx_strlen("hmac_key=")) { 615 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"hmac_key=\""); 616 | return NGX_CONF_ERROR; 617 | } 618 | 619 | /* save what's after "hmac_key=" */ 620 | hmac_key.len = value[i].len - ngx_strlen("hmac_key="); 621 | hmac_key.data = (u_char *)(value[i].data + sizeof("hmac_key=") - 1); 622 | continue; 623 | } 624 | 625 | /* is "no_fallback" flag present ? */ 626 | if (ngx_strncmp(value[i].data, "no_fallback", sizeof("no_fallback") - 1) == 0 ) { 627 | no_fallback = 1; 628 | continue; 629 | } 630 | 631 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid arguement (%V)", &value[i]); 632 | return NGX_CONF_ERROR; 633 | } 634 | 635 | /* if has and hmac and name have not been set, default to md5 */ 636 | if (hash == NGX_CONF_UNSET_PTR && hmac == NULL && text == NULL) { 637 | hash = ngx_http_sticky_misc_md5; 638 | } 639 | 640 | /* don't allow meaning less parameters */ 641 | if (hmac_key.len > 0 && hash != NGX_CONF_UNSET_PTR) { 642 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"hmac_key=\" is meaningless when \"hmac\" is used. Please remove it."); 643 | return NGX_CONF_ERROR; 644 | } 645 | 646 | /* ensure we have an hmac key if hmac's been set */ 647 | if (hmac_key.len == 0 && hmac != NULL) { 648 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please specify \"hmac_key=\" when using \"hmac\""); 649 | return NGX_CONF_ERROR; 650 | } 651 | 652 | /* ensure hash is NULL to avoid conflicts later */ 653 | if (hash == NGX_CONF_UNSET_PTR) { 654 | hash = NULL; 655 | } 656 | 657 | /* save the sticky parameters */ 658 | sticky_conf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_sticky_module); 659 | sticky_conf->cookie_name = name; 660 | sticky_conf->cookie_domain = domain; 661 | sticky_conf->cookie_path = path; 662 | sticky_conf->cookie_expires = expires; 663 | sticky_conf->cookie_secure = secure; 664 | sticky_conf->cookie_httponly = httponly; 665 | sticky_conf->hash = hash; 666 | sticky_conf->hmac = hmac; 667 | sticky_conf->text = text; 668 | sticky_conf->hmac_key = hmac_key; 669 | sticky_conf->no_fallback = no_fallback; 670 | sticky_conf->peers = NULL; /* ensure it's null before running */ 671 | 672 | upstream_conf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); 673 | 674 | /* 675 | * ensure another upstream module has not been already loaded 676 | * peer.init_upstream is set to null and the upstream module use RR if not set 677 | * But this check only works when the other module is declared before sticky 678 | */ 679 | if (upstream_conf->peer.init_upstream) { 680 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "You can't use sticky with another upstream module"); 681 | return NGX_CONF_ERROR; 682 | } 683 | 684 | /* configure the upstream to get back to this module */ 685 | upstream_conf->peer.init_upstream = ngx_http_init_upstream_sticky; 686 | 687 | upstream_conf->flags = NGX_HTTP_UPSTREAM_CREATE 688 | | NGX_HTTP_UPSTREAM_MAX_FAILS 689 | | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT 690 | | NGX_HTTP_UPSTREAM_DOWN 691 | | NGX_HTTP_UPSTREAM_WEIGHT; 692 | 693 | return NGX_CONF_OK; 694 | } 695 | 696 | /* 697 | * alloc stick configuration 698 | */ 699 | static void *ngx_http_sticky_create_conf(ngx_conf_t *cf) 700 | { 701 | ngx_http_sticky_srv_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_sticky_srv_conf_t)); 702 | if (conf == NULL) { 703 | return NGX_CONF_ERROR; 704 | } 705 | 706 | return conf; 707 | } 708 | -------------------------------------------------------------------------------- /patches/nginx-sticky-module-issue-24.patch: -------------------------------------------------------------------------------- 1 | Index: ngx_http_sticky_misc.h 2 | =================================================================== 3 | --- ngx_http_sticky_misc.h (revision 49) 4 | +++ ngx_http_sticky_misc.h (working copy) 5 | @@ -15,7 +15,7 @@ 6 | typedef ngx_int_t (*ngx_http_sticky_misc_hmac_pt)(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); 7 | typedef ngx_int_t (*ngx_http_sticky_misc_text_pt)(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); 8 | 9 | -ngx_int_t ngx_http_sticky_misc_set_cookie (ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires); 10 | +ngx_int_t ngx_http_sticky_misc_set_cookie (ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires, unsigned secure, unsigned httponly); 11 | ngx_int_t ngx_http_sticky_misc_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); 12 | ngx_int_t ngx_http_sticky_misc_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); 13 | ngx_int_t ngx_http_sticky_misc_hmac_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); 14 | Index: t/basic.t 15 | =================================================================== 16 | --- t/basic.t (revision 0) 17 | +++ t/basic.t (revision 0) 18 | @@ -0,0 +1,350 @@ 19 | +# vi:filetype= 20 | + 21 | +use lib 'lib'; 22 | +use Test::Nginx::Socket; # 'no_plan'; 23 | +use URI::Escape; 24 | + 25 | +repeat_each(1); 26 | + 27 | +plan tests => repeat_each() * 2 * blocks(); 28 | + 29 | +run_tests(); 30 | + 31 | +__DATA__ 32 | + 33 | +=== TEST 1: hash=md5 34 | +--- http_config 35 | + upstream backend { 36 | + server localhost:$TEST_NGINX_SERVER_PORT; 37 | + server 127.0.0.2:80; 38 | + server 127.0.0.3:80; 39 | + server 127.0.0.4:80; 40 | + server 127.0.0.5:80; 41 | + sticky name=route hash=md5; 42 | + } 43 | +--- config 44 | + location /backend { 45 | + rewrite /backend /frontend break; 46 | + proxy_pass http://backend; 47 | + proxy_set_header Host $host; 48 | + } 49 | + location /frontend { 50 | + echo -n $echo_client_request_headers; 51 | + } 52 | +--- request 53 | +GET /backend 54 | +--- response_headers 55 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92 56 | + 57 | +=== TEST 2: hash=sha1 58 | +--- http_config 59 | + upstream backend { 60 | + server localhost:$TEST_NGINX_SERVER_PORT; 61 | + server 127.0.0.2:80; 62 | + server 127.0.0.3:80; 63 | + server 127.0.0.4:80; 64 | + server 127.0.0.5:80; 65 | + sticky name=route hash=sha1; 66 | + } 67 | +--- config 68 | + location /backend { 69 | + rewrite /backend /frontend break; 70 | + proxy_pass http://backend; 71 | + proxy_set_header Host $host; 72 | + } 73 | + location /frontend { 74 | + echo -n $echo_client_request_headers; 75 | + } 76 | +--- request 77 | +GET /backend 78 | +--- response_headers 79 | +Set-Cookie: route=4262de333a18749d31416a617184734678797276 80 | + 81 | +=== TEST 3: hmac=md5 82 | +--- http_config 83 | + upstream backend { 84 | + server localhost:$TEST_NGINX_SERVER_PORT; 85 | + server 127.0.0.2:80; 86 | + server 127.0.0.3:80; 87 | + server 127.0.0.4:80; 88 | + server 127.0.0.5:80; 89 | + sticky name=route hmac=md5 hmac_key=secret; 90 | + } 91 | +--- config 92 | + location /backend { 93 | + rewrite /backend /frontend break; 94 | + proxy_pass http://backend; 95 | + proxy_set_header Host $host; 96 | + } 97 | + location /frontend { 98 | + echo -n $echo_client_request_headers; 99 | + } 100 | +--- request 101 | +GET /backend 102 | +--- response_headers 103 | +Set-Cookie: route=d20fd0a9eb6864058781ed6104e4c9fd 104 | + 105 | +=== TEST 4: hmac=sha1 106 | +--- http_config 107 | + upstream backend { 108 | + server localhost:$TEST_NGINX_SERVER_PORT; 109 | + server 127.0.0.2:80; 110 | + server 127.0.0.3:80; 111 | + server 127.0.0.4:80; 112 | + server 127.0.0.5:80; 113 | + sticky name=route hmac=sha1 hmac_key=secret; 114 | + } 115 | +--- config 116 | + location /backend { 117 | + rewrite /backend /frontend break; 118 | + proxy_pass http://backend; 119 | + proxy_set_header Host $host; 120 | + } 121 | + location /frontend { 122 | + echo -n $echo_client_request_headers; 123 | + } 124 | +--- request 125 | +GET /backend 126 | +--- response_headers 127 | +Set-Cookie: route=34734c8d4b451151897b62db281c0b055e035adc 128 | + 129 | +=== TEST 5: hmac=sha1 hmac_key=secret2 130 | +--- http_config 131 | + upstream backend { 132 | + server localhost:$TEST_NGINX_SERVER_PORT; 133 | + server 127.0.0.2:80; 134 | + server 127.0.0.3:80; 135 | + server 127.0.0.4:80; 136 | + server 127.0.0.5:80; 137 | + sticky name=route hmac=sha1 hmac_key=secret2; 138 | + } 139 | +--- config 140 | + location /backend { 141 | + rewrite /backend /frontend break; 142 | + proxy_pass http://backend; 143 | + proxy_set_header Host $host; 144 | + } 145 | + location /frontend { 146 | + echo -n $echo_client_request_headers; 147 | + } 148 | +--- request 149 | +GET /backend 150 | +--- response_headers 151 | +Set-Cookie: route=959bfc3973750a198925a3aedf9570f5d9b7e6f8 152 | + 153 | +=== TEST 6: domain=.example.com 154 | +--- http_config 155 | + upstream backend { 156 | + server localhost:$TEST_NGINX_SERVER_PORT; 157 | + server 127.0.0.2:80; 158 | + server 127.0.0.3:80; 159 | + server 127.0.0.4:80; 160 | + server 127.0.0.5:80; 161 | + sticky domain=.example.com; 162 | + } 163 | +--- config 164 | + location /backend { 165 | + rewrite /backend /frontend break; 166 | + proxy_pass http://backend; 167 | + proxy_set_header Host $host; 168 | + } 169 | + location /frontend { 170 | + echo -n $echo_client_request_headers; 171 | + } 172 | +--- request 173 | +GET /backend 174 | +--- response_headers 175 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Domain=.example.com 176 | + 177 | +=== TEST 7: path=/example 178 | +--- http_config 179 | + upstream backend { 180 | + server localhost:$TEST_NGINX_SERVER_PORT; 181 | + server 127.0.0.2:80; 182 | + server 127.0.0.3:80; 183 | + server 127.0.0.4:80; 184 | + server 127.0.0.5:80; 185 | + sticky path=/example; 186 | + } 187 | +--- config 188 | + location /backend { 189 | + rewrite /backend /frontend break; 190 | + proxy_pass http://backend; 191 | + proxy_set_header Host $host; 192 | + } 193 | + location /frontend { 194 | + echo -n $echo_client_request_headers; 195 | + } 196 | +--- request 197 | +GET /backend 198 | +--- response_headers 199 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Path=/example 200 | + 201 | +=== TEST 8: expires=1h 202 | +--- http_config 203 | + upstream backend { 204 | + server localhost:$TEST_NGINX_SERVER_PORT; 205 | + server 127.0.0.2:80; 206 | + server 127.0.0.3:80; 207 | + server 127.0.0.4:80; 208 | + server 127.0.0.5:80; 209 | + sticky expires=1h; 210 | + } 211 | +--- config 212 | + location /backend { 213 | + rewrite /backend /frontend break; 214 | + proxy_pass http://backend; 215 | + proxy_set_header Host $host; 216 | + } 217 | + location /frontend { 218 | + echo -n $echo_client_request_headers; 219 | + } 220 | +--- request 221 | +GET /backend 222 | +--- response_headers 223 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Max-Age=3600 224 | + 225 | +=== TEST 9: text=md5 226 | +--- http_config 227 | + upstream backend { 228 | + server localhost:$TEST_NGINX_SERVER_PORT; 229 | + server 127.0.0.2:80; 230 | + server 127.0.0.3:80; 231 | + server 127.0.0.4:80; 232 | + server 127.0.0.5:80; 233 | + sticky name=route text=md5; 234 | + } 235 | +--- config 236 | + location /backend { 237 | + rewrite /backend /frontend break; 238 | + proxy_pass http://backend; 239 | + proxy_set_header Host $host; 240 | + } 241 | + location /frontend { 242 | + echo -n $echo_client_request_headers; 243 | + } 244 | +--- request 245 | +GET /backend 246 | +--- response_headers 247 | +Set-Cookie: route=f6082e846954099610d58161bf189f37 248 | + 249 | +=== TEST 10: text=sha1 250 | +--- http_config 251 | + upstream backend { 252 | + server localhost:$TEST_NGINX_SERVER_PORT; 253 | + server 127.0.0.2:80; 254 | + server 127.0.0.3:80; 255 | + server 127.0.0.4:80; 256 | + server 127.0.0.5:80; 257 | + sticky name=route text=sha1; 258 | + } 259 | +--- config 260 | + location /backend { 261 | + rewrite /backend /frontend break; 262 | + proxy_pass http://backend; 263 | + proxy_set_header Host $host; 264 | + } 265 | + location /frontend { 266 | + echo -n $echo_client_request_headers; 267 | + } 268 | +--- request 269 | +GET /backend 270 | +--- response_headers 271 | +Set-Cookie: route=17305d40bf37f65329da1850efddd32840891e32 272 | + 273 | +=== TEST 11: text=raw 274 | +--- http_config 275 | + upstream backend { 276 | + server localhost:$TEST_NGINX_SERVER_PORT; 277 | + server 127.0.0.2:80; 278 | + server 127.0.0.3:80; 279 | + server 127.0.0.4:80; 280 | + server 127.0.0.5:80; 281 | + sticky name=route text=raw; 282 | + } 283 | +--- config 284 | + location /backend { 285 | + rewrite /backend /frontend break; 286 | + proxy_pass http://backend; 287 | + proxy_set_header Host $host; 288 | + } 289 | + location /frontend { 290 | + echo -n $echo_client_request_headers; 291 | + } 292 | +--- request 293 | +GET /backend 294 | +--- response_headers 295 | +Set-Cookie: route=127.0.0.1:1984 296 | + 297 | +=== TEST 12: no_fallback 298 | +--- http_config 299 | + upstream backend { 300 | + server localhost:$TEST_NGINX_SERVER_PORT; 301 | + server 127.0.0.2:80; 302 | + server 127.0.0.3:80; 303 | + server 127.0.0.4:80; 304 | + server 127.0.0.5:80; 305 | + sticky no_fallback; 306 | + } 307 | +--- config 308 | + location /backend { 309 | + rewrite /backend /frontend break; 310 | + proxy_pass http://backend; 311 | + proxy_set_header Host $host; 312 | + } 313 | + location /frontend { 314 | + echo -n $echo_client_request_headers; 315 | + } 316 | +--- request 317 | +GET /backend 318 | +--- response_headers 319 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92 320 | + 321 | +=== TEST 13: secure 322 | +--- http_config 323 | + upstream backend { 324 | + server localhost:$TEST_NGINX_SERVER_PORT; 325 | + server 127.0.0.2:80; 326 | + server 127.0.0.3:80; 327 | + server 127.0.0.4:80; 328 | + server 127.0.0.5:80; 329 | + sticky secure; 330 | + } 331 | +--- config 332 | + location /backend { 333 | + rewrite /backend /frontend break; 334 | + proxy_pass http://backend; 335 | + proxy_set_header Host $host; 336 | + } 337 | + location /frontend { 338 | + echo -n $echo_client_request_headers; 339 | + } 340 | +--- request 341 | +GET /backend 342 | +--- response_headers 343 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Secure 344 | + 345 | +=== TEST 14: httponly 346 | +--- http_config 347 | + upstream backend { 348 | + server localhost:$TEST_NGINX_SERVER_PORT; 349 | + server 127.0.0.2:80; 350 | + server 127.0.0.3:80; 351 | + server 127.0.0.4:80; 352 | + server 127.0.0.5:80; 353 | + sticky httponly; 354 | + } 355 | +--- config 356 | + location /backend { 357 | + rewrite /backend /frontend break; 358 | + proxy_pass http://backend; 359 | + proxy_set_header Host $host; 360 | + } 361 | + location /frontend { 362 | + echo -n $echo_client_request_headers; 363 | + } 364 | +--- request 365 | +GET /backend 366 | +--- response_headers 367 | +Set-Cookie: route=908c1a9fb15095f454c085282da20d92; HttpOnly 368 | + 369 | Index: ngx_http_sticky_module.c 370 | =================================================================== 371 | --- ngx_http_sticky_module.c (revision 49) 372 | +++ ngx_http_sticky_module.c (working copy) 373 | @@ -23,6 +23,8 @@ 374 | ngx_str_t cookie_domain; 375 | ngx_str_t cookie_path; 376 | time_t cookie_expires; 377 | + unsigned cookie_secure:1; 378 | + unsigned cookie_httponly:1; 379 | ngx_str_t hmac_key; 380 | ngx_http_sticky_misc_hash_pt hash; 381 | ngx_http_sticky_misc_hmac_pt hmac; 382 | @@ -358,7 +360,7 @@ 383 | 384 | if (iphp->rrp.peers->peer[i].sockaddr == pc->sockaddr && iphp->rrp.peers->peer[i].socklen == pc->socklen) { 385 | if (conf->hash || conf->hmac || conf->text) { 386 | - ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &conf->peers[i].digest, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires); 387 | + ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &conf->peers[i].digest, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires, conf->cookie_secure, conf->cookie_httponly); 388 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] set cookie \"%V\" value=\"%V\" index=%ui", &conf->cookie_name, &conf->peers[i].digest, i); 389 | } else { 390 | ngx_str_t route; 391 | @@ -373,7 +375,7 @@ 392 | } 393 | ngx_snprintf(route.data, route.len, "%d", i); 394 | route.len = ngx_strlen(route.data); 395 | - ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &route, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires); 396 | + ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &route, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires, conf->cookie_secure, conf->cookie_httponly); 397 | ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] set cookie \"%V\" value=\"%V\" index=%ui", &conf->cookie_name, &tmp, i); 398 | } 399 | break; /* found and hopefully the cookie have been set */ 400 | @@ -401,6 +403,8 @@ 401 | ngx_str_t path = ngx_string(""); 402 | ngx_str_t hmac_key = ngx_string(""); 403 | time_t expires = NGX_CONF_UNSET; 404 | + unsigned secure = 0; 405 | + unsigned httponly = 0; 406 | ngx_http_sticky_misc_hash_pt hash = NGX_CONF_UNSET_PTR; 407 | ngx_http_sticky_misc_hmac_pt hmac = NULL; 408 | ngx_http_sticky_misc_text_pt text = NULL; 409 | @@ -477,6 +481,16 @@ 410 | continue; 411 | } 412 | 413 | + if (ngx_strncmp(value[i].data, "secure", 6) == 0 && value[i].len == 6) { 414 | + secure = 1; 415 | + continue; 416 | + } 417 | + 418 | + if (ngx_strncmp(value[i].data, "httponly", 8) == 0 && value[i].len == 8) { 419 | + httponly = 1; 420 | + continue; 421 | + } 422 | + 423 | /* is "text=" is starting the argument ? */ 424 | if ((u_char *)ngx_strstr(value[i].data, "text=") == value[i].data) { 425 | 426 | @@ -646,6 +660,8 @@ 427 | sticky_conf->cookie_domain = domain; 428 | sticky_conf->cookie_path = path; 429 | sticky_conf->cookie_expires = expires; 430 | + sticky_conf->cookie_secure = secure; 431 | + sticky_conf->cookie_httponly = httponly; 432 | sticky_conf->hash = hash; 433 | sticky_conf->hmac = hmac; 434 | sticky_conf->text = text; 435 | Index: ngx_http_sticky_misc.c 436 | =================================================================== 437 | --- ngx_http_sticky_misc.c (revision 49) 438 | +++ ngx_http_sticky_misc.c (working copy) 439 | @@ -16,7 +16,7 @@ 440 | #define ngx_str_set(str, text) (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text 441 | #endif 442 | 443 | -ngx_int_t ngx_http_sticky_misc_set_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires) 444 | +ngx_int_t ngx_http_sticky_misc_set_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires, unsigned secure, unsigned httponly) 445 | { 446 | u_char *cookie, *p; 447 | size_t len; 448 | @@ -48,6 +48,16 @@ 449 | len += sizeof("; Path=") - 1 + path->len; 450 | } 451 | 452 | + /* ; Secure */ 453 | + if (secure) { 454 | + len += sizeof("; Secure") - 1; 455 | + } 456 | + 457 | + /* ; HttpOnly */ 458 | + if (httponly) { 459 | + len += sizeof("; HttpOnly") - 1; 460 | + } 461 | + 462 | cookie = ngx_pnalloc(r->pool, len); 463 | if (cookie == NULL) { 464 | return NGX_ERROR; 465 | @@ -72,6 +82,14 @@ 466 | p = ngx_copy(p, path->data, path->len); 467 | } 468 | 469 | + if (secure) { 470 | + p = ngx_copy(p, "; Secure", sizeof("; Secure") - 1); 471 | + } 472 | + 473 | + if (httponly) { 474 | + p = ngx_copy(p, "; HttpOnly", sizeof("; HttpOnly") - 1); 475 | + } 476 | + 477 | part = &r->headers_out.headers.part; 478 | elt = part->elts; 479 | set_cookie = NULL; 480 | -------------------------------------------------------------------------------- /patches/nginx-sticky-module-issue-31.patch: -------------------------------------------------------------------------------- 1 | diff -ur nginx-sticky-module-1.1.orig/ngx_http_sticky_misc.c nginx-sticky-module-1.1/ngx_http_sticky_misc.c 2 | --- nginx-sticky-module-1.1.orig/ngx_http_sticky_misc.c 2012-09-24 09:48:21.000000000 -0700 3 | +++ nginx-sticky-module-1.1/ngx_http_sticky_misc.c 2013-11-08 14:03:41.200018436 -0800 4 | @@ -16,6 +16,16 @@ 5 | #define ngx_str_set(str, text) (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text 6 | #endif 7 | 8 | +static cookie_expires(char *str, size_t size, time_t t) 9 | +{ 10 | + char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; 11 | + char *wdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; 12 | + struct tm e; 13 | + gmtime_r(&t, &e); 14 | + return snprintf(str, size, "%s, %02d-%s-%04d %02d:%02d:%02d GMT", 15 | + wdays[e.tm_wday], e.tm_mday, months[e.tm_mon], e.tm_year + 1900, e.tm_hour,e.tm_min,e.tm_sec); 16 | +} 17 | + 18 | ngx_int_t ngx_http_sticky_misc_set_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires) 19 | { 20 | u_char *cookie, *p; 21 | @@ -24,6 +34,8 @@ 22 | ngx_str_t remove; 23 | ngx_list_part_t *part; 24 | ngx_uint_t i; 25 | + char expires_str[80]; 26 | + int expires_len; 27 | 28 | if (value == NULL) { 29 | ngx_str_set(&remove, "_remove_"); 30 | @@ -38,9 +50,10 @@ 31 | len += sizeof("; Domain=") - 1 + domain->len; 32 | } 33 | 34 | - /*; Max-Age= */ 35 | + /*; Expires= */ 36 | if (expires != NGX_CONF_UNSET) { 37 | - len += sizeof("; Max-Age=") - 1 + NGX_TIME_T_LEN; 38 | + expires_len = cookie_expires(expires_str, sizeof(expires_str), time(NULL) + expires); 39 | + len += sizeof("; Expires=") - 1 + expires_len; 40 | } 41 | 42 | /* ; Path= */ 43 | @@ -63,8 +76,8 @@ 44 | } 45 | 46 | if (expires != NGX_CONF_UNSET) { 47 | - p = ngx_copy(p, "; Max-Age=", sizeof("; Max-Age=") - 1); 48 | - p = ngx_snprintf(p, NGX_TIME_T_LEN, "%T", expires); 49 | + p = ngx_copy(p, "; Expires=", sizeof("; Expires=") - 1); 50 | + p = ngx_copy(p, expires_str, expires_len); 51 | } 52 | 53 | if (path->len > 0) { 54 | -------------------------------------------------------------------------------- /patches/nginx-sticky-module-issue-33.patch: -------------------------------------------------------------------------------- 1 | --- nginx-sticky-module-read-only/ngx_http_sticky_misc.c Tue Apr 1 23:58:58 2014 2 | +++ nginx-sticky-module-nginx-sticky-module-1.2/ngx_http_sticky_misc.c Thu Feb 6 09:48:56 2014 3 | @@ -278,8 +278,11 @@ 4 | if (digest->data == NULL) { 5 | return NGX_ERROR; 6 | } 7 | +#if defined(nginx_version) && nginx_version >= 1005003 8 | + digest->len = ngx_sock_ntop(in, sizeof(struct sockaddr_in), digest->data, len, 1); 9 | +#else 10 | digest->len = ngx_sock_ntop(in, digest->data, len, 1); 11 | - return NGX_OK; 12 | +#endif 13 | return NGX_OK; 14 | } 15 | -------------------------------------------------------------------------------- /t/basic.t: -------------------------------------------------------------------------------- 1 | # vi:filetype= 2 | 3 | use lib 'lib'; 4 | use Test::Nginx::Socket; # 'no_plan'; 5 | use URI::Escape; 6 | 7 | repeat_each(1); 8 | 9 | plan tests => repeat_each() * 2 * blocks(); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: hash=md5 16 | --- http_config 17 | upstream backend { 18 | server localhost:$TEST_NGINX_SERVER_PORT; 19 | server 127.0.0.2:80; 20 | server 127.0.0.3:80; 21 | server 127.0.0.4:80; 22 | server 127.0.0.5:80; 23 | sticky name=route hash=md5; 24 | } 25 | --- config 26 | location /backend { 27 | rewrite /backend /frontend break; 28 | proxy_pass http://backend; 29 | proxy_set_header Host $host; 30 | } 31 | location /frontend { 32 | echo -n $echo_client_request_headers; 33 | } 34 | --- request 35 | GET /backend 36 | --- response_headers 37 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92 38 | 39 | === TEST 2: hash=sha1 40 | --- http_config 41 | upstream backend { 42 | server localhost:$TEST_NGINX_SERVER_PORT; 43 | server 127.0.0.2:80; 44 | server 127.0.0.3:80; 45 | server 127.0.0.4:80; 46 | server 127.0.0.5:80; 47 | sticky name=route hash=sha1; 48 | } 49 | --- config 50 | location /backend { 51 | rewrite /backend /frontend break; 52 | proxy_pass http://backend; 53 | proxy_set_header Host $host; 54 | } 55 | location /frontend { 56 | echo -n $echo_client_request_headers; 57 | } 58 | --- request 59 | GET /backend 60 | --- response_headers 61 | Set-Cookie: route=4262de333a18749d31416a617184734678797276 62 | 63 | === TEST 3: hmac=md5 64 | --- http_config 65 | upstream backend { 66 | server localhost:$TEST_NGINX_SERVER_PORT; 67 | server 127.0.0.2:80; 68 | server 127.0.0.3:80; 69 | server 127.0.0.4:80; 70 | server 127.0.0.5:80; 71 | sticky name=route hmac=md5 hmac_key=secret; 72 | } 73 | --- config 74 | location /backend { 75 | rewrite /backend /frontend break; 76 | proxy_pass http://backend; 77 | proxy_set_header Host $host; 78 | } 79 | location /frontend { 80 | echo -n $echo_client_request_headers; 81 | } 82 | --- request 83 | GET /backend 84 | --- response_headers 85 | Set-Cookie: route=d20fd0a9eb6864058781ed6104e4c9fd 86 | 87 | === TEST 4: hmac=sha1 88 | --- http_config 89 | upstream backend { 90 | server localhost:$TEST_NGINX_SERVER_PORT; 91 | server 127.0.0.2:80; 92 | server 127.0.0.3:80; 93 | server 127.0.0.4:80; 94 | server 127.0.0.5:80; 95 | sticky name=route hmac=sha1 hmac_key=secret; 96 | } 97 | --- config 98 | location /backend { 99 | rewrite /backend /frontend break; 100 | proxy_pass http://backend; 101 | proxy_set_header Host $host; 102 | } 103 | location /frontend { 104 | echo -n $echo_client_request_headers; 105 | } 106 | --- request 107 | GET /backend 108 | --- response_headers 109 | Set-Cookie: route=34734c8d4b451151897b62db281c0b055e035adc 110 | 111 | === TEST 5: hmac=sha1 hmac_key=secret2 112 | --- http_config 113 | upstream backend { 114 | server localhost:$TEST_NGINX_SERVER_PORT; 115 | server 127.0.0.2:80; 116 | server 127.0.0.3:80; 117 | server 127.0.0.4:80; 118 | server 127.0.0.5:80; 119 | sticky name=route hmac=sha1 hmac_key=secret2; 120 | } 121 | --- config 122 | location /backend { 123 | rewrite /backend /frontend break; 124 | proxy_pass http://backend; 125 | proxy_set_header Host $host; 126 | } 127 | location /frontend { 128 | echo -n $echo_client_request_headers; 129 | } 130 | --- request 131 | GET /backend 132 | --- response_headers 133 | Set-Cookie: route=959bfc3973750a198925a3aedf9570f5d9b7e6f8 134 | 135 | === TEST 6: domain=.example.com 136 | --- http_config 137 | upstream backend { 138 | server localhost:$TEST_NGINX_SERVER_PORT; 139 | server 127.0.0.2:80; 140 | server 127.0.0.3:80; 141 | server 127.0.0.4:80; 142 | server 127.0.0.5:80; 143 | sticky domain=.example.com; 144 | } 145 | --- config 146 | location /backend { 147 | rewrite /backend /frontend break; 148 | proxy_pass http://backend; 149 | proxy_set_header Host $host; 150 | } 151 | location /frontend { 152 | echo -n $echo_client_request_headers; 153 | } 154 | --- request 155 | GET /backend 156 | --- response_headers 157 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Domain=.example.com 158 | 159 | === TEST 7: path=/example 160 | --- http_config 161 | upstream backend { 162 | server localhost:$TEST_NGINX_SERVER_PORT; 163 | server 127.0.0.2:80; 164 | server 127.0.0.3:80; 165 | server 127.0.0.4:80; 166 | server 127.0.0.5:80; 167 | sticky path=/example; 168 | } 169 | --- config 170 | location /backend { 171 | rewrite /backend /frontend break; 172 | proxy_pass http://backend; 173 | proxy_set_header Host $host; 174 | } 175 | location /frontend { 176 | echo -n $echo_client_request_headers; 177 | } 178 | --- request 179 | GET /backend 180 | --- response_headers 181 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Path=/example 182 | 183 | === TEST 8: expires=1h 184 | --- http_config 185 | upstream backend { 186 | server localhost:$TEST_NGINX_SERVER_PORT; 187 | server 127.0.0.2:80; 188 | server 127.0.0.3:80; 189 | server 127.0.0.4:80; 190 | server 127.0.0.5:80; 191 | sticky expires=1h; 192 | } 193 | --- config 194 | location /backend { 195 | rewrite /backend /frontend break; 196 | proxy_pass http://backend; 197 | proxy_set_header Host $host; 198 | } 199 | location /frontend { 200 | echo -n $echo_client_request_headers; 201 | } 202 | --- request 203 | GET /backend 204 | --- response_headers 205 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Max-Age=3600 206 | 207 | === TEST 9: text=md5 208 | --- http_config 209 | upstream backend { 210 | server localhost:$TEST_NGINX_SERVER_PORT; 211 | server 127.0.0.2:80; 212 | server 127.0.0.3:80; 213 | server 127.0.0.4:80; 214 | server 127.0.0.5:80; 215 | sticky name=route text=md5; 216 | } 217 | --- config 218 | location /backend { 219 | rewrite /backend /frontend break; 220 | proxy_pass http://backend; 221 | proxy_set_header Host $host; 222 | } 223 | location /frontend { 224 | echo -n $echo_client_request_headers; 225 | } 226 | --- request 227 | GET /backend 228 | --- response_headers 229 | Set-Cookie: route=f6082e846954099610d58161bf189f37 230 | 231 | === TEST 10: text=sha1 232 | --- http_config 233 | upstream backend { 234 | server localhost:$TEST_NGINX_SERVER_PORT; 235 | server 127.0.0.2:80; 236 | server 127.0.0.3:80; 237 | server 127.0.0.4:80; 238 | server 127.0.0.5:80; 239 | sticky name=route text=sha1; 240 | } 241 | --- config 242 | location /backend { 243 | rewrite /backend /frontend break; 244 | proxy_pass http://backend; 245 | proxy_set_header Host $host; 246 | } 247 | location /frontend { 248 | echo -n $echo_client_request_headers; 249 | } 250 | --- request 251 | GET /backend 252 | --- response_headers 253 | Set-Cookie: route=17305d40bf37f65329da1850efddd32840891e32 254 | 255 | === TEST 11: text=raw 256 | --- http_config 257 | upstream backend { 258 | server localhost:$TEST_NGINX_SERVER_PORT; 259 | server 127.0.0.2:80; 260 | server 127.0.0.3:80; 261 | server 127.0.0.4:80; 262 | server 127.0.0.5:80; 263 | sticky name=route text=raw; 264 | } 265 | --- config 266 | location /backend { 267 | rewrite /backend /frontend break; 268 | proxy_pass http://backend; 269 | proxy_set_header Host $host; 270 | } 271 | location /frontend { 272 | echo -n $echo_client_request_headers; 273 | } 274 | --- request 275 | GET /backend 276 | --- response_headers 277 | Set-Cookie: route=127.0.0.1:1984 278 | 279 | === TEST 12: no_fallback 280 | --- http_config 281 | upstream backend { 282 | server localhost:$TEST_NGINX_SERVER_PORT; 283 | server 127.0.0.2:80; 284 | server 127.0.0.3:80; 285 | server 127.0.0.4:80; 286 | server 127.0.0.5:80; 287 | sticky no_fallback; 288 | } 289 | --- config 290 | location /backend { 291 | rewrite /backend /frontend break; 292 | proxy_pass http://backend; 293 | proxy_set_header Host $host; 294 | } 295 | location /frontend { 296 | echo -n $echo_client_request_headers; 297 | } 298 | --- request 299 | GET /backend 300 | --- response_headers 301 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92 302 | 303 | === TEST 13: secure 304 | --- http_config 305 | upstream backend { 306 | server localhost:$TEST_NGINX_SERVER_PORT; 307 | server 127.0.0.2:80; 308 | server 127.0.0.3:80; 309 | server 127.0.0.4:80; 310 | server 127.0.0.5:80; 311 | sticky secure; 312 | } 313 | --- config 314 | location /backend { 315 | rewrite /backend /frontend break; 316 | proxy_pass http://backend; 317 | proxy_set_header Host $host; 318 | } 319 | location /frontend { 320 | echo -n $echo_client_request_headers; 321 | } 322 | --- request 323 | GET /backend 324 | --- response_headers 325 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92; Secure 326 | 327 | === TEST 14: httponly 328 | --- http_config 329 | upstream backend { 330 | server localhost:$TEST_NGINX_SERVER_PORT; 331 | server 127.0.0.2:80; 332 | server 127.0.0.3:80; 333 | server 127.0.0.4:80; 334 | server 127.0.0.5:80; 335 | sticky httponly; 336 | } 337 | --- config 338 | location /backend { 339 | rewrite /backend /frontend break; 340 | proxy_pass http://backend; 341 | proxy_set_header Host $host; 342 | } 343 | location /frontend { 344 | echo -n $echo_client_request_headers; 345 | } 346 | --- request 347 | GET /backend 348 | --- response_headers 349 | Set-Cookie: route=908c1a9fb15095f454c085282da20d92; HttpOnly 350 | 351 | --------------------------------------------------------------------------------