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