├── LICENSE ├── README.md ├── README ├── client-wss.jpg └── nginx-wss.jpg ├── config ├── nginx_test ├── README.md ├── test_websocket.html └── websocket_test.conf ├── ngx_http_set_header.c ├── ngx_http_set_header.h ├── ngx_websocket.c ├── ngx_websocket.h ├── ngx_websocket_handler.c ├── ngx_websocket_module.c └── t ├── config └── ngx_websocket_echo_module.c /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, pingostack 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX-based Websocket Server 2 | 3 | ## nginx-websocket-module 4 | 5 | ### Project blog 6 | 7 | [http://pingos.io/](http://pingos.io/) 8 | 9 | * wss client 10 | ![client-wss](./README/client-wss.jpg) 11 | 12 | * nginx-websocket-module supports wss protocol 13 | ![nginx-wss](./README/nginx-wss.jpg) 14 | 15 | ## Code sample 16 | 17 | **If you want to know how to develop a websocket server, refer to the code in the ['t/ngx_websocket_echo_module.c'](t/ngx_websocket_echo_module.c) .** 18 | 19 | ## Build 20 | 21 | ```shell 22 | $ 23 | $ git clone https://github.com/nginx/nginx.git 24 | $ 25 | $ git clone https://github.com/im-pingo/nginx-websocket-module.git 26 | $ 27 | $ cd nginx 28 | $ 29 | $ ./auto/configure --add-module=../nginx-websocket-module --add-module=../nginx-websocket-module/t 30 | $ 31 | $ sudo make && sudo make install 32 | $ 33 | ``` 34 | 35 | ## Config file 36 | 37 | ### websocket 38 | 39 | * *syntax* : websocket [any] 40 | 41 | * *context*: location 42 | 43 | **The switch of websocket service has no args** 44 | 45 | ```nginx 46 | 47 | websocket out_queue=[num] message_length=[num] frame_length=[num] ping_interval=[time] timeout=[time]; 48 | 49 | ``` 50 | 51 | #### options 52 | 53 | ##### out_queue 54 | 55 | * *syntax*: out_queue=[num] (default 512) 56 | * *context*: websocket's arg 57 | 58 | **Number of out queue** 59 | 60 | ##### message_length 61 | 62 | * *syntax*: message_length=[num] (default 4096000 bytes) 63 | * *context*: websocket's arg 64 | 65 | **Max length of websocket message** 66 | 67 | ##### ping_interval 68 | 69 | * *syntax*: ping_interval=[msec] (default 5000ms) 70 | * *context*: websocket's arg 71 | 72 | **Time interval between pings** 73 | 74 | ##### timeout 75 | 76 | * *syntax*: timeout=[msec] (default 15000ms) 77 | * *context*: websocket's arg 78 | 79 | **receive timeout** 80 | 81 | ### websocket_echo 82 | 83 | * *syntax*: websocket_echo [no args] 84 | * *context*: location 85 | 86 | **The server responses the data it received** 87 | 88 | ```nginx 89 | 90 | websocket_echo; 91 | 92 | ``` 93 | 94 | ### Example nginx.conf 95 | 96 | ```nginx 97 | 98 | daemon on; 99 | master_process on; 100 | #user nobody; 101 | worker_processes 1; 102 | 103 | error_log logs/error.log info; 104 | 105 | pid logs/nginx.pid; 106 | events { 107 | worker_connections 1024; 108 | } 109 | 110 | 111 | http { 112 | include mime.types; 113 | default_type application/octet-stream; 114 | sendfile on; 115 | keepalive_timeout 65; 116 | 117 | server { 118 | listen 80; 119 | 120 | # use wss(ssl) 121 | listen 443 ssl; 122 | ssl_certificate /usr/local/nginx/key/im-pingo.crt; 123 | ssl_certificate_key /usr/local/nginx/key/im-pingo.key; 124 | 125 | server_name localhost; 126 | 127 | location /im-pingo { 128 | websocket out_queue=512 message_length=4096000 ping_interval=5000ms timeout=15s; 129 | websocket_echo; 130 | } 131 | } 132 | } 133 | 134 | ``` 135 | -------------------------------------------------------------------------------- /README/client-wss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingostack/nginx-websocket-module/207993fbe44f133ef832dc1b7f9a0d2751171341/README/client-wss.jpg -------------------------------------------------------------------------------- /README/nginx-wss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingostack/nginx-websocket-module/207993fbe44f133ef832dc1b7f9a0d2751171341/README/nginx-wss.jpg -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name="ngx_websocket_module" 2 | WEBSOCKET_MODULES=" \ 3 | ngx_websocket_module \ 4 | " 5 | WEBSOCKET_SRCS=" \ 6 | $ngx_addon_dir/ngx_websocket_module.c \ 7 | $ngx_addon_dir/ngx_http_set_header.c \ 8 | $ngx_addon_dir/ngx_websocket_handler.c \ 9 | $ngx_addon_dir/ngx_websocket.c \ 10 | " 11 | 12 | WEBSOCKET_DEPS=" \ 13 | #ngx_addon_dir/ngx_http_set_header.h \ 14 | #ngx_addon_dir/ngx_websocket.h \ 15 | " 16 | HTTP_MODULES="$HTTP_MODULES $WEBSOCKET_MODULES" 17 | 18 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $WEBSOCKET_SRCS" 19 | 20 | USE_OPENSSL=YES 21 | 22 | WEBSOCKET_LIBS="" 23 | 24 | NGX_ADDON_DEPS="$NGX_ADDON_DEPS $WEBSOCKET_DEPS" 25 | 26 | CORE_LIBS="$CORE_LIBS $WEBSOCKET_LIBS" 27 | CFLAGS="$CFLAGS -I $ngx_addon_dir" 28 | -------------------------------------------------------------------------------- /nginx_test/README.md: -------------------------------------------------------------------------------- 1 | # Nginx Test 2 | ## Browser-based Integration Test for Nginx Websocket 3 | 4 | This test exercises the websocket logic using the websocket client implementation native to modern browsers. 5 | In the test, we start an instance of Nginx with the websocket echo module loaded, then navigate using any 6 | modern browser to a web page containing javascript code that connects to that websocket and validates 7 | correct operation 8 | 9 | ### Quick Start for Linux using Bash shell 10 | Since there are many ways that the websocket modules could be compiled, this is less quick that we'd like. 11 | * Create a working directory - replace ${MY_HOME_OR_LOCAL_SCRATCH} with an appropriate directory: 12 | > cd ${MY_HOME_OR_LOCAL_SCRATCH}
13 | > mkdir nginx_temp
14 | > cd nginx_temp 15 | * Copy test files into working directory 16 | > cp ${PATH_TO_NGINX_WEBSOCKET_GIT_REPO}/nginx-test/* . 17 | * Symlink websocket modules into working directory (you could also copy, but symlinks mean you pick up changes following fixes) 18 | > ln -s ${PATH_TO_MODULES}/ngx_websocket_echo_module.so .
19 | > ln -s ${PATH_TO_MODULES}/ngx_websocket_module.so . 20 | > ln -s ${PATH_TO_NGINX_CONF}/mime.types . 21 | * Run Nginx 22 | > ${PATH_TO_NGINX}/nginx -p $(pwd) -c websocket_test.conf 23 | * If the execution fails because of missing shared object dependencies (e.g. zlib, pcre, openssl), add paths to these dependencies to LD_LIBRARY_PATH and rerun. 24 | 25 | ### Run the test 26 | * New open a browser and enter the URL of the test HTML page. For a browser running on the same host as Nginx 27 | > http://localhost:8220/test_websocket.html 28 | * If accessing remotely then replace localhost with the name of the host where Nginx is running. 29 | * The output of the test is shown in the browser. Scroll down if necessary to confirm that all tests are green. 30 | -------------------------------------------------------------------------------- /nginx_test/test_websocket.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Test 6 | 7 | 210 | 211 |

WebSocket Test

212 | 213 |
214 | 215 | 216 | -------------------------------------------------------------------------------- /nginx_test/websocket_test.conf: -------------------------------------------------------------------------------- 1 | load_module ngx_websocket_module.so; 2 | load_module ngx_websocket_echo_module.so; 3 | 4 | worker_processes 1; 5 | 6 | error_log logs/error.log debug; 7 | pid logs/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { 14 | include mime.types; 15 | default_type application/octet-stream; 16 | sendfile on; 17 | keepalive_timeout 65; 18 | 19 | access_log logs/access.log; 20 | 21 | client_body_temp_path temp; 22 | proxy_temp_path temp; 23 | fastcgi_temp_path temp; 24 | uwsgi_temp_path temp; 25 | scgi_temp_path temp; 26 | 27 | server { 28 | listen 8220; 29 | server_name localhost; 30 | 31 | root .; 32 | 33 | location /echo_small_buffer { 34 | websocket out_queue=512 message_length=4096 ping_interval=5000ms timeout=600s; 35 | websocket_echo; 36 | } 37 | 38 | location /echo { 39 | websocket out_queue=512 message_length=1024000 frame_length=4095 ping_interval=5000ms timeout=600s; 40 | websocket_echo; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ngx_http_set_header.c: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | typedef struct ngx_http_header_val_s ngx_http_header_val_t; 13 | 14 | typedef ngx_int_t (*ngx_http_set_header_pt)(ngx_http_request_t *r, 15 | ngx_http_header_val_t *hv, ngx_str_t *value); 16 | 17 | struct ngx_http_header_val_s { 18 | ngx_http_complex_value_t value; 19 | ngx_uint_t hash; 20 | ngx_str_t key; 21 | ngx_http_set_header_pt handler; 22 | ngx_uint_t offset; 23 | }; 24 | 25 | typedef struct { 26 | ngx_str_t name; 27 | ngx_uint_t offset; 28 | ngx_http_set_header_pt handler; 29 | 30 | } ngx_http_set_header_t; 31 | 32 | //TODO need fill all header set in future 33 | /* for header has no quick link in ngx_http_headers_out_t */ 34 | static ngx_int_t ngx_http_set_header_out_other(ngx_http_request_t *r, 35 | ngx_http_header_val_t *hv, ngx_str_t *value); 36 | /* for header has quick link like ngx_table_elt_t* in ngx_http_headers_out_t */ 37 | static ngx_int_t ngx_http_set_header_out_builtin(ngx_http_request_t *r, 38 | ngx_http_header_val_t *hv, ngx_str_t *value); 39 | /* for header has quick link like ngx_array_t in ngx_http_headers_out_t */ 40 | static ngx_int_t ngx_http_set_header_out_builtin_multi(ngx_http_request_t *r, 41 | ngx_http_header_val_t *hv, ngx_str_t *value); 42 | /* for header has quick link like ngx_table_elt_t* and 43 | * other attribute in ngx_http_headers_out_t defined below */ 44 | static ngx_int_t ngx_http_set_header_out_content_type(ngx_http_request_t *r, 45 | ngx_http_header_val_t *hv, ngx_str_t *value); 46 | 47 | static ngx_http_set_header_t ngx_http_set_header_out_handlers[] = { 48 | 49 | { ngx_string("Server"), 50 | offsetof(ngx_http_headers_out_t, server), 51 | ngx_http_set_header_out_builtin }, 52 | 53 | { ngx_string("Date"), 54 | offsetof(ngx_http_headers_out_t, date), 55 | ngx_http_set_header_out_builtin }, 56 | 57 | { ngx_string("Content-Type"), 58 | offsetof(ngx_http_headers_out_t, content_type), 59 | ngx_http_set_header_out_content_type }, 60 | 61 | { ngx_string("Cache-Control"), 62 | offsetof(ngx_http_headers_out_t, cache_control), 63 | ngx_http_set_header_out_builtin_multi }, 64 | 65 | { ngx_null_string, 0, ngx_http_set_header_out_other } 66 | }; 67 | 68 | static ngx_int_t 69 | ngx_http_set_header_out_helper(ngx_http_request_t *r, 70 | ngx_http_header_val_t *hv, ngx_str_t *value, unsigned no_create) 71 | { 72 | ngx_table_elt_t *h; 73 | ngx_list_part_t *part; 74 | ngx_uint_t i; 75 | 76 | part = &r->headers_out.headers.part; 77 | h = part->elts; 78 | 79 | for (i = 0; /* void */; ++i) { 80 | 81 | if (i >= part->nelts) { 82 | if (part->next == NULL) { 83 | break; 84 | } 85 | 86 | part = part->next; 87 | h = part->elts; 88 | i = 0; 89 | } 90 | 91 | if (h[i].hash != 0 92 | && h[i].key.len == hv->key.len 93 | && ngx_strncasecmp(hv->key.data, h[i].key.data, h[i].key.len) == 0) 94 | /* header has been set */ 95 | { 96 | h[i].value = *value; 97 | if (value->len == 0) { /* if value is empty, remove header */ 98 | h[i].hash = 0; 99 | } else { 100 | h[i].hash = hv->hash; 101 | } 102 | 103 | return NGX_OK; 104 | } 105 | } 106 | 107 | if (no_create && value->len == 0) { /* set header to empty but header cannot found */ 108 | return NGX_OK; 109 | } 110 | 111 | /* header has not been set, create it */ 112 | 113 | h = ngx_list_push(&r->headers_out.headers); 114 | 115 | if (h == NULL) { 116 | return NGX_ERROR; 117 | } 118 | 119 | h->hash = hv->hash; 120 | h->key = hv->key; 121 | h->value = *value; 122 | 123 | h->lowcase_key = ngx_pnalloc(r->pool, h->key.len); 124 | if (h->lowcase_key == NULL) { 125 | return NGX_ERROR; 126 | } 127 | 128 | ngx_strlow(h->lowcase_key, h->key.data, h->key.len); 129 | 130 | return NGX_OK; 131 | } 132 | 133 | static ngx_int_t 134 | ngx_http_set_header_out_other(ngx_http_request_t * r, 135 | ngx_http_header_val_t * hv,ngx_str_t * value) 136 | { 137 | return ngx_http_set_header_out_helper(r, hv, value, 0); 138 | } 139 | 140 | 141 | static ngx_int_t 142 | ngx_http_set_header_out_builtin(ngx_http_request_t *r, 143 | ngx_http_header_val_t *hv, ngx_str_t *value) 144 | { 145 | ngx_table_elt_t *h, **old; 146 | 147 | if (hv->offset) { 148 | old = (ngx_table_elt_t **) ((char *) &r->headers_out + hv->offset); 149 | } else { 150 | old = NULL; 151 | } 152 | 153 | if (old == NULL || *old == NULL) { 154 | /* user should use ngx_http_set_header_out_other but use this func to set header */ 155 | //TODO 156 | return ngx_http_set_header_out_helper(r, hv, value, 0); 157 | } 158 | 159 | h = *old; 160 | 161 | h->value = *value; 162 | if (value->len == 0) { /* if value is empty, remove header */ 163 | 164 | h->hash = 0; 165 | return NGX_OK; 166 | } 167 | 168 | h->hash = hv->hash; 169 | h->key = hv->key; 170 | 171 | return NGX_OK; 172 | } 173 | 174 | static ngx_int_t 175 | ngx_http_set_header_out_builtin_multi(ngx_http_request_t *r, 176 | ngx_http_header_val_t *hv, ngx_str_t *value) 177 | { 178 | ngx_array_t *pa; 179 | ngx_table_elt_t *ho, **ph; 180 | ngx_uint_t i; 181 | 182 | pa = (ngx_array_t *) ((char *) &r->headers_out + hv->offset); 183 | 184 | if (pa->elts == NULL) { 185 | if (ngx_array_init(pa, r->pool, 2, sizeof(ngx_table_elt_t *)) 186 | != NGX_OK) 187 | { 188 | return NGX_ERROR; 189 | } 190 | } 191 | 192 | if (pa->nelts > 0) { 193 | ph = pa->elts; 194 | for (i = 1; i < pa->nelts; i++) { /* clear old value */ 195 | ph[i]->hash = 0; 196 | ph[i]->value.len = 0; 197 | } 198 | 199 | ph[0]->value = *value; 200 | 201 | if (value->len == 0) { 202 | ph[0]->hash = 0; 203 | } else { 204 | ph[0]->hash = hv->hash; 205 | } 206 | 207 | return NGX_OK; 208 | } 209 | 210 | /* header does not set */ 211 | ph = ngx_array_push(pa); 212 | if (ph == NULL) { 213 | return NGX_ERROR; 214 | } 215 | 216 | ho = ngx_list_push(&r->headers_out.headers); 217 | if (ho == NULL) { 218 | return NGX_ERROR; 219 | } 220 | 221 | ho->value = *value; 222 | ho->hash = hv->hash; 223 | ho->key = hv->key; 224 | *ph = ho; 225 | 226 | return NGX_OK; 227 | } 228 | 229 | static ngx_int_t 230 | ngx_http_set_header_out_content_type(ngx_http_request_t *r, 231 | ngx_http_header_val_t *hv, ngx_str_t *value) 232 | { 233 | ngx_uint_t i; 234 | 235 | r->headers_out.content_type_len = value->len; 236 | 237 | for (i = 0; i < value->len; i++) { 238 | if (value->data[i] == ';') { 239 | r->headers_out.content_type_len = i; 240 | break; 241 | } 242 | } 243 | 244 | r->headers_out.content_type = *value; 245 | r->headers_out.content_type_hash = hv->hash; 246 | r->headers_out.content_type_lowcase = NULL; 247 | 248 | value->len = 0; 249 | 250 | return ngx_http_set_header_out_helper(r, hv, value, 1); 251 | } 252 | 253 | ngx_int_t 254 | ngx_http_set_header_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) 255 | { 256 | ngx_http_header_val_t hv; 257 | ngx_http_set_header_t *handlers = ngx_http_set_header_out_handlers; 258 | ngx_uint_t i; 259 | ngx_str_t v; 260 | 261 | hv.hash = ngx_hash_key_lc(key->data, key->len); 262 | hv.key = *key; 263 | 264 | hv.offset = 0; 265 | hv.handler = NULL; 266 | 267 | for (i = 0; handlers[i].name.len; ++i) { 268 | if (hv.key.len != handlers[i].name.len 269 | || ngx_strncasecmp(hv.key.data, handlers[i].name.data, 270 | handlers[i].name.len) != 0) 271 | { 272 | continue; 273 | } 274 | 275 | hv.offset = handlers[i].offset; 276 | hv.handler = handlers[i].handler; 277 | 278 | break; 279 | } 280 | 281 | if (handlers[i].name.len == 0 && handlers[i].handler) { 282 | hv.offset = handlers[i].offset; 283 | hv.handler = handlers[i].handler; 284 | } 285 | 286 | v = *value; 287 | 288 | return hv.handler(r, &hv, &v); 289 | } 290 | 291 | 292 | ngx_str_t * 293 | ngx_http_get_header_in(ngx_http_request_t *r, ngx_str_t *key) 294 | { 295 | ngx_table_elt_t *h; 296 | ngx_list_part_t *part; 297 | ngx_uint_t i; 298 | 299 | part = &r->headers_in.headers.part; 300 | h = part->elts; 301 | 302 | for (i = 0; /* void */; ++i) { 303 | 304 | if (i >= part->nelts) { 305 | if (part->next == NULL) { 306 | break; 307 | } 308 | 309 | part = part->next; 310 | h = part->elts; 311 | i = 0; 312 | } 313 | 314 | if (h[i].hash == 0) { 315 | continue; 316 | } 317 | 318 | if (h[i].key.len == key->len 319 | && ngx_strncasecmp(h[i].key.data, key->data, key->len) == 0) 320 | { 321 | return &h[i].value; 322 | } 323 | } 324 | 325 | return NULL; 326 | } 327 | -------------------------------------------------------------------------------- /ngx_http_set_header.h: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #ifndef _NGX_RTMP_HTTP_HEADER_OUT_H_INCLUDED_ 9 | #define _NGX_RTMP_HTTP_HEADER_OUT_H_INCLUDED_ 10 | 11 | #include 12 | #include 13 | 14 | ngx_int_t ngx_http_set_header_out(ngx_http_request_t *r, 15 | ngx_str_t *key, ngx_str_t *value); 16 | 17 | ngx_str_t *ngx_http_get_header_in(ngx_http_request_t *r, ngx_str_t *key); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /ngx_websocket.c: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #include "ngx_websocket.h" 9 | 10 | void * 11 | ngx_websocket_rmemcpy(void *dst, const void* src, size_t n) 12 | { 13 | u_char *d, *s; 14 | 15 | d = dst; 16 | s = (u_char*)src + n - 1; 17 | 18 | while(s >= (u_char*)src) { 19 | *d++ = *s--; 20 | } 21 | 22 | return dst; 23 | } 24 | -------------------------------------------------------------------------------- /ngx_websocket.h: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #ifndef __NGX_WEBSOCKET_H_INCLUDE__ 9 | #define __NGX_WEBSOCKET_H_INCLUDE__ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define NGX_WEBSOCKET_REC_CONF 0x00010000 18 | 19 | #define NGX_WEBSOCKET_MAX_CHUNK_HEADER 14 20 | 21 | #define NGX_WEBSOCKET_OPCODE_TEXT 0X01 22 | #define NGX_WEBSOCKET_OPCODE_BINARY 0X02 23 | #define NGX_WEBSOCKET_OPCODE_CLOSE 0X08 24 | #define NGX_WEBSOCKET_OPCODE_PING 0X09 25 | #define NGX_WEBSOCKET_OPCODE_PONG 0X0A 26 | 27 | typedef struct ngx_websocket_header_s ngx_websocket_header_t; 28 | typedef struct ngx_websocket_frame_s ngx_websocket_frame_t; 29 | typedef struct ngx_websocket_session_s ngx_websocket_session_t; 30 | typedef struct ngx_websocket_ctx_s ngx_websocket_ctx_t; 31 | typedef struct ngx_websocket_loc_conf_s ngx_websocket_loc_conf_t; 32 | 33 | typedef void (* ngx_websocket_recv_handler_pt) 34 | (ngx_websocket_session_t *ws, ngx_str_t *msg, u_char opcode); 35 | typedef void (* ngx_websocket_connect_handler_pt)(ngx_websocket_session_t *ws); 36 | typedef void (* ngx_websocket_disconnect_handler_pt)(ngx_websocket_session_t *ws); 37 | 38 | struct ngx_websocket_header_s { 39 | u_char fin:1; 40 | u_char rsv1:1; 41 | u_char rsv2:1; 42 | u_char rsv3:1; 43 | u_char opcode:4; 44 | 45 | u_char mask:1; 46 | u_char payload_length:7; 47 | 48 | uint16_t extended_playload_length16; 49 | uint64_t extended_playload_length64; 50 | 51 | u_char masking_key[4]; 52 | }; 53 | 54 | struct ngx_websocket_frame_s { 55 | u_char *phl; 56 | u_char *ph; 57 | ngx_uint_t opcode:4; 58 | ngx_flag_t append; 59 | uint64_t len; 60 | uint64_t mlen; 61 | ngx_buf_t *buf; 62 | ngx_websocket_frame_t *next; 63 | }; 64 | 65 | struct ngx_websocket_session_s { 66 | ngx_http_request_t *r; 67 | ngx_websocket_recv_handler_pt recv_handler; 68 | ngx_websocket_disconnect_handler_pt disconnect_handler; 69 | ngx_pool_t *pool; 70 | ngx_log_t *log; 71 | ngx_websocket_frame_t in_frame; 72 | ngx_event_t ping_evt; 73 | ngx_msec_t ping_interval; 74 | ngx_msec_t timeout; 75 | ngx_msec_t last_recv; 76 | ngx_uint_t close_status; 77 | ngx_chain_t **out; 78 | ngx_uint_t out_last; 79 | ngx_uint_t out_pos; 80 | ngx_uint_t out_queue; 81 | ngx_chain_t *out_chain; 82 | void *ctx[]; 83 | }; 84 | 85 | struct ngx_websocket_ctx_s { 86 | ngx_websocket_session_t *ws; 87 | }; 88 | 89 | struct ngx_websocket_loc_conf_s { 90 | ngx_str_t name; 91 | ngx_websocket_connect_handler_pt connect_handler; 92 | ngx_uint_t out_queue; 93 | ngx_uint_t max_length; 94 | ngx_uint_t max_frame_length; 95 | ngx_chain_t *in_free; 96 | ngx_pool_t *pool; 97 | ngx_msec_t ping_interval; 98 | ngx_msec_t timeout; 99 | }; 100 | 101 | void ngx_websocket_read_handler(ngx_http_request_t *r); 102 | void *ngx_websocket_rmemcpy(void *dst, const void* src, size_t n); 103 | 104 | ngx_int_t ngx_websocket_send_message( 105 | ngx_websocket_session_t *ws, ngx_str_t *str, ngx_int_t opcode); 106 | void 107 | ngx_websocket_finalize_session(ngx_websocket_session_t *s); 108 | 109 | #include "ngx_http_set_header.h" 110 | 111 | extern ngx_module_t ngx_websocket_module; 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /ngx_websocket_handler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #include "ngx_websocket.h" 9 | 10 | static void 11 | ngx_websocket_send_close_frame(ngx_websocket_session_t *ws, ngx_uint_t close_status, ngx_str_t *close_reason) 12 | { 13 | // If close_status is set, then we are already in a CLOSING state 14 | if (ws->close_status) { 15 | return; 16 | } 17 | ws->close_status = close_status; 18 | ngx_str_t frame_data; 19 | frame_data.len = 2 + (close_reason? close_reason->len: 0); 20 | frame_data.data = ngx_pcalloc(ws->r->pool, frame_data.len); 21 | frame_data.data[0] = (close_status >> 8) & 0xff; 22 | frame_data.data[1] = close_status & 0xff; 23 | if (close_reason) { 24 | ngx_memcpy(frame_data.data + 2, close_reason->data, close_reason->len); 25 | } 26 | 27 | ngx_websocket_send_message(ws, &frame_data, NGX_WEBSOCKET_OPCODE_CLOSE); 28 | } 29 | 30 | static void 31 | ngx_websocket_close_request(ngx_http_request_t *r, ngx_int_t rc) 32 | { 33 | ngx_connection_t *c; 34 | 35 | r = r->main; 36 | c = r->connection; 37 | 38 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, 39 | "http request count:%d blk:%d", r->count, r->blocked); 40 | 41 | if (r->count == 0) { 42 | ngx_log_error(NGX_LOG_ALERT, c->log, 0, "http request count is zero"); 43 | } 44 | 45 | r->count--; 46 | 47 | if (r->count || r->blocked) { 48 | return; 49 | } 50 | 51 | #if (NGX_HTTP_V2) 52 | if (r->stream) { 53 | ngx_http_v2_close_stream(r->stream, rc); 54 | return; 55 | } 56 | #endif 57 | 58 | ngx_http_free_request(r, rc); 59 | ngx_http_close_connection(c); 60 | } 61 | 62 | void 63 | ngx_websocket_finalize_session(ngx_websocket_session_t *ws) 64 | { 65 | if (!ws || !ws->r) { 66 | return; 67 | } 68 | 69 | ngx_websocket_close_request(ws->r, NGX_HTTP_OK); 70 | } 71 | 72 | // https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 73 | #define NGX_WS_CLOSE_STATUS_OK 1000 74 | #define NGX_WS_CLOSE_STATUS_PROTOCOL_ERROR 1002 75 | #define NGX_WS_CLOSE_STATUS_TOO_LARGE 1009 76 | 77 | 78 | static ngx_int_t 79 | ngx_websocket_recv(ngx_http_request_t *r, ngx_err_t *err) 80 | { 81 | ngx_int_t n; 82 | ngx_connection_t *c; 83 | ngx_websocket_session_t *ws; 84 | ngx_websocket_ctx_t *ctx; 85 | ngx_websocket_frame_t *f; 86 | ngx_websocket_header_t h; 87 | ngx_buf_t *b; 88 | size_t old_size; 89 | u_char *p, *old_pos; 90 | uint64_t i; 91 | ngx_event_t *rev; 92 | ngx_str_t msg; 93 | ngx_websocket_loc_conf_t *wlcf; 94 | 95 | c = r->connection; 96 | b = NULL; 97 | old_pos = NULL; 98 | old_size = 0; 99 | rev = c->read; 100 | wlcf = ngx_http_get_module_loc_conf(r, ngx_websocket_module); 101 | 102 | ctx = ngx_http_get_module_ctx(r, ngx_websocket_module); 103 | if (ctx == NULL) { 104 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 105 | "websocket: recv| ctx is null"); 106 | return NGX_ERROR; 107 | } 108 | 109 | ws = ctx->ws; 110 | f = &ws->in_frame; 111 | 112 | for (;;) { 113 | 114 | if (f->buf == NULL) { 115 | f->buf = ngx_create_temp_buf(ws->pool, wlcf->max_length); 116 | } 117 | b = f->buf; 118 | 119 | if (old_size) { 120 | b->pos = b->start; 121 | b->last = ngx_movemem(b->pos, old_pos, old_size); 122 | } else { 123 | #if 1 124 | n = recv(c->fd, b->last, b->end - b->last, 0); 125 | if (n == 0) { 126 | rev->eof = 1; 127 | c->error = 1; 128 | *err = 0; 129 | 130 | return NGX_ERROR; 131 | 132 | } else if (n == -1) { 133 | *err = ngx_socket_errno; 134 | 135 | if (*err != NGX_EAGAIN) { 136 | rev->eof = 1; 137 | c->error = 1; 138 | return NGX_ERROR; 139 | } 140 | 141 | return NGX_AGAIN; 142 | } 143 | 144 | /* aio does not call this handler */ 145 | 146 | if ((ngx_event_flags & NGX_USE_LEVEL_EVENT) && rev->active) { 147 | 148 | if (ngx_del_event(rev, NGX_READ_EVENT, 0) != NGX_OK) { 149 | ngx_websocket_close_request(r, 0); 150 | return NGX_OK; 151 | } 152 | } 153 | #else 154 | n = c->recv(c, b->last, b->end - b->last); 155 | 156 | if (n == NGX_ERROR || n == 0) { 157 | return NGX_ERROR; 158 | } 159 | 160 | if (n == NGX_AGAIN) { 161 | if (ngx_handle_read_event(c->read, 0) != NGX_OK) { 162 | ngx_websocket_close_request(r, 0); 163 | return NGX_OK; 164 | } 165 | return NGX_AGAIN; 166 | } 167 | 168 | #endif 169 | b->last += n; 170 | } 171 | 172 | old_pos = NULL; 173 | old_size = 0; 174 | 175 | if (f->ph == NULL) { 176 | f->ph = b->pos; 177 | f->phl = NULL; 178 | } 179 | 180 | if (f->phl == NULL) { 181 | p = f->ph; 182 | h.fin = (*p) >> 7; 183 | if (h.fin == 0) { 184 | f->append = 1; 185 | } 186 | 187 | h.opcode = *p & 0x0f; 188 | if (f->opcode == 0) { 189 | f->opcode = h.opcode; 190 | } 191 | 192 | p++; 193 | if (b->last - p < 1) { 194 | continue; 195 | } 196 | 197 | h.mask = (*p) >> 7; 198 | h.payload_length = (*p) & 0x7f; 199 | p++; 200 | 201 | if (h.payload_length < 126) { 202 | f->len = h.payload_length; 203 | } else if (h.payload_length == 126) { 204 | if (b->last - p < 2) { 205 | continue; 206 | } 207 | ngx_websocket_rmemcpy(&h.extended_playload_length16, p, 2); 208 | p += 2; 209 | f->len = h.extended_playload_length16; 210 | } else if (h.payload_length == 127) { 211 | if (b->last - p < 2) { 212 | continue; 213 | } 214 | ngx_websocket_rmemcpy(&h.extended_playload_length64, p, 8); 215 | p += 8; 216 | f->len = h.extended_playload_length64; 217 | } 218 | 219 | if (h.mask) { 220 | if (b->last - p < 4) { 221 | continue; 222 | } 223 | ngx_memcpy(h.masking_key, p, 4); 224 | //ngx_websocket_rmemcpy(h.masking_key, p, 4); 225 | p += 4; 226 | } 227 | f->phl = p; 228 | } 229 | 230 | p = f->phl; 231 | 232 | if (b->last == b->end && (uint64_t) (b->last - p) < f->len) { 233 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 234 | "websocket: recv| message is too large."); 235 | *err = NGX_WS_CLOSE_STATUS_TOO_LARGE; 236 | return NGX_HTTP_REQUEST_ENTITY_TOO_LARGE; 237 | } 238 | 239 | if ((uint64_t)(b->last - p) < f->len) { 240 | return NGX_AGAIN; 241 | } 242 | 243 | for (i = 0; i < f->len; i++) { 244 | *p = h.masking_key[i%4] ^ *p; 245 | p++; 246 | } 247 | 248 | if (f->append) { 249 | ngx_memmove(f->ph, f->phl, f->len); 250 | f->mlen += f->len; 251 | b->last -= f->phl - f->ph; 252 | p -= f->phl - f->ph; 253 | } else { 254 | b->pos = f->phl; 255 | } 256 | 257 | if (h.fin == 1) { 258 | msg.data = b->pos; 259 | msg.len = p - b->pos; 260 | if (f->opcode == NGX_WEBSOCKET_OPCODE_CLOSE) { 261 | ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, 262 | "websocket: recv| get close message."); 263 | *err = msg.len >= 2? (msg.data[ 0 ] << 8) + msg.data[ 1 ]: NGX_WS_CLOSE_STATUS_PROTOCOL_ERROR; 264 | return NGX_HTTP_CLOSE; 265 | } else if (f->opcode == NGX_WEBSOCKET_OPCODE_PING) { 266 | ngx_websocket_send_message(ws, &msg, NGX_WEBSOCKET_OPCODE_PONG); 267 | } else if (ws->recv_handler) { 268 | ws->last_recv = ngx_time(); 269 | ws->recv_handler(ws, &msg, f->opcode); 270 | } 271 | 272 | if (b->last == p) { 273 | b->last = b->pos = b->start; 274 | } else { 275 | old_pos = p; 276 | old_size = b->last - p; 277 | } 278 | f->ph = NULL; 279 | f->phl = NULL; 280 | f->opcode = 0; 281 | f->append = 0; 282 | 283 | return NGX_OK; 284 | } 285 | 286 | // if fin == 0 287 | if (b->last > p) { 288 | f->ph = p; 289 | f->phl = NULL; 290 | } 291 | } 292 | 293 | return NGX_OK; 294 | } 295 | 296 | void 297 | ngx_websocket_read_handler(ngx_http_request_t *r) 298 | { 299 | ngx_err_t err; 300 | ngx_event_t *rev; 301 | ngx_connection_t *c; 302 | ngx_websocket_ctx_t *ctx; 303 | int rc; 304 | 305 | c = r->connection; 306 | rev = c->read; 307 | 308 | #if (NGX_HTTP_V2) 309 | 310 | if (r->stream) { 311 | if (c->error) { 312 | err = 0; 313 | goto closed; 314 | } 315 | 316 | return; 317 | } 318 | 319 | #endif 320 | 321 | 322 | #if (NGX_HAVE_KQUEUE) 323 | 324 | if (ngx_event_flags & NGX_USE_KQUEUE_EVENT && rev->pending_eof) { 325 | 326 | rev->eof = 1; 327 | c->error = 1; 328 | err = rev->kq_errno; 329 | 330 | goto closed; 331 | } 332 | 333 | #endif 334 | 335 | 336 | 337 | #if (NGX_HAVE_EPOLLRDHUP) 338 | 339 | if (((ngx_event_flags & NGX_USE_EPOLL_EVENT) && ngx_use_epoll_rdhup) && 340 | rev->pending_eof) 341 | { 342 | socklen_t len; 343 | 344 | rev->eof = 1; 345 | c->error = 1; 346 | 347 | err = 0; 348 | len = sizeof(ngx_err_t); 349 | 350 | /* 351 | * BSDs and Linux return 0 and set a pending error in err 352 | * Solaris returns -1 and sets errno 353 | */ 354 | 355 | if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, (void *) &err, &len) 356 | == -1) 357 | { 358 | err = ngx_socket_errno; 359 | } 360 | 361 | goto closed; 362 | } 363 | 364 | #endif 365 | 366 | rc = ngx_websocket_recv(r, &err); 367 | if (rc == NGX_ERROR) { 368 | ngx_log_error(NGX_LOG_DEBUG, c->log, err, 369 | "websocket-recv: read_handler| recv return error"); 370 | goto closed; 371 | } else if (rc == NGX_HTTP_CLOSE || rc == NGX_HTTP_REQUEST_ENTITY_TOO_LARGE) { 372 | // Echo the close frame back to the caller if the close operation originated there 373 | if ((ctx = ngx_http_get_module_ctx(r, ngx_websocket_module)) != 0 && 374 | ctx->ws && !ctx->ws->close_status) 375 | ngx_websocket_send_close_frame(ctx->ws, err, 0); 376 | 377 | if (rc == NGX_HTTP_CLOSE) 378 | ngx_websocket_close_request(r, NGX_HTTP_OK); 379 | } 380 | 381 | return; 382 | 383 | closed: 384 | 385 | if (err) { 386 | rev->error = 1; 387 | } 388 | 389 | ngx_log_error(NGX_LOG_DEBUG, c->log, err, 390 | "websocket-recv: read_handler| client prematurely closed connection"); 391 | 392 | ngx_http_finalize_request(r, NGX_HTTP_CLIENT_CLOSED_REQUEST); 393 | } 394 | -------------------------------------------------------------------------------- /ngx_websocket_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #include "ngx_websocket.h" 9 | 10 | static u_char SHA_INPUT[] = "XXXXXXXXXXXXXXXXXXXXXXXX258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 11 | static ngx_str_t HEADER_UPGRAGE = ngx_string("Upgrade"); 12 | static ngx_str_t HEADER_WS_KEY = ngx_string("Sec-WebSocket-Key"); 13 | static ngx_str_t HEADER_WS_VERSION = ngx_string("Sec-WebSocket-Version"); 14 | //static ngx_str_t HEADER_WS_EXTENSIONS = ngx_string("Sec-WebSocket-Extensions"); 15 | 16 | static ngx_keyval_t ngx_websocket_headers[] = { 17 | { ngx_string("Upgrade"), ngx_string("websocket") }, 18 | { ngx_string("Sec-WebSocket-Version"), ngx_string("13") }, 19 | { ngx_string("Sec-WebSocket-Accept"), ngx_null_string }, 20 | { ngx_string("Sec-WebSocket-Protocol:"), ngx_null_string }, 21 | { ngx_string("Sec-WebSocket-Extensions"), ngx_null_string }, 22 | { ngx_string("WebSocket-Server"), ngx_string("im-pingo-websocket") }, 23 | { ngx_null_string, ngx_null_string } 24 | }; 25 | 26 | static void * 27 | ngx_websocket_create_loc_conf(ngx_conf_t *cf); 28 | static char * 29 | ngx_websocket_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); 30 | static char * 31 | ngx_websocket_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 32 | static ngx_int_t 33 | ngx_websocket_handler(ngx_http_request_t *r); 34 | 35 | static ngx_http_module_t ngx_websocket_module_ctx = { 36 | NULL, /* preconfiguration */ 37 | NULL, /* postconfiguration */ 38 | 39 | NULL, /* create main configuration */ 40 | NULL, /* init main configuration */ 41 | 42 | NULL, /* create server configuration */ 43 | NULL, /* merge server configuration */ 44 | 45 | ngx_websocket_create_loc_conf, /* create location configuration */ 46 | ngx_websocket_merge_loc_conf /* merge location configuration */ 47 | }; 48 | 49 | 50 | static ngx_command_t ngx_websocket_commands[] = { 51 | 52 | { ngx_string("websocket"), 53 | NGX_HTTP_LOC_CONF|NGX_CONF_ANY, 54 | ngx_websocket_slot, 55 | NGX_HTTP_LOC_CONF_OFFSET, 56 | 0, 57 | NULL }, 58 | 59 | ngx_null_command 60 | }; 61 | 62 | ngx_module_t ngx_websocket_module = { 63 | NGX_MODULE_V1, 64 | &ngx_websocket_module_ctx, /* module context */ 65 | ngx_websocket_commands, /* module directives */ 66 | NGX_HTTP_MODULE, /* module type */ 67 | NULL, /* init master */ 68 | NULL, /* init module */ 69 | NULL, /* init process */ 70 | NULL, /* init thread */ 71 | NULL, /* exit thread */ 72 | NULL, /* exit process */ 73 | NULL, /* exit master */ 74 | NGX_MODULE_V1_PADDING 75 | }; 76 | 77 | static void * 78 | ngx_websocket_create_loc_conf(ngx_conf_t *cf) 79 | { 80 | ngx_websocket_loc_conf_t *wlcf; 81 | 82 | wlcf = ngx_pcalloc(cf->pool, sizeof(ngx_websocket_loc_conf_t)); 83 | if (wlcf == NULL) { 84 | return NULL; 85 | } 86 | wlcf->out_queue = NGX_CONF_UNSET; 87 | wlcf->max_length = NGX_CONF_UNSET; 88 | wlcf->max_frame_length = NGX_CONF_UNSET; 89 | wlcf->ping_interval = NGX_CONF_UNSET_MSEC; 90 | wlcf->timeout = NGX_CONF_UNSET_MSEC; 91 | wlcf->pool = ngx_create_pool(1024, cf->log); 92 | 93 | return wlcf; 94 | } 95 | 96 | static char * 97 | ngx_websocket_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 98 | { 99 | ngx_websocket_loc_conf_t *prev = parent; 100 | ngx_websocket_loc_conf_t *conf = child; 101 | 102 | ngx_conf_merge_uint_value(conf->out_queue, prev->out_queue, 512); 103 | ngx_conf_merge_uint_value(conf->max_length, prev->max_length, 4096000); 104 | ngx_conf_merge_uint_value(conf->max_frame_length, prev->max_frame_length, 65535); 105 | ngx_conf_merge_msec_value(conf->ping_interval, prev->ping_interval, 5000); 106 | ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 107 | conf->ping_interval * 3); 108 | 109 | return NGX_CONF_OK; 110 | } 111 | 112 | static char * 113 | ngx_websocket_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 114 | { 115 | ngx_http_core_loc_conf_t *clcf; 116 | ngx_uint_t i; 117 | ngx_str_t *args, v; 118 | ngx_websocket_loc_conf_t *wlcf; 119 | 120 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 121 | clcf->handler = ngx_websocket_handler; 122 | 123 | wlcf = ngx_http_conf_get_module_loc_conf(cf, ngx_websocket_module); 124 | if (wlcf == NULL) { 125 | return "websocket module loc conf is null"; 126 | } 127 | 128 | wlcf->name = clcf->name; 129 | 130 | args = cf->args->elts; 131 | for (i = 1; i < cf->args->nelts; ++i) { 132 | if (ngx_strncmp(args[i].data, "out_queue=", 10) == 0) { 133 | v.data = args[i].data + 10; 134 | v.len = args[i].len - 10; 135 | wlcf->out_queue = ngx_atoi(v.data, v.len); 136 | 137 | } else if (ngx_strncmp(args[i].data, "message_length=", 15) == 0) { 138 | v.data = args[i].data + 15; 139 | v.len = args[i].len - 15; 140 | wlcf->max_length = ngx_atoi(v.data, v.len); 141 | 142 | } else if (ngx_strncmp(args[i].data, "frame_length=", 13) == 0) { 143 | v.data = args[i].data + 13; 144 | v.len = args[i].len - 13; 145 | wlcf->max_frame_length = ngx_atoi(v.data, v.len); 146 | 147 | } else if (ngx_strncmp(args[i].data, "ping_interval=", 14) == 0) { 148 | v.data = args[i].data + 14; 149 | v.len = args[i].len - 14; 150 | wlcf->ping_interval = ngx_parse_time(&v, 0); 151 | if (wlcf->ping_interval == (ngx_msec_t) NGX_ERROR) { 152 | return "invalid value"; 153 | } 154 | } else if (ngx_strncmp(args[i].data, "timeout=", 8) == 0) { 155 | v.data = args[i].data + 8; 156 | v.len = args[i].len - 8; 157 | wlcf->timeout = ngx_parse_time(&v, 0); 158 | if (wlcf->timeout == (ngx_msec_t) NGX_ERROR) { 159 | return "invalid value"; 160 | } 161 | } else { 162 | return "invalid option"; 163 | } 164 | } 165 | 166 | return NGX_CONF_OK; 167 | } 168 | 169 | static void 170 | ngx_websocket_ping(ngx_event_t *ev) 171 | { 172 | ngx_websocket_session_t *ws; 173 | 174 | ws = ev->data; 175 | if ((1000 * (ngx_time() - ws->last_recv)) >= ws->timeout) { 176 | ngx_websocket_finalize_session(ws); 177 | return; 178 | } 179 | 180 | ngx_websocket_send_message(ws, NULL, NGX_WEBSOCKET_OPCODE_PING); 181 | 182 | ngx_add_timer(ev, ws->ping_interval); 183 | } 184 | 185 | static ngx_websocket_session_t * 186 | ngx_websocket_init_session(ngx_http_request_t *r) 187 | { 188 | ngx_websocket_session_t *ws; 189 | ngx_websocket_ctx_t *ctx; 190 | ngx_websocket_loc_conf_t *wlcf; 191 | 192 | wlcf = ngx_http_get_module_loc_conf(r, ngx_websocket_module); 193 | ctx = ngx_http_get_module_ctx(r, ngx_websocket_module); 194 | if (ctx) { 195 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 196 | "websocket: init_session| websocket session has been inited"); 197 | return NULL; 198 | } 199 | ctx = ngx_pcalloc(r->connection->pool, sizeof(ngx_websocket_ctx_t)); 200 | if (!ctx) { 201 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 202 | "websocket: init_session| pcalloc ctx failed"); 203 | return NULL; 204 | } 205 | 206 | ws = ngx_pcalloc(r->connection->pool, 207 | sizeof(ngx_websocket_session_t) + ngx_http_max_module * sizeof(void *)); 208 | if (!ws) { 209 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 210 | "websocket: init_session| pcalloc websocket session failed"); 211 | return NULL; 212 | } 213 | 214 | ws->out = ngx_pcalloc(r->connection->pool, 215 | sizeof(ngx_chain_t *) * wlcf->out_queue); 216 | 217 | ws->r = r; 218 | ctx->ws = ws; 219 | 220 | ws->pool = ngx_create_pool(4096, r->connection->log); 221 | ws->log = r->connection->log; 222 | 223 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 224 | "websocket: init_session| init websocket session"); 225 | 226 | ngx_http_set_ctx(r, ctx, ngx_websocket_module); 227 | ws->out_queue = wlcf->out_queue; 228 | ws->ping_interval = wlcf->ping_interval; 229 | ws->timeout = wlcf->timeout; 230 | ws->last_recv = ngx_time(); 231 | 232 | ws->ping_evt.handler = ngx_websocket_ping; 233 | ws->ping_evt.log = ws->log; 234 | ws->ping_evt.data = ws; 235 | ws->close_status = 0; 236 | ngx_add_timer(&ws->ping_evt, ws->ping_interval); 237 | 238 | return ws; 239 | } 240 | 241 | static ngx_int_t 242 | ngx_websocket_send_header(ngx_http_request_t *r) 243 | { 244 | ngx_int_t rc; 245 | ngx_keyval_t *h; 246 | static ngx_str_t http_status = ngx_string("101 Switching Protocols"); 247 | 248 | r->headers_out.status_line = http_status; 249 | r->headers_out.status = NGX_HTTP_SWITCHING_PROTOCOLS; 250 | r->keepalive = 1; 251 | 252 | h = ngx_websocket_headers; 253 | for (; h->key.len; ++h) { 254 | if (!h->value.data || !h->value.len) { 255 | continue; 256 | } 257 | 258 | rc = ngx_http_set_header_out(r, &h->key, &h->value); 259 | if (rc != NGX_OK) { 260 | return rc; 261 | } 262 | } 263 | 264 | return ngx_http_send_header(r); 265 | } 266 | 267 | static void 268 | ngx_websocket_cleanup(void *data) 269 | { 270 | ngx_http_request_t *r; 271 | ngx_websocket_session_t *ws; 272 | ngx_websocket_ctx_t *ctx; 273 | 274 | r = data; 275 | ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, 276 | "websocket: cleanup| disconnect"); 277 | 278 | ctx = ngx_http_get_module_ctx(r, ngx_websocket_module); 279 | if (!ctx) { 280 | return; 281 | } 282 | 283 | ws = ctx->ws; 284 | if (ws->ping_evt.timer_set) { 285 | ngx_del_timer(&ws->ping_evt); 286 | } 287 | if (ws && ws->disconnect_handler) { 288 | ws->disconnect_handler(ws); 289 | } 290 | } 291 | 292 | static void 293 | ngx_websocket_free_chain(ngx_websocket_session_t *ws, ngx_chain_t *cl) { 294 | ngx_http_request_t *r; 295 | ngx_websocket_loc_conf_t *wlcf; 296 | 297 | r = ws->r; 298 | wlcf = ngx_http_get_module_loc_conf(r, ngx_websocket_module); 299 | cl->next = wlcf->in_free; 300 | wlcf->in_free = cl; 301 | } 302 | 303 | static ngx_chain_t * 304 | ngx_websocket_prepare_chain(ngx_websocket_session_t *ws, 305 | ngx_str_t *msg, u_char opcode) 306 | { 307 | ngx_http_request_t *r; 308 | ngx_websocket_loc_conf_t *wlcf; 309 | ngx_chain_t *out; 310 | ngx_buf_t *b; 311 | u_char *p; 312 | 313 | r = ws->r; 314 | wlcf = ngx_http_get_module_loc_conf(r, ngx_websocket_module); 315 | 316 | if (msg && (msg->len > wlcf->max_length)) { 317 | ngx_log_error(NGX_LOG_ERR, ws->log, 0, "websocket: prepare_chain| " 318 | "max message length is %d", wlcf->max_length); 319 | return NULL; 320 | } 321 | 322 | if (wlcf->in_free) { 323 | out = wlcf->in_free; 324 | wlcf->in_free = wlcf->in_free->next; 325 | out->buf->last = out->buf->pos = out->buf->start; 326 | out->next = NULL; 327 | b = out->buf; 328 | ngx_memzero(b->start, wlcf->max_length); 329 | } else { 330 | b = ngx_create_temp_buf(wlcf->pool, wlcf->max_length); 331 | out = ngx_pcalloc(wlcf->pool, sizeof(ngx_chain_t)); 332 | out->buf = b; 333 | out->next = NULL; 334 | } 335 | 336 | b->last_in_chain = 1; 337 | b->flush = 1; 338 | 339 | p = b->last; 340 | 341 | if (!msg) { 342 | *p++ = 0x80 | opcode; // fin + opcode 343 | *p++ = 0; 344 | b->last = p; 345 | 346 | } else { 347 | size_t s, n; 348 | u_char *d; 349 | 350 | // Respect the frame size limit 351 | for (s = msg->len, d = msg->data; (n = s > wlcf->max_frame_length? wlcf->max_frame_length: s) > 0; s -= n, p += n, d += n ) { 352 | 353 | *p++ = ((s - n) == 0? 0x80:0x00 ) | opcode; // fin + opcode 354 | 355 | if (n < 126) { 356 | *p++ = n; 357 | } else if (n >= 126 && n <= 0xffff) { 358 | *p++ = 126; 359 | *p++ = (n & 0xff00) >> 8; 360 | *p++ = n & 0x00ff; 361 | } else if (n > 0xffff) { 362 | *p++ = 127; 363 | *p++ = (n & 0xff000000) >> 24; 364 | *p++ = (n & 0x00ff0000) >> 16; 365 | *p++ = (n & 0x0000ff00) >> 8; 366 | *p++ = (n & 0x000000ff); 367 | } 368 | 369 | b->last = ngx_cpymem(p, d, n); 370 | opcode = 0; 371 | } 372 | } 373 | 374 | return out; 375 | } 376 | 377 | void 378 | ngx_websocket_write_handler(ngx_http_request_t *r) 379 | { 380 | ngx_websocket_ctx_t *ctx; 381 | ngx_websocket_session_t *ws; 382 | ngx_event_t *wev; 383 | ngx_int_t rc; 384 | 385 | wev = r->connection->write; 386 | 387 | if (r->connection->destroyed) { 388 | return; 389 | } 390 | 391 | if (wev->timedout) { 392 | ngx_log_error(NGX_LOG_INFO, r->connection->log, NGX_ETIMEDOUT, 393 | "websocket| write_handler| client timed out"); 394 | r->connection->timedout = 1; 395 | if (r->header_sent) { 396 | ngx_http_finalize_request(r, NGX_HTTP_CLIENT_CLOSED_REQUEST); 397 | ngx_http_run_posted_requests(r->connection); 398 | } else { 399 | r->error_page = 1; 400 | ngx_http_finalize_request(r, NGX_HTTP_SERVICE_UNAVAILABLE); 401 | } 402 | 403 | return; 404 | } 405 | 406 | if (wev->timer_set) { 407 | ngx_del_timer(wev); 408 | } 409 | 410 | ctx = ngx_http_get_module_ctx(r, ngx_websocket_module); 411 | ws = ctx->ws; 412 | 413 | if (ws->out_chain == NULL && ws->out_pos != ws->out_last) { 414 | ws->out_chain = ws->out[ws->out_pos]; 415 | } 416 | 417 | while (ws->out_chain) { 418 | 419 | if (r->connection->buffered) { 420 | rc = ngx_http_output_filter(r, NULL); 421 | } else { 422 | rc = ngx_http_output_filter(r, ws->out_chain); 423 | } 424 | 425 | if (rc == NGX_AGAIN) { 426 | if (ngx_handle_write_event(wev, 0) != NGX_OK) { 427 | ngx_log_error(NGX_LOG_ERR, r->connection->log, ngx_errno, 428 | "websocket: write_handler| handle write event failed"); 429 | ngx_http_finalize_request(r, NGX_ERROR); 430 | } 431 | return; 432 | } 433 | 434 | if (rc == NGX_ERROR) { 435 | ngx_log_error(NGX_LOG_ERR, r->connection->log, ngx_errno, 436 | "websocket: write_handler| send error"); 437 | ngx_http_finalize_request(r, NGX_ERROR); 438 | return; 439 | } 440 | 441 | /* NGX_OK */ 442 | 443 | ngx_websocket_free_chain(ws, ws->out[ws->out_pos]); 444 | ++ws->out_pos; 445 | ws->out_pos %= ws->out_queue; 446 | if (ws->out_pos == ws->out_last) { 447 | ws->out_chain = NULL; 448 | break; 449 | } 450 | 451 | ws->out_chain = ws->out[ws->out_pos]; 452 | } 453 | 454 | if (wev->active) { 455 | ngx_del_event(wev, NGX_WRITE_EVENT, 0); 456 | } 457 | } 458 | 459 | ngx_int_t 460 | ngx_websocket_send_message(ngx_websocket_session_t *ws, 461 | ngx_str_t *str, ngx_int_t opcode) 462 | { 463 | ngx_uint_t nmsg; 464 | ngx_http_request_t *r; 465 | ngx_websocket_ctx_t *ctx; 466 | ngx_chain_t *out; 467 | 468 | r = ws->r; 469 | 470 | ctx = ngx_http_get_module_ctx(r, ngx_websocket_module); 471 | if (ctx == NULL) { 472 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 473 | "websocket: send| websocket ctx is null"); 474 | return NGX_ERROR; 475 | } 476 | 477 | out = ngx_websocket_prepare_chain(ws, str, opcode); 478 | if (out == NULL) { 479 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 480 | "websocket: send| prepare chain failed."); 481 | return NGX_ERROR; 482 | } 483 | 484 | nmsg = (ws->out_last - ws->out_pos) % ws->out_queue + 1; 485 | 486 | if (nmsg >= ws->out_queue) { 487 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, 488 | "websocket: send_message| drop message bufs=%ui", nmsg); 489 | return NGX_AGAIN; 490 | } 491 | 492 | ws->out[ws->out_last++] = out; 493 | ws->out_last %= ws->out_queue; 494 | 495 | if (!r->connection->write->active) { 496 | ngx_websocket_write_handler(r); 497 | ngx_http_run_posted_requests(r->connection); 498 | } 499 | 500 | return NGX_OK; 501 | } 502 | 503 | static ngx_int_t 504 | ngx_websocket_handshark(ngx_http_request_t *r) 505 | { 506 | u_char sha_digest[SHA_DIGEST_LENGTH] = { 0 }; 507 | ngx_str_t *upgrade; 508 | ngx_str_t *ws_version; 509 | ngx_str_t *ws_key; 510 | //ngx_str_t *ws_extensions; 511 | ngx_str_t ws_accept, digest; 512 | ngx_int_t ret; 513 | ngx_int_t v; 514 | ngx_websocket_loc_conf_t *wlcf; 515 | ngx_websocket_session_t *ws; 516 | 517 | if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) { 518 | return NGX_DECLINED; 519 | } 520 | 521 | upgrade = ngx_http_get_header_in(r, &HEADER_UPGRAGE); 522 | ws_key = ngx_http_get_header_in(r, &HEADER_WS_KEY); 523 | ws_version = ngx_http_get_header_in(r, &HEADER_WS_VERSION); 524 | // ws_extensions = ngx_http_get_header_in(r, &HEADER_WS_EXTENSIONS); 525 | 526 | v = ngx_atoi(ws_version->data, ws_version->len); 527 | 528 | if (v != 13 || !upgrade || !ws_key) { 529 | return NGX_DECLINED; 530 | } 531 | 532 | ws = ngx_websocket_init_session(r); 533 | if (!ws) { 534 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 535 | "websocket: handshark| init session failed"); 536 | 537 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 538 | } 539 | 540 | wlcf = ngx_http_get_module_loc_conf(r, ngx_websocket_module); 541 | if (wlcf->connect_handler) { 542 | wlcf->connect_handler(ws); 543 | } 544 | 545 | ngx_memcpy(SHA_INPUT, ws_key->data, ws_key->len); 546 | SHA1(SHA_INPUT, sizeof(SHA_INPUT) - 1, sha_digest); 547 | 548 | digest.data = sha_digest; 549 | digest.len = sizeof(sha_digest); 550 | 551 | ws_accept.data = ngx_pcalloc(r->connection->pool, 28); 552 | ngx_encode_base64(&ws_accept, &digest); 553 | 554 | ngx_websocket_headers[2].value = ws_accept; 555 | 556 | ret = ngx_websocket_send_header(r); 557 | if (ret != NGX_OK) { 558 | return ret; 559 | } 560 | 561 | ngx_http_send_special(r, NGX_HTTP_FLUSH); 562 | 563 | return NGX_OK; 564 | } 565 | 566 | static ngx_int_t 567 | ngx_websocket_handler(ngx_http_request_t *r) 568 | { 569 | ngx_http_cleanup_t *cln; 570 | ngx_int_t ret; 571 | 572 | ret = ngx_websocket_handshark(r); 573 | if (ret != NGX_OK) { 574 | return ret; 575 | } 576 | 577 | cln = ngx_http_cleanup_add(r, 0); 578 | if (cln == NULL) { 579 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 580 | } 581 | cln->handler = ngx_websocket_cleanup; 582 | cln->data = r; 583 | 584 | r->read_event_handler = ngx_websocket_read_handler; 585 | r->write_event_handler = ngx_websocket_write_handler; 586 | r->count++; 587 | 588 | return NGX_DONE; 589 | } 590 | -------------------------------------------------------------------------------- /t/config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_websocket_echo_module 2 | 3 | HTTP_MODULES="$HTTP_MODULES \ 4 | ngx_websocket_echo_module \ 5 | " 6 | 7 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS 8 | $ngx_addon_dir/ngx_websocket_echo_module.c \ 9 | " 10 | CFLAGS="$CFLAGS -I $ngx_addon_dir" -------------------------------------------------------------------------------- /t/ngx_websocket_echo_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: im-pingo 3 | * @Copyright: pngox 4 | * @Github: https://github.com/im-pingo 5 | * @EMail: cczjp89@gmail.com 6 | */ 7 | 8 | #include "ngx_websocket.h" 9 | 10 | static char * 11 | ngx_websocket_echo_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 12 | static void 13 | ngx_websocket_connect_handler(ngx_websocket_session_t *ws); 14 | static void 15 | ngx_websocket_recv_handler(ngx_websocket_session_t *ws, 16 | ngx_str_t *msg, u_char opcode); 17 | static void 18 | ngx_websocket_disconnect_handler(ngx_websocket_session_t *ws); 19 | 20 | static ngx_http_module_t ngx_websocket_echo_module_ctx = { 21 | NULL, /* preconfiguration */ 22 | NULL, /* postconfiguration */ 23 | 24 | NULL, /* create main configuration */ 25 | NULL, /* init main configuration */ 26 | 27 | NULL, /* create server configuration */ 28 | NULL, /* merge server configuration */ 29 | 30 | NULL, /* create location configuration */ 31 | NULL /* merge location configuration */ 32 | }; 33 | 34 | 35 | static ngx_command_t ngx_websocket_echo_commands[] = { 36 | 37 | { ngx_string("websocket_echo"), 38 | NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, 39 | ngx_websocket_echo_slot, 40 | NGX_HTTP_LOC_CONF_OFFSET, 41 | 0, 42 | NULL }, 43 | 44 | ngx_null_command 45 | }; 46 | 47 | ngx_module_t ngx_websocket_echo_module = { 48 | NGX_MODULE_V1, 49 | &ngx_websocket_echo_module_ctx, /* module context */ 50 | ngx_websocket_echo_commands, /* module directives */ 51 | NGX_HTTP_MODULE, /* module type */ 52 | NULL, /* init master */ 53 | NULL, /* init module */ 54 | NULL, /* init process */ 55 | NULL, /* init thread */ 56 | NULL, /* exit thread */ 57 | NULL, /* exit process */ 58 | NULL, /* exit master */ 59 | NGX_MODULE_V1_PADDING 60 | }; 61 | 62 | static char * 63 | ngx_websocket_echo_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 64 | { 65 | ngx_websocket_loc_conf_t *wlcf; 66 | 67 | wlcf = ngx_http_conf_get_module_loc_conf(cf, ngx_websocket_module); 68 | wlcf->connect_handler = ngx_websocket_connect_handler; 69 | 70 | return NGX_CONF_OK; 71 | } 72 | 73 | static void 74 | ngx_websocket_connect_handler(ngx_websocket_session_t *ws) 75 | { 76 | ngx_log_error(NGX_LOG_DEBUG, ws->log, 0, 77 | "websocket-echo: connect_handler| new client connection."); 78 | 79 | ws->recv_handler = ngx_websocket_recv_handler; 80 | ws->disconnect_handler = ngx_websocket_disconnect_handler; 81 | } 82 | 83 | static void 84 | ngx_websocket_recv_handler(ngx_websocket_session_t *ws, 85 | ngx_str_t *msg, u_char opcode) 86 | { 87 | switch (opcode) { 88 | case NGX_WEBSOCKET_OPCODE_TEXT: 89 | case NGX_WEBSOCKET_OPCODE_BINARY: 90 | ngx_log_error(NGX_LOG_DEBUG, ws->log, 0, 91 | "websocket-echo: recv_handler| recv: %V", msg); 92 | ngx_websocket_send_message(ws, msg, opcode); 93 | break; 94 | 95 | case NGX_WEBSOCKET_OPCODE_PONG: 96 | ngx_log_error(NGX_LOG_DEBUG, ws->log, 0, 97 | "websocket-echo: recv_handler| getting pong ..."); 98 | break; 99 | 100 | default: 101 | ngx_log_error(NGX_LOG_WARN, ws->log, 0, 102 | "websocket-echo: recv_handler| drop message, opcode %d", opcode); 103 | } 104 | } 105 | 106 | static void 107 | ngx_websocket_disconnect_handler(ngx_websocket_session_t *ws) 108 | { 109 | ngx_log_error(NGX_LOG_DEBUG, ws->log, 0, 110 | "websocket-echo: disconnect_handler| client disconnect."); 111 | } --------------------------------------------------------------------------------