├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── nginx.conf ├── nginx.vh.default.conf ├── ngx_http_websockify_module.c ├── t ├── handshake.t └── transport.t └── websocket.h /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tg123 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | 5 | # Libraries 6 | *.lib 7 | *.a 8 | 9 | # Shared objects (inc. Windows DLLs) 10 | *.dll 11 | *.so 12 | *.so.* 13 | *.dylib 14 | 15 | # Executables 16 | *.exe 17 | *.out 18 | *.app 19 | t/servroot/ 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | LABEL maintainer="NGINX Docker Maintainers " 4 | 5 | ADD . /src 6 | 7 | ENV NGINX_VERSION 1.12.2 8 | ENV LUAJIT_VERSION 2.0.5 9 | ENV LUA_NGINX_MODULE_VERSION 0.10.15 10 | 11 | RUN GPG_KEYS=B0F4253373F8F6F510D42178520A9993A1C052F8 \ 12 | && CONFIG="\ 13 | --prefix=/etc/nginx \ 14 | --sbin-path=/usr/sbin/nginx \ 15 | --modules-path=/usr/lib/nginx/modules \ 16 | --conf-path=/etc/nginx/nginx.conf \ 17 | --error-log-path=/var/log/nginx/error.log \ 18 | --http-log-path=/var/log/nginx/access.log \ 19 | --pid-path=/var/run/nginx.pid \ 20 | --lock-path=/var/run/nginx.lock \ 21 | --http-client-body-temp-path=/var/cache/nginx/client_temp \ 22 | --http-proxy-temp-path=/var/cache/nginx/proxy_temp \ 23 | --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ 24 | --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ 25 | --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ 26 | --user=nginx \ 27 | --group=nginx \ 28 | --with-http_ssl_module \ 29 | --with-http_realip_module \ 30 | --with-http_addition_module \ 31 | --with-http_sub_module \ 32 | --with-http_dav_module \ 33 | --with-http_flv_module \ 34 | --with-http_mp4_module \ 35 | --with-http_gunzip_module \ 36 | --with-http_gzip_static_module \ 37 | --with-http_random_index_module \ 38 | --with-http_secure_link_module \ 39 | --with-http_stub_status_module \ 40 | --with-http_auth_request_module \ 41 | --with-http_xslt_module=dynamic \ 42 | --with-http_image_filter_module=dynamic \ 43 | --with-http_geoip_module=dynamic \ 44 | --with-threads \ 45 | --with-stream \ 46 | --with-stream_ssl_module \ 47 | --with-stream_ssl_preread_module \ 48 | --with-stream_realip_module \ 49 | --with-stream_geoip_module=dynamic \ 50 | --with-http_slice_module \ 51 | --with-mail \ 52 | --with-mail_ssl_module \ 53 | --with-compat \ 54 | --with-file-aio \ 55 | --with-http_v2_module \ 56 | --add-module=/src \ 57 | --add-module=/lua-nginx-module-${LUA_NGINX_MODULE_VERSION} \ 58 | " \ 59 | && addgroup -S nginx \ 60 | && adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \ 61 | && apk add --no-cache --virtual .build-deps \ 62 | gcc \ 63 | libc-dev \ 64 | make \ 65 | openssl-dev \ 66 | pcre-dev \ 67 | zlib-dev \ 68 | readline-dev \ 69 | linux-headers \ 70 | curl \ 71 | gnupg \ 72 | libxslt-dev \ 73 | gd-dev \ 74 | geoip-dev \ 75 | && curl -fSL http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz -o nginx.tar.gz \ 76 | && curl -fSL http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz.asc -o nginx.tar.gz.asc \ 77 | && export GNUPGHOME="$(mktemp -d)" \ 78 | && found=''; \ 79 | for server in \ 80 | ha.pool.sks-keyservers.net \ 81 | hkp://keyserver.ubuntu.com:80 \ 82 | hkp://p80.pool.sks-keyservers.net:80 \ 83 | pgp.mit.edu \ 84 | ; do \ 85 | echo "Fetching GPG key $GPG_KEYS from $server"; \ 86 | gpg --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$GPG_KEYS" && found=yes && break; \ 87 | done; \ 88 | test -z "$found" && echo >&2 "error: failed to fetch GPG key $GPG_KEYS" && exit 1; \ 89 | gpg --batch --verify nginx.tar.gz.asc nginx.tar.gz \ 90 | && rm -r "$GNUPGHOME" nginx.tar.gz.asc \ 91 | && mkdir -p /usr/src \ 92 | && tar -zxC /usr/src -f nginx.tar.gz \ 93 | && rm nginx.tar.gz \ 94 | 95 | # LuaJit & nginx-lua-module 96 | && curl -fSL https://github.com/openresty/luajit2/archive/v${LUAJIT_VERSION}.tar.gz -o LuaJIT.tar.gz \ 97 | && tar zxvf LuaJIT.tar.gz \ 98 | 99 | && curl -fSL https://github.com/openresty/lua-nginx-module/archive/v${LUA_NGINX_MODULE_VERSION}.tar.gz -o lua-nginx-module.tar.gz \ 100 | && tar zxvf lua-nginx-module.tar.gz \ 101 | 102 | && rm -f LuaJIT.tar.gz lua-nginx-module.tar.gz \ 103 | 104 | && make -C /luajit2-${LUAJIT_VERSION} \ 105 | && make -C /luajit2-${LUAJIT_VERSION} install \ 106 | 107 | && cd /usr/src/nginx-$NGINX_VERSION \ 108 | && ./configure $CONFIG --with-debug \ 109 | && make -j$(getconf _NPROCESSORS_ONLN) \ 110 | && mv objs/nginx objs/nginx-debug \ 111 | && mv objs/ngx_http_xslt_filter_module.so objs/ngx_http_xslt_filter_module-debug.so \ 112 | && mv objs/ngx_http_image_filter_module.so objs/ngx_http_image_filter_module-debug.so \ 113 | && mv objs/ngx_http_geoip_module.so objs/ngx_http_geoip_module-debug.so \ 114 | && mv objs/ngx_stream_geoip_module.so objs/ngx_stream_geoip_module-debug.so \ 115 | && ./configure $CONFIG \ 116 | && make -j$(getconf _NPROCESSORS_ONLN) \ 117 | && make install \ 118 | && rm -rf /etc/nginx/html/ \ 119 | && mkdir /etc/nginx/conf.d/ \ 120 | && mkdir -p /usr/share/nginx/html/ \ 121 | && install -m644 html/index.html /usr/share/nginx/html/ \ 122 | && install -m644 html/50x.html /usr/share/nginx/html/ \ 123 | && install -m755 objs/nginx-debug /usr/sbin/nginx-debug \ 124 | && install -m755 objs/ngx_http_xslt_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_xslt_filter_module-debug.so \ 125 | && install -m755 objs/ngx_http_image_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_image_filter_module-debug.so \ 126 | && install -m755 objs/ngx_http_geoip_module-debug.so /usr/lib/nginx/modules/ngx_http_geoip_module-debug.so \ 127 | && install -m755 objs/ngx_stream_geoip_module-debug.so /usr/lib/nginx/modules/ngx_stream_geoip_module-debug.so \ 128 | && ln -s ../../usr/lib/nginx/modules /etc/nginx/modules \ 129 | && strip /usr/sbin/nginx* \ 130 | && strip /usr/lib/nginx/modules/*.so \ 131 | && rm -rf /usr/src/nginx-$NGINX_VERSION \ 132 | \ 133 | # Bring in gettext so we can get `envsubst`, then throw 134 | # the rest away. To do this, we need to install `gettext` 135 | # then move `envsubst` out of the way so `gettext` can 136 | # be deleted completely, then move `envsubst` back. 137 | && apk add --no-cache --virtual .gettext gettext \ 138 | && mv /usr/bin/envsubst /tmp/ \ 139 | \ 140 | && runDeps="$( \ 141 | scanelf --needed --nobanner --format '%n#p' /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \ 142 | | tr ',' '\n' \ 143 | | sort -u \ 144 | | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ 145 | )" \ 146 | && apk add --no-cache --virtual .nginx-rundeps $runDeps \ 147 | && apk del .build-deps \ 148 | && apk del .gettext \ 149 | && mv /tmp/envsubst /usr/local/bin/ \ 150 | \ 151 | # Bring in tzdata so users could set the timezones through the environment 152 | # variables 153 | && apk add --no-cache tzdata libgcc \ 154 | \ 155 | # forward request and error logs to docker log collector 156 | && ln -sf /dev/stdout /var/log/nginx/access.log \ 157 | && ln -sf /dev/stderr /var/log/nginx/error.log \ 158 | && rm -rf /src /lua-nginx-module-${LUA_NGINX_MODULE_VERSION} /luajit2-${LUAJIT_VERSION} 159 | 160 | COPY nginx.conf /etc/nginx/nginx.conf 161 | COPY nginx.vh.default.conf /etc/nginx/conf.d/default.conf 162 | 163 | EXPOSE 80 164 | 165 | STOPSIGNAL SIGTERM 166 | 167 | CMD ["nginx", "-g", "daemon off;"] 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 tgic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Websockify port for Nginx 2 | 3 | Embed the [Websockify](https://github.com/kanaka/websockify/) into Nginx 4 | 5 | ## Installation 6 | 7 | 8 | git clone https://github.com/tg123/websockify-nginx-module.git 9 | 10 | cd path/to/nginx_source 11 | 12 | ./configure --add-module=/path/to/websockify-nginx-module/ 13 | 14 | make 15 | make install 16 | 17 | 18 | ## Uasge 19 | 20 | ### Single noVNC websockify proxy 21 | 22 | in your `nginx.conf` 23 | 24 | ``` 25 | location /websockify { 26 | websockify_pass yourvncip:port 27 | } 28 | ``` 29 | 30 | 31 | 1. visit in your browser, 32 | 1. Host is your `nginx server`'s ip 33 | 1. port is your `nginx server`'s listening port 34 | 1. Click connect 35 | 36 | 37 | ### Quick start with Docker 38 | 39 | Proxy `192.168.188.42:5901` to your localhost/websockify. 40 | 41 | _Note_: 5901 is hardcoded in `nginx.vh.default.conf` 42 | 43 | ``` 44 | docker run -d --add-host vnchost:192.168.188.42 -p 80:80 farmer1992/nginx-websockify 45 | ``` 46 | 47 | ### Dynamic vnc upstream with help of [ngx-lua](https://github.com/chaoslawful/lua-nginx-module) 48 | 49 | an example script read ip and port from url params and verify them by md5 50 | 51 | __SECURITY VULNERABILITY WARNING__ 52 | 53 | > this is only an exmaple for you to understand how to work together with ngx-lua 54 | > do NOT use this script in production. 55 | 56 | > anyone who know your private key can connect any machine behind your nginx proxy, 57 | > you should restrict target ip and port in a whitelist. 58 | 59 | 60 | in your `nginx.conf` 61 | 62 | ``` 63 | location /websockify { 64 | 65 | set $vnc_addr ''; 66 | access_by_lua ' 67 | 68 | -- your private key here 69 | local key = "CHANGE_ME_!!!!" 70 | 71 | -- read from url params 72 | local args = ngx.req.get_uri_args() 73 | local ip = args["ip"] or "127.0.0.1" 74 | local port = args["port"] or "5900" 75 | local sign = args["sign"] 76 | local t = tonumber(args["t"]) or 0 77 | local elapse = ngx.time() - t 78 | 79 | -- make sure the signature are generated within 30 seconds 80 | if elapse > 30 or elapse < 0 then 81 | ngx.exit(ngx.HTTP_FORBIDDEN) 82 | end 83 | 84 | local addr = ip .. ":" .. port 85 | 86 | -- verify the signature 87 | if ngx.md5(key .. t .. addr .. key) ~= sign then 88 | ngx.exit(ngx.HTTP_FORBIDDEN) 89 | end 90 | 91 | ngx.var.vnc_addr = addr 92 | '; 93 | 94 | websockify_pass $vnc_addr; 95 | } 96 | ``` 97 | 98 | use ajax call to `vnc_url.php` to retrieve the websockify url, then let noVNC connect to it. 99 | 100 | ``` 101 | $t, 117 | 'sign' => md5($key . $t . "$addr:$port" . $key), 118 | 'ip' => $addr, 119 | 'port' => $port, 120 | )); 121 | ``` 122 | 123 | 124 | 125 | # Directives 126 | 127 | * `websockify_buffer_size`: Default: `65543 = 65535 + 4 + 4 (websocket max frame size + header + mask)` 128 | 129 | The buffer size used to store the encode/decode data. 130 | each websockify connection will cost `websockify_buffer_size` * 2 ( 1 upstream + 1 downstream ) addational memory 131 | 132 | 133 | * `websockify_read_timeout`: Default `60s` 134 | 135 | [proxy_read_timeout](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout) of websockify upstream 136 | 137 | 138 | * `websockify_connect_timeout`: Default `60s` 139 | 140 | [proxy_connect_timeout](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_connect_timeout) of websockify upstream 141 | 142 | 143 | * `websockify_send_timeout`: Default `60s` 144 | 145 | [proxy_send_timeout](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_send_timeout) of websockify upstream 146 | 147 | 148 | # Nginx Compatibility 149 | 150 | * v0.02 - v0.0.3 151 | * 1.7.x (Tested on 1.7.9) 152 | * 1.6.x (Tested on 1.6.2) 153 | 154 | * v0.0.1 155 | 156 | * 1.5.x (Tested on 1.5.9) 157 | * 1.4.x (Tested on 1.4.4) 158 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_module_name=ngx_http_websockify_module 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP_AUX_FILTER 5 | ngx_module_srcs="$ngx_addon_dir/ngx_http_websockify_module.c" 6 | ngx_module_deps="$ngx_addon_dir/websocket.h" 7 | ngx_module_libs=SHA1 8 | 9 | . auto/module 10 | 11 | ngx_addon_name=$ngx_module_name 12 | else 13 | HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_websockify_module" 14 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_websockify_module.c" 15 | NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_addon_dir/websocket.h" 16 | CORE_LIBS="$CORE_LIBS" 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes 1; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | #gzip on; 30 | 31 | include /etc/nginx/conf.d/*.conf; 32 | } 33 | -------------------------------------------------------------------------------- /nginx.vh.default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | #charset koi8-r; 6 | #access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | } 12 | 13 | location /websockify { 14 | websockify_pass vnchost:5901; 15 | } 16 | 17 | #error_page 404 /404.html; 18 | 19 | # redirect server error pages to the static page /50x.html 20 | # 21 | error_page 500 502 503 504 /50x.html; 22 | location = /50x.html { 23 | root /usr/share/nginx/html; 24 | } 25 | 26 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 27 | # 28 | #location ~ \.php$ { 29 | # proxy_pass http://127.0.0.1; 30 | #} 31 | 32 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 33 | # 34 | #location ~ \.php$ { 35 | # root html; 36 | # fastcgi_pass 127.0.0.1:9000; 37 | # fastcgi_index index.php; 38 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 39 | # include fastcgi_params; 40 | #} 41 | 42 | # deny access to .htaccess files, if Apache's document root 43 | # concurs with nginx's one 44 | # 45 | #location ~ /\.ht { 46 | # deny all; 47 | #} 48 | } 49 | 50 | -------------------------------------------------------------------------------- /ngx_http_websockify_module.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Nginx Websockify Module 3 | * Embed websockify into Nginx 4 | * https://github.com/tg123/websockify-nginx-module 5 | * 6 | * WebSocket to TCP Protocol Bridge/Proxy 7 | * SEE ALSO: websockify https://github.com/kanaka/websockify 8 | * 9 | * Copyright (C) 2014 - 2015 10 | * 11 | * The MIT License (MIT) 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | #include "websocket.h" 21 | 22 | #define BUFFER_SIZE (MAX_WEBSOCKET_FRAME_SIZE + MAX_WEBSOCKET_FRAME_HEADER_SIZE + WEBSOCKET_FRAME_MASK_SIZE) 23 | 24 | #define BUFFER_FLUSH_TIMEOUT 20 25 | 26 | #ifdef _MSC_VER 27 | #define WEBSOCKIFY_FUNC __FUNCTION__ 28 | #else 29 | #define WEBSOCKIFY_FUNC __func__ 30 | #endif 31 | 32 | typedef struct ngx_http_websockify_loc_conf_s { 33 | ngx_http_upstream_conf_t upstream; 34 | 35 | ngx_array_t *websockify_lengths; 36 | ngx_array_t *websockify_values; 37 | } ngx_http_websockify_loc_conf_t; 38 | 39 | typedef enum { 40 | WEBSOCKIFY_ENCODING_PROTOCOL_UNSET = 0, 41 | WEBSOCKIFY_ENCODING_PROTOCOL_BASE64, 42 | WEBSOCKIFY_ENCODING_PROTOCOL_BINARY 43 | } websockify_encoding_protocol_e; 44 | 45 | typedef struct ngx_http_websockify_request_ctx_s { 46 | ngx_http_request_t *request; 47 | ngx_flag_t need_cleanup_fake_recv_buff; 48 | ngx_flag_t closed; 49 | websockify_encoding_protocol_e encoding_protocol; 50 | 51 | ngx_buf_t *encode_send_buf; 52 | ngx_buf_t *decode_send_buf; 53 | 54 | ngx_event_t flush_all_ev; 55 | 56 | ngx_send_pt original_ngx_downstream_send; 57 | ngx_send_pt original_ngx_upstream_send; 58 | 59 | ngx_recv_pt original_ngx_upstream_recv; 60 | 61 | } ngx_http_websockify_ctx_t ; 62 | 63 | 64 | ngx_module_t ngx_http_websockify_module; 65 | 66 | static ngx_int_t ngx_http_websockify_handler(ngx_http_request_t *r); 67 | static char *ngx_http_websockify(ngx_conf_t *cf, ngx_command_t *cmd, 68 | void *conf); 69 | 70 | static void *ngx_http_websockify_create_loc_conf(ngx_conf_t *cf); 71 | static char *ngx_http_websockify_merge_loc_conf(ngx_conf_t *cf, void *parent, 72 | void *child); 73 | 74 | static ngx_int_t ngx_http_websockify_create_request(ngx_http_request_t *r); 75 | static ngx_int_t ngx_http_websockify_reinit_request(ngx_http_request_t *r); 76 | static ngx_int_t ngx_http_websockify_process_header(ngx_http_request_t *r); 77 | static void ngx_http_websockify_abort_request(ngx_http_request_t *r); 78 | static void ngx_http_websockify_finalize_request(ngx_http_request_t *r, 79 | ngx_int_t rc); 80 | 81 | 82 | static void ngx_http_websockify_flush_all(ngx_event_t *ev); 83 | 84 | static ssize_t ngx_http_websockify_flush(ngx_connection_t *c, ngx_buf_t *b, 85 | ngx_send_pt send); 86 | 87 | static ngx_inline ssize_t ngx_http_websockify_flush_downstream( 88 | ngx_http_websockify_ctx_t *ctx); 89 | static ngx_inline ssize_t ngx_http_websockify_flush_upstream( 90 | ngx_http_websockify_ctx_t *ctx); 91 | 92 | typedef ssize_t (*ngx_http_websockify_flush_pt)(ngx_http_websockify_ctx_t *ctx); 93 | 94 | static ngx_inline size_t ngx_http_websockify_freesize(ngx_buf_t *b, 95 | size_t size); 96 | 97 | static ssize_t 98 | ngx_http_websockify_send_and_transform(ngx_connection_t *c, u_char *buf, 99 | const size_t size, ngx_send_pt transform, ngx_http_websockify_flush_pt flush); 100 | 101 | static ssize_t ngx_http_websockify_send_downstream_with_encode( 102 | ngx_connection_t *c, 103 | u_char *buf, const size_t size); 104 | 105 | static ssize_t 106 | ngx_http_websockify_encode_one_frame(ngx_connection_t *c, u_char *buf, 107 | const size_t size); 108 | 109 | static ssize_t ngx_http_websockify_send_downstream_frame( 110 | ngx_http_websockify_ctx_t *ctx, u_char opcode, u_char *payload, size_t size); 111 | 112 | static ssize_t ngx_http_websockify_send_upstream_with_decode( 113 | ngx_connection_t *c, u_char *buf, const size_t size); 114 | 115 | static ssize_t 116 | ngx_http_websockify_decode_one_frame(ngx_connection_t *c, u_char *buf, 117 | const size_t size); 118 | 119 | static ssize_t ngx_http_websockify_empty_recv(ngx_connection_t *c, u_char *buf, 120 | size_t size); 121 | 122 | 123 | static ngx_inline size_t ngx_http_websockify_freesize(ngx_buf_t *b, size_t size) 124 | { 125 | 126 | size_t free_size; 127 | 128 | free_size = b->end - b->last; 129 | 130 | if (free_size >= size) { 131 | return free_size; 132 | } 133 | 134 | return 0; 135 | } 136 | 137 | static ngx_inline ssize_t 138 | ngx_http_websockify_flush_downstream(ngx_http_websockify_ctx_t *ctx) 139 | { 140 | ngx_http_request_t *r; 141 | r = ctx->request; 142 | 143 | if (ctx->closed) { 144 | return NGX_ERROR; 145 | } 146 | 147 | if ( r->connection ) { 148 | return ngx_http_websockify_flush(r->connection, ctx->encode_send_buf, 149 | ctx->original_ngx_downstream_send); 150 | } 151 | 152 | return NGX_ERROR; 153 | } 154 | 155 | static ngx_inline ssize_t 156 | ngx_http_websockify_flush_upstream(ngx_http_websockify_ctx_t *ctx) 157 | { 158 | ngx_http_request_t *r; 159 | r = ctx->request; 160 | 161 | if (ctx->closed) { 162 | return NGX_ERROR; 163 | } 164 | 165 | if ( r->upstream->peer.connection ) { 166 | return ngx_http_websockify_flush(r->upstream->peer.connection, 167 | ctx->decode_send_buf, ctx->original_ngx_upstream_send); 168 | } 169 | 170 | return NGX_ERROR; 171 | } 172 | 173 | static void 174 | ngx_http_websockify_flush_all(ngx_event_t *ev) 175 | { 176 | ngx_http_websockify_ctx_t *ctx; 177 | 178 | ctx = ev->data; 179 | 180 | if (ngx_http_websockify_flush_downstream(ctx) == NGX_ERROR 181 | || ngx_http_websockify_flush_upstream(ctx) == NGX_ERROR) { 182 | 183 | ctx->closed = 1; 184 | } 185 | } 186 | 187 | static ssize_t 188 | ngx_http_websockify_flush(ngx_connection_t *c, ngx_buf_t *b, 189 | ngx_send_pt send) 190 | { 191 | ngx_http_websockify_ctx_t *ctx; 192 | ngx_http_request_t *r; 193 | ssize_t n; 194 | 195 | r = c->data; 196 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 197 | 198 | 199 | if (b->last == b->pos) { 200 | return 0; 201 | } 202 | 203 | for (;;) { 204 | n = send(c, b->pos, b->last - b->pos); 205 | 206 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: sent buffer : %d / %d", 207 | WEBSOCKIFY_FUNC, n, b->last - b->pos); 208 | 209 | if (n > 0) { 210 | b->pos += n; 211 | 212 | if (b->pos == b->last) { 213 | b->pos = b->start; 214 | b->last = b->start; 215 | } 216 | } 217 | 218 | if (n == NGX_AGAIN) { 219 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: add timer", WEBSOCKIFY_FUNC); 220 | ngx_add_timer(&(ctx->flush_all_ev), BUFFER_FLUSH_TIMEOUT); 221 | } 222 | 223 | if ( (n <= 0) || (b->pos == b->last) ) { 224 | break; 225 | } 226 | } 227 | 228 | return n; 229 | } 230 | 231 | static ssize_t 232 | ngx_http_websockify_send_downstream_frame(ngx_http_websockify_ctx_t *ctx, 233 | u_char opcode, u_char *payload, size_t size) 234 | { 235 | ngx_buf_t *b; 236 | size_t header_length; 237 | 238 | 239 | 240 | if (ngx_http_websockify_flush_downstream(ctx) == NGX_ERROR ) { 241 | return NGX_ERROR; 242 | } 243 | 244 | header_length = websocket_server_encoded_header_length(size); 245 | 246 | b = ctx->encode_send_buf; 247 | 248 | if ( !ngx_http_websockify_freesize(b, (header_length + size))) { 249 | return NGX_AGAIN; 250 | } 251 | 252 | websocket_server_write_frame_header(b->last, opcode, size); 253 | 254 | if (size > 0) { 255 | ngx_memcpy(b->last + header_length, payload, size); 256 | } 257 | 258 | if (ngx_http_websockify_flush_downstream(ctx) == NGX_ERROR ) { 259 | return NGX_ERROR; 260 | } 261 | 262 | b->last += header_length + size; 263 | 264 | return header_length + size; 265 | } 266 | 267 | static ssize_t 268 | ngx_http_websockify_send_and_transform(ngx_connection_t *c, u_char *buf, 269 | const size_t size, ngx_send_pt transform, ngx_http_websockify_flush_pt flush) 270 | { 271 | ngx_http_websockify_ctx_t *ctx; 272 | ngx_http_request_t *r; 273 | size_t total = 0; 274 | ssize_t consumed; 275 | 276 | r = c->data; 277 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 278 | 279 | if (flush(ctx) == NGX_ERROR ) { 280 | return NGX_ERROR; 281 | } 282 | 283 | while (size > total) { 284 | consumed = transform(c, buf + total, size - total); 285 | 286 | if (consumed == NGX_ERROR) { 287 | return NGX_ERROR; 288 | 289 | } else if (consumed == NGX_AGAIN) { 290 | 291 | if (flush(ctx) > 0) { 292 | continue; 293 | } 294 | 295 | if (total == 0) { 296 | return NGX_AGAIN; 297 | } 298 | 299 | break; 300 | 301 | } else if (consumed > 0) { 302 | 303 | total += consumed; 304 | } else { 305 | // should never happen 306 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: assert failed", 307 | WEBSOCKIFY_FUNC); 308 | return NGX_ERROR; 309 | } 310 | 311 | } 312 | 313 | if (flush(ctx) == NGX_ERROR ) { 314 | return NGX_ERROR; 315 | } 316 | 317 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: %d / %d", WEBSOCKIFY_FUNC, 318 | total , size); 319 | 320 | return total; 321 | } 322 | 323 | static ssize_t 324 | ngx_http_websockify_send_downstream_with_encode(ngx_connection_t *c, 325 | u_char *buf, 326 | const size_t size) 327 | { 328 | 329 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: [%d]", WEBSOCKIFY_FUNC, 330 | size); 331 | 332 | return ngx_http_websockify_send_and_transform(c, buf, size, 333 | ngx_http_websockify_encode_one_frame, ngx_http_websockify_flush_downstream); 334 | } 335 | 336 | static ssize_t 337 | ngx_http_websockify_encode_one_frame(ngx_connection_t *c, 338 | u_char *buf, 339 | const size_t size) 340 | { 341 | ngx_http_websockify_ctx_t *ctx; 342 | ngx_buf_t *b; 343 | ngx_http_request_t *r; 344 | size_t payload_length; 345 | size_t header_length; 346 | 347 | size_t free_size; 348 | size_t consumed_size = 0; 349 | 350 | 351 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: [%d]", WEBSOCKIFY_FUNC, 352 | size); 353 | 354 | r = c->data; 355 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 356 | 357 | b = ctx->encode_send_buf; 358 | free_size = ngx_http_websockify_freesize(b, MIN_SERVER_FRAME_SIZE); 359 | 360 | if ( !free_size ) { 361 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, 362 | "%s: no enough buffer, try again... ", WEBSOCKIFY_FUNC); 363 | return NGX_AGAIN; 364 | } 365 | 366 | free_size = ngx_min(free_size, 367 | MAX_WEBSOCKET_FRAME_SIZE + MAX_WEBSOCKET_FRAME_HEADER_SIZE); 368 | 369 | switch (ctx->encoding_protocol) { 370 | case WEBSOCKIFY_ENCODING_PROTOCOL_BASE64: 371 | 372 | // inverse of ngx_base64_encoded_length. 373 | consumed_size = ngx_min(websocket_payload_consume_size(free_size) / 4 * 3 - 2, 374 | size); 375 | 376 | payload_length = ngx_base64_encoded_length(consumed_size); 377 | header_length = websocket_server_encoded_header_length(payload_length); 378 | 379 | websocket_server_write_frame_header(b->last, WEBSOCKET_OPCODE_TEXT, 380 | payload_length); 381 | 382 | // base64 encode 383 | ngx_str_t src; 384 | ngx_str_t dst; 385 | 386 | src.data = buf; 387 | src.len = consumed_size; 388 | 389 | dst.data = b->last + header_length; 390 | 391 | ngx_encode_base64(&dst, &src); 392 | 393 | break; 394 | 395 | case WEBSOCKIFY_ENCODING_PROTOCOL_BINARY: 396 | 397 | consumed_size = ngx_min(websocket_payload_consume_size(free_size), size); 398 | 399 | payload_length = consumed_size; 400 | header_length = websocket_server_encoded_header_length(payload_length); 401 | 402 | websocket_server_write_frame_header(b->last, WEBSOCKET_OPCODE_BINARY, 403 | payload_length); 404 | 405 | ngx_memcpy(b->last + header_length, buf, consumed_size); 406 | 407 | break; 408 | 409 | default: 410 | // not support 411 | return NGX_ERROR; 412 | break; 413 | } 414 | 415 | b->last += header_length + payload_length; // push encoded data into buffer 416 | 417 | return (ssize_t)consumed_size; 418 | } 419 | 420 | static ssize_t 421 | ngx_http_websockify_send_upstream_with_decode(ngx_connection_t *c, u_char *buf, 422 | const size_t size) 423 | { 424 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: [%d]", WEBSOCKIFY_FUNC, 425 | size); 426 | 427 | return ngx_http_websockify_send_and_transform(c, buf, size, 428 | ngx_http_websockify_decode_one_frame, ngx_http_websockify_flush_upstream); 429 | 430 | } 431 | 432 | static ssize_t 433 | ngx_http_websockify_decode_one_frame(ngx_connection_t *c, u_char *buf, 434 | const size_t size) 435 | { 436 | ngx_http_websockify_ctx_t *ctx; 437 | ngx_buf_t *b; 438 | ngx_http_request_t *r; 439 | websocket_frame_t frame; 440 | 441 | ssize_t header_length; 442 | size_t used_buf_size; 443 | size_t need_buf_size; 444 | ssize_t reply; 445 | 446 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: [%d]", WEBSOCKIFY_FUNC, 447 | size); 448 | 449 | r = c->data; 450 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 451 | 452 | b = ctx->decode_send_buf; 453 | 454 | header_length = websocket_server_decode_next_frame(&frame, buf, size); 455 | 456 | if (header_length == NGX_ERROR) { 457 | ngx_log_error(NGX_LOG_ERR, c->log, 0, 458 | "%s: decoding websocket frame > 65535 is not supported!", 459 | WEBSOCKIFY_FUNC); 460 | return NGX_ERROR; 461 | } 462 | 463 | if (header_length == NGX_AGAIN) { 464 | return NGX_AGAIN; 465 | } 466 | 467 | used_buf_size = 0; 468 | 469 | switch (frame.opcode) { 470 | case WEBSOCKET_OPCODE_CONTINUATION: 471 | case WEBSOCKET_OPCODE_PONG: 472 | // do nothing 473 | break; 474 | 475 | case WEBSOCKET_OPCODE_TEXT: 476 | case WEBSOCKET_OPCODE_BINARY: 477 | 478 | switch (ctx->encoding_protocol) { 479 | case WEBSOCKIFY_ENCODING_PROTOCOL_BASE64: 480 | need_buf_size = ngx_base64_decoded_length(frame.payload_length); 481 | break; 482 | case WEBSOCKIFY_ENCODING_PROTOCOL_BINARY: 483 | need_buf_size = frame.payload_length; 484 | break; 485 | default: 486 | return NGX_ERROR; 487 | break; 488 | } 489 | 490 | if ( !ngx_http_websockify_freesize(b, need_buf_size) ) { 491 | 492 | if (need_buf_size + b->start > b->end) { 493 | ngx_log_error(NGX_LOG_ERR, c->log, 0, "%s: buffer size too small", 494 | WEBSOCKIFY_FUNC); 495 | return NGX_ERROR; 496 | } 497 | 498 | return NGX_AGAIN; 499 | } 500 | 501 | websocket_server_decode_unmask_payload(&frame); 502 | 503 | 504 | switch (ctx->encoding_protocol) { 505 | case WEBSOCKIFY_ENCODING_PROTOCOL_BASE64:; 506 | 507 | // base64 decode the data 508 | ngx_str_t src; 509 | ngx_str_t dst; 510 | 511 | src.data = frame.payload; 512 | src.len = frame.payload_length; 513 | 514 | dst.data = b->last; 515 | 516 | if ( ngx_decode_base64(&dst, &src) != NGX_OK ) { 517 | ngx_log_error(NGX_LOG_ERR, c->log, 0, 518 | "%s: decode websocket base64 frame payload error!", WEBSOCKIFY_FUNC); 519 | return NGX_ERROR; 520 | } 521 | 522 | used_buf_size = dst.len; 523 | break; 524 | case WEBSOCKIFY_ENCODING_PROTOCOL_BINARY: 525 | ngx_memcpy(b->last, frame.payload, frame.payload_length); 526 | 527 | used_buf_size = frame.payload_length; 528 | 529 | break; 530 | default: 531 | return NGX_ERROR; 532 | break; 533 | } 534 | 535 | 536 | break; 537 | 538 | case WEBSOCKET_OPCODE_CLOSE: 539 | // TODO testcases 540 | // TODO status code hardcoded (1000 = 03e8) 541 | reply = ngx_http_websockify_send_downstream_frame(ctx, WEBSOCKET_OPCODE_CLOSE, 542 | (u_char *)"\x03\xe8 Closed", 9); 543 | 544 | if (reply < 0) { 545 | return reply; 546 | } 547 | 548 | ctx->closed = 1; 549 | 550 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: CLOSE replied [%d]", 551 | WEBSOCKIFY_FUNC, 552 | size); 553 | 554 | return NGX_ERROR; // let ngx close connection 555 | break; 556 | 557 | case WEBSOCKET_OPCODE_PING: 558 | 559 | // TODO testcases 560 | 561 | if ( !ngx_http_websockify_freesize(ctx->encode_send_buf, 562 | websocket_server_encoded_length(frame.payload_length))) { 563 | return NGX_AGAIN; 564 | } 565 | 566 | websocket_server_decode_unmask_payload(&frame); 567 | 568 | reply = ngx_http_websockify_send_downstream_frame(ctx, WEBSOCKET_OPCODE_PONG, 569 | frame.payload, frame.payload_length); 570 | 571 | if (reply < 0) { 572 | return reply; 573 | } 574 | 575 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "%s: PING replied [%d]", 576 | WEBSOCKIFY_FUNC, 577 | size); 578 | 579 | break; 580 | 581 | default: 582 | ngx_log_error(NGX_LOG_ALERT, c->log, 0, "%s: unsupported opcode: [%d] ", 583 | WEBSOCKIFY_FUNC, frame.opcode); 584 | break; 585 | } 586 | 587 | b->last += used_buf_size; // push decoded data into buffer 588 | 589 | return (ssize_t)(header_length + frame.payload_length); 590 | } 591 | 592 | static ngx_command_t ngx_http_websockify_commands[] = { 593 | { 594 | ngx_string("websockify_pass"), 595 | NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_TAKE1, 596 | &ngx_http_websockify, 597 | NGX_HTTP_LOC_CONF_OFFSET, 598 | 0, 599 | NULL 600 | }, 601 | 602 | { 603 | ngx_string("websockify_buffer_size"), 604 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 605 | ngx_conf_set_size_slot, 606 | NGX_HTTP_LOC_CONF_OFFSET, 607 | offsetof(ngx_http_websockify_loc_conf_t, upstream.buffer_size), 608 | NULL 609 | }, 610 | 611 | { 612 | ngx_string("websockify_connect_timeout"), 613 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 614 | ngx_conf_set_msec_slot, 615 | NGX_HTTP_LOC_CONF_OFFSET, 616 | offsetof(ngx_http_websockify_loc_conf_t, upstream.connect_timeout), 617 | NULL 618 | }, 619 | 620 | { 621 | ngx_string("websockify_send_timeout"), 622 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 623 | ngx_conf_set_msec_slot, 624 | NGX_HTTP_LOC_CONF_OFFSET, 625 | offsetof(ngx_http_websockify_loc_conf_t, upstream.send_timeout), 626 | NULL 627 | }, 628 | 629 | { 630 | ngx_string("websockify_read_timeout"), 631 | NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 632 | ngx_conf_set_msec_slot, 633 | NGX_HTTP_LOC_CONF_OFFSET, 634 | offsetof(ngx_http_websockify_loc_conf_t, upstream.read_timeout), 635 | NULL 636 | }, 637 | 638 | ngx_null_command 639 | }; 640 | 641 | 642 | static void * 643 | ngx_http_websockify_create_loc_conf(ngx_conf_t *cf) 644 | { 645 | ngx_http_websockify_loc_conf_t *conf; 646 | 647 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_websockify_loc_conf_t)); 648 | if (conf == NULL) { 649 | return NULL; 650 | } 651 | 652 | /* 653 | * set by ngx_pcalloc(): 654 | * 655 | * conf->upstream.bufs.num = 0; 656 | * conf->upstream.next_upstream = 0; 657 | * conf->upstream.temp_path = NULL; 658 | * conf->upstream.uri = { 0, NULL }; 659 | * conf->upstream.location = NULL; 660 | */ 661 | 662 | conf->upstream.local = NGX_CONF_UNSET_PTR; 663 | conf->upstream.connect_timeout = NGX_CONF_UNSET_MSEC; 664 | conf->upstream.send_timeout = NGX_CONF_UNSET_MSEC; 665 | conf->upstream.read_timeout = NGX_CONF_UNSET_MSEC; 666 | 667 | conf->upstream.buffer_size = NGX_CONF_UNSET_SIZE; 668 | //conf->upstream.buffer_size = BUFFER_SIZE; 669 | 670 | /* the hardcoded values */ 671 | conf->upstream.cyclic_temp_file = 0; 672 | conf->upstream.buffering = 0; 673 | //conf->upstream.buffer_size = 0; 674 | //conf->upstream.busy_buffers_size = 0; 675 | conf->upstream.ignore_client_abort = 0; 676 | conf->upstream.send_lowat = 0; 677 | conf->upstream.bufs.num = 0; 678 | conf->upstream.busy_buffers_size = 0; 679 | conf->upstream.max_temp_file_size = 0; 680 | conf->upstream.temp_file_write_size = 0; 681 | conf->upstream.intercept_errors = 1; 682 | conf->upstream.intercept_404 = 1; 683 | conf->upstream.pass_request_headers = 0; 684 | conf->upstream.pass_request_body = 0; 685 | 686 | return conf; 687 | } 688 | 689 | static char * 690 | ngx_http_websockify_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 691 | { 692 | ngx_http_websockify_loc_conf_t *prev = parent; 693 | ngx_http_websockify_loc_conf_t *conf = child; 694 | 695 | ngx_conf_merge_ptr_value(conf->upstream.local, 696 | prev->upstream.local, NULL); 697 | 698 | ngx_conf_merge_msec_value(conf->upstream.connect_timeout, 699 | prev->upstream.connect_timeout, 60000); 700 | 701 | ngx_conf_merge_msec_value(conf->upstream.send_timeout, 702 | prev->upstream.send_timeout, 60000); 703 | 704 | ngx_conf_merge_msec_value(conf->upstream.read_timeout, 705 | prev->upstream.read_timeout, 60000); 706 | 707 | ngx_conf_merge_size_value(conf->upstream.buffer_size, 708 | prev->upstream.buffer_size, 709 | (size_t) BUFFER_SIZE); 710 | 711 | ngx_conf_merge_bitmask_value(conf->upstream.next_upstream, 712 | prev->upstream.next_upstream, 713 | (NGX_CONF_BITMASK_SET 714 | | NGX_HTTP_UPSTREAM_FT_ERROR 715 | | NGX_HTTP_UPSTREAM_FT_TIMEOUT)); 716 | 717 | if (conf->upstream.next_upstream & NGX_HTTP_UPSTREAM_FT_OFF) { 718 | conf->upstream.next_upstream = NGX_CONF_BITMASK_SET 719 | | NGX_HTTP_UPSTREAM_FT_OFF; 720 | } 721 | 722 | if (conf->upstream.upstream == NULL) { 723 | conf->upstream.upstream = prev->upstream.upstream; 724 | } 725 | 726 | if (conf->websockify_lengths == NULL) { 727 | conf->websockify_lengths = prev->websockify_lengths; 728 | conf->websockify_values = prev->websockify_values; 729 | } 730 | if (conf->upstream.buffer_size == 0) { 731 | conf->upstream.buffer_size = BUFFER_SIZE; 732 | } 733 | 734 | return NGX_CONF_OK; 735 | } 736 | 737 | static ngx_http_module_t ngx_http_websockify_module_ctx = { 738 | NULL, /* preconfiguration */ 739 | NULL, /* postconfiguration */ 740 | 741 | NULL, /* create main configuration */ 742 | NULL, /* init main configuration */ 743 | 744 | NULL, /* create server configuration */ 745 | NULL, /* merge server configuration */ 746 | 747 | ngx_http_websockify_create_loc_conf, /* create location configuration */ 748 | ngx_http_websockify_merge_loc_conf /* merge location configuration */ 749 | }; 750 | 751 | ngx_module_t ngx_http_websockify_module = { 752 | NGX_MODULE_V1, 753 | &ngx_http_websockify_module_ctx, /* module context */ 754 | ngx_http_websockify_commands, /* module directives */ 755 | NGX_HTTP_MODULE, /* module type */ 756 | NULL, /* init master */ 757 | NULL, /* init module */ 758 | NULL, /* init process */ 759 | NULL, /* init thread */ 760 | NULL, /* exit thread */ 761 | NULL, /* exit process */ 762 | NULL, /* exit master */ 763 | NGX_MODULE_V1_PADDING 764 | }; 765 | 766 | static ngx_int_t 767 | ngx_http_websockify_handler(ngx_http_request_t *r) 768 | { 769 | ngx_int_t rc; 770 | ngx_http_upstream_t *u; 771 | ngx_http_websockify_loc_conf_t *wlcf; 772 | ngx_http_websockify_ctx_t *ctx; 773 | ngx_str_t var_pass; 774 | ngx_url_t url; 775 | 776 | wlcf = ngx_http_get_module_loc_conf(r, ngx_http_websockify_module); 777 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 778 | 779 | u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); 780 | if (u == NULL) { 781 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 782 | } 783 | 784 | if ( wlcf->websockify_lengths ) { // parse from var 785 | 786 | if (ngx_http_script_run(r, &var_pass, wlcf->websockify_lengths->elts, 0, 787 | wlcf->websockify_values->elts) == NULL) { 788 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 789 | } 790 | 791 | u->resolved = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t)); 792 | if (u->resolved == NULL) { 793 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 794 | } 795 | 796 | ngx_memzero(&url, sizeof(ngx_url_t)); 797 | 798 | url.url.len = var_pass.len; 799 | url.url.data = var_pass.data; 800 | url.uri_part = 1; 801 | url.no_resolve = 1; 802 | 803 | if (ngx_parse_url(r->pool, &url) != NGX_OK) { 804 | if (url.err) { 805 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 806 | "%s in upstream \"%V\"", url.err, &url.url); 807 | } 808 | 809 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 810 | } 811 | 812 | u->resolved->host = url.host; 813 | u->resolved->port = (in_port_t) (url.no_port ? 5900 : url.port); 814 | u->resolved->no_port = url.no_port; 815 | 816 | if (url.addrs && url.addrs[0].sockaddr) { 817 | u->resolved->sockaddr = url.addrs[0].sockaddr; 818 | u->resolved->socklen = url.addrs[0].socklen; 819 | u->resolved->naddrs = 1; 820 | } 821 | 822 | } 823 | 824 | u->schema.len = sizeof("websockify://") - 1; 825 | u->schema.data = (u_char *) "websockify://"; 826 | 827 | u->peer.log = r->connection->log; 828 | u->peer.log_error = NGX_ERROR_ERR; 829 | 830 | u->output.tag = (ngx_buf_tag_t) &ngx_http_websockify_module; 831 | 832 | u->conf = &wlcf->upstream; 833 | 834 | u->create_request = ngx_http_websockify_create_request; 835 | u->reinit_request = ngx_http_websockify_reinit_request; 836 | u->process_header = ngx_http_websockify_process_header; 837 | u->abort_request = ngx_http_websockify_abort_request; 838 | u->finalize_request = ngx_http_websockify_finalize_request; 839 | 840 | r->upstream = u; 841 | 842 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_websockify_ctx_t)); 843 | if (ctx == NULL) { 844 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 845 | } 846 | 847 | ctx->request = r; 848 | ctx->encode_send_buf = ngx_create_temp_buf(r->pool, u->conf->buffer_size); 849 | ctx->decode_send_buf = ngx_create_temp_buf(r->pool, u->conf->buffer_size); 850 | 851 | ctx->encoding_protocol = WEBSOCKIFY_ENCODING_PROTOCOL_UNSET; 852 | 853 | ctx->flush_all_ev.log = r->connection->log; 854 | ctx->flush_all_ev.data = ctx; 855 | ctx->flush_all_ev.handler = ngx_http_websockify_flush_all; 856 | 857 | ctx-> closed = 0; 858 | 859 | if (!ctx->encode_send_buf || !ctx->decode_send_buf) { 860 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 861 | } 862 | 863 | ngx_http_set_ctx(r, ctx, ngx_http_websockify_module); 864 | 865 | //ngx_http_upstream_init(r); 866 | rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init); 867 | if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { 868 | return rc; 869 | } 870 | 871 | return NGX_DONE; 872 | } 873 | 874 | static ngx_int_t 875 | ngx_http_websockify_create_request(ngx_http_request_t *r) 876 | { 877 | ngx_http_websockify_ctx_t *ctx; 878 | 879 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 880 | 881 | if ( ctx == NULL ) { 882 | return NGX_ERROR; 883 | } 884 | 885 | if ( r->method != NGX_HTTP_GET ) { 886 | r->headers_out.status = NGX_HTTP_BAD_REQUEST; 887 | return NGX_OK; 888 | } 889 | 890 | // tricky, let nginx call my reinit 891 | // do nothing to tcp connections 892 | r->upstream->request_sent = 1; 893 | 894 | return NGX_OK; 895 | } 896 | 897 | static ssize_t 898 | ngx_http_websockify_empty_recv(ngx_connection_t *c, u_char *buf, size_t size) 899 | { 900 | ngx_http_request_t *r; 901 | ngx_http_websockify_ctx_t *ctx; 902 | ssize_t n; 903 | 904 | r = c->data; 905 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 906 | 907 | n = ctx->original_ngx_upstream_recv(c, buf, 1); 908 | c->recv = ctx->original_ngx_upstream_recv; 909 | 910 | // tricky, if n == NGX_ERROR upstream might not have connections now 911 | // return control to nginx and let nginx deal with it 912 | if (n == NGX_ERROR) { 913 | return NGX_AGAIN; 914 | } 915 | 916 | if (n == NGX_AGAIN) { 917 | ctx->need_cleanup_fake_recv_buff = 1; 918 | return 1; 919 | } 920 | 921 | ctx->need_cleanup_fake_recv_buff = 0; 922 | return n; 923 | } 924 | 925 | static ngx_int_t 926 | ngx_http_websockify_reinit_request(ngx_http_request_t *r) 927 | { 928 | ngx_http_upstream_t *u; 929 | ngx_http_websockify_ctx_t *ctx; 930 | 931 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 932 | 933 | if (r->header_sent) { 934 | return NGX_OK; 935 | } 936 | 937 | u = r->upstream; 938 | // hack for empty reply tcp connection 939 | ctx->original_ngx_upstream_recv = u->peer.connection->recv; 940 | u->peer.connection->recv = ngx_http_websockify_empty_recv; 941 | 942 | u->read_event_handler(r, r->upstream); 943 | 944 | // this happens if some error occurs during read_event_handler 945 | if ( u->peer.connection == NULL ) { 946 | return NGX_ERROR; 947 | } 948 | 949 | return NGX_OK; 950 | } 951 | 952 | static ngx_int_t 953 | ngx_http_websockify_process_header(ngx_http_request_t *r) 954 | { 955 | ngx_http_websockify_ctx_t *ctx; 956 | ngx_http_upstream_t *u; 957 | 958 | ngx_table_elt_t *h; 959 | ngx_list_part_t *part; 960 | ngx_uint_t i; 961 | 962 | ngx_sha1_t sha1; 963 | 964 | ngx_str_t ws_key = {0, NULL}; 965 | ngx_flag_t accept_binary = 0; 966 | ngx_flag_t accept_base64 = 0; 967 | 968 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 969 | "websockify : ngx_http_websockify_process_header"); 970 | 971 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 972 | 973 | if ( ctx == NULL ) { 974 | return NGX_ERROR; 975 | } 976 | 977 | u = r->upstream; 978 | 979 | if (ctx->need_cleanup_fake_recv_buff) { 980 | u->buffer.last = u->buffer.start; 981 | } 982 | 983 | part = &r->headers_in.headers.part; 984 | h = part->elts; 985 | 986 | for (i = 0; /* void */; i++) { 987 | 988 | if (i >= part->nelts) { 989 | if (part->next == NULL) { 990 | break; 991 | } 992 | 993 | part = part->next; 994 | h = part->elts; 995 | i = 0; 996 | } 997 | 998 | if (ngx_strncasecmp(h[i].key.data, (u_char *) "Sec-WebSocket-Key", 999 | h[i].key.len) == 0) { 1000 | 1001 | ngx_str_t src; 1002 | u_char src_data[20]; 1003 | 1004 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 1005 | "websockify : found SEC_WEBSOCKET_KEY : %s", h[i].value.data); 1006 | 1007 | src.data = src_data; 1008 | src.len = 20; 1009 | 1010 | ngx_sha1_init(&sha1); 1011 | ngx_sha1_update(&sha1, h[i].value.data, h[i].value.len); 1012 | ngx_sha1_update(&sha1, HYBI_GUID, 36); 1013 | ngx_sha1_final(src.data, &sha1); 1014 | 1015 | ws_key.data = ngx_palloc(r->pool, HYBI10_ACCEPTHDRLEN); 1016 | 1017 | ngx_encode_base64(&ws_key, &src); 1018 | 1019 | } else if (ngx_strncasecmp(h[i].key.data, (u_char *) "Sec-WebSocket-Protocol", 1020 | h[i].key.len) == 0) { 1021 | 1022 | if (ngx_strstrn(h[i].value.data, "base64", 6 - 1)) { 1023 | accept_base64 = 1; 1024 | } 1025 | 1026 | if (ngx_strstrn(h[i].value.data, "binary", 6 - 1)) { 1027 | accept_binary = 1; 1028 | } 1029 | 1030 | } 1031 | } 1032 | 1033 | if ( ws_key.len > 0 && ( accept_base64 || accept_binary ) ) { 1034 | 1035 | u->headers_in.status_n = NGX_HTTP_SWITCHING_PROTOCOLS; 1036 | ngx_str_set(&u->headers_in.status_line, "101 Switching Protocols"); 1037 | u->headers_in.content_length_n = -1; 1038 | 1039 | h = ngx_list_push(&r->headers_out.headers); 1040 | h->hash = 1; 1041 | ngx_str_set(&h->key, "Sec-WebSocket-Accept"); 1042 | h->value = ws_key; 1043 | 1044 | h = ngx_list_push(&r->headers_out.headers); 1045 | h->hash = 1; 1046 | ngx_str_set(&h->key, "Upgrade"); 1047 | ngx_str_set(&h->value, "websocket"); 1048 | 1049 | h = ngx_list_push(&r->headers_out.headers); 1050 | h->hash = 1; 1051 | ngx_str_set(&h->key, "Sec-WebSocket-Protocol"); 1052 | 1053 | if ( accept_binary ) { 1054 | ngx_str_set(&h->value, "binary"); 1055 | ctx->encoding_protocol = WEBSOCKIFY_ENCODING_PROTOCOL_BINARY; 1056 | } else { 1057 | ngx_str_set(&h->value, "base64"); 1058 | ctx->encoding_protocol = WEBSOCKIFY_ENCODING_PROTOCOL_BASE64; 1059 | } 1060 | 1061 | u->state->status = u->headers_in.status_n; 1062 | u->upgrade = 1; 1063 | 1064 | if ( r->connection->send != ngx_http_websockify_send_downstream_with_encode ) { 1065 | 1066 | ctx->original_ngx_downstream_send = r->connection->send; 1067 | 1068 | r->connection->send = ngx_http_websockify_send_downstream_with_encode; 1069 | } 1070 | 1071 | if ( r->upstream->peer.connection->send != 1072 | ngx_http_websockify_send_upstream_with_decode ) { 1073 | 1074 | ctx->original_ngx_upstream_send = r->upstream->peer.connection->send; 1075 | 1076 | r->upstream->peer.connection->send = 1077 | ngx_http_websockify_send_upstream_with_decode; 1078 | } 1079 | 1080 | 1081 | 1082 | } else { 1083 | u->headers_in.status_n = NGX_HTTP_BAD_REQUEST; 1084 | } 1085 | 1086 | return NGX_OK; 1087 | } 1088 | 1089 | static void 1090 | ngx_http_websockify_abort_request(ngx_http_request_t *r) 1091 | { 1092 | return; 1093 | } 1094 | 1095 | static void 1096 | ngx_http_websockify_finalize_request(ngx_http_request_t *r, ngx_int_t rc) 1097 | { 1098 | ngx_http_websockify_ctx_t *ctx; 1099 | 1100 | ctx = ngx_http_get_module_ctx(r, ngx_http_websockify_module); 1101 | ctx->closed = 1; 1102 | 1103 | if (ctx->flush_all_ev.timer_set) { 1104 | ngx_del_timer(&(ctx->flush_all_ev)); 1105 | } 1106 | 1107 | return; 1108 | } 1109 | 1110 | static char * 1111 | ngx_http_websockify(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 1112 | { 1113 | ngx_http_websockify_loc_conf_t *wlcf = conf; 1114 | 1115 | ngx_str_t *value, *url; 1116 | ngx_url_t u; 1117 | ngx_uint_t n; 1118 | ngx_http_core_loc_conf_t *clcf; 1119 | ngx_http_script_compile_t sc; 1120 | 1121 | if (wlcf->upstream.upstream || wlcf->websockify_lengths) { 1122 | return "is duplicate"; 1123 | } 1124 | 1125 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 1126 | clcf->handler = ngx_http_websockify_handler; 1127 | 1128 | // TODO websockify:// 1129 | value = cf->args->elts; 1130 | 1131 | url = &value[1]; 1132 | 1133 | n = ngx_http_script_variables_count(url); 1134 | 1135 | if (n) { 1136 | 1137 | ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); 1138 | 1139 | sc.cf = cf; 1140 | sc.source = url; 1141 | sc.lengths = &wlcf->websockify_lengths; 1142 | sc.values = &wlcf->websockify_values; 1143 | sc.variables = n; 1144 | sc.complete_lengths = 1; 1145 | sc.complete_values = 1; 1146 | 1147 | if (ngx_http_script_compile(&sc) != NGX_OK) { 1148 | return NGX_CONF_ERROR; 1149 | } 1150 | 1151 | return NGX_CONF_OK; 1152 | } 1153 | 1154 | ngx_memzero(&u, sizeof(ngx_url_t)); 1155 | u.url.data = url->data; 1156 | u.url.len = url->len; 1157 | u.default_port = 5900; 1158 | u.no_resolve = 1; 1159 | 1160 | wlcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0); 1161 | if (wlcf->upstream.upstream == NULL) { 1162 | return NGX_CONF_ERROR; 1163 | } 1164 | 1165 | return NGX_CONF_OK; 1166 | } 1167 | -------------------------------------------------------------------------------- /t/handshake.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket; 2 | 3 | plan tests => 13; 4 | 5 | log_level('debug'); 6 | 7 | run_tests(); 8 | 9 | __DATA__ 10 | 11 | === TEST 1: websocket handshake 12 | --- config 13 | location /websockify { 14 | websockify_pass 127.0.0.1:5901; 15 | } 16 | --- tcp_listen: 5901 17 | --- tcp_reply: RFB 18 | --- request 19 | GET /websockify 20 | --- more_headers 21 | Origin:http://0 22 | Sec-WebSocket-Key:n2wfgJF+qto2ahU4+aoNkQ== 23 | Sec-WebSocket-Protocol:base64 24 | Sec-WebSocket-Version:13 25 | Upgrade:websocket 26 | --- error_code: 101 27 | --- response_headers 28 | Connection:upgrade 29 | Sec-WebSocket-Accept:avyE+tn9ibLdRUFx8CXeJlSusVA= 30 | Sec-WebSocket-Protocol:base64 31 | Upgrade:websocket 32 | --- abort 33 | 34 | 35 | 36 | === TEST 2: bad upstream 37 | --- config 38 | location /websockify { 39 | websockify_pass 127.0.0.1:5901; 40 | } 41 | --- request 42 | GET /websockify 43 | --- more_headers 44 | Origin:http://0 45 | Sec-WebSocket-Key:n2wfgJF+qto2ahU4+aoNkQ== 46 | Sec-WebSocket-Protocol:base64 47 | Sec-WebSocket-Version:13 48 | Upgrade:websocket 49 | --- error_code: 502 50 | 51 | 52 | 53 | === TEST 3: bad handshake header 54 | --- config 55 | location /websockify { 56 | websockify_pass 127.0.0.1:5901; 57 | } 58 | --- tcp_listen: 5901 59 | --- tcp_reply: RFB 60 | --- request 61 | GET /websockify 62 | --- error_code: 400 63 | --- abort 64 | 65 | 66 | === TEST 4: select protocol 67 | --- config 68 | location /websockify { 69 | websockify_pass 127.0.0.1:5901; 70 | } 71 | --- tcp_listen: 5901 72 | --- tcp_reply: RFB 73 | --- request 74 | GET /websockify 75 | --- more_headers 76 | Origin:http://0 77 | Sec-WebSocket-Key:n2wfgJF+qto2ahU4+aoNkQ== 78 | Sec-WebSocket-Protocol:base64, binary 79 | Sec-WebSocket-Version:13 80 | Upgrade:websocket 81 | --- error_code: 101 82 | --- response_headers 83 | Connection:upgrade 84 | Sec-WebSocket-Accept:avyE+tn9ibLdRUFx8CXeJlSusVA= 85 | Sec-WebSocket-Protocol:binary 86 | Upgrade:websocket 87 | --- abort 88 | 89 | 90 | 91 | === TEST 5: unsupported protocol 92 | --- config 93 | location /websockify { 94 | websockify_pass 127.0.0.1:5901; 95 | } 96 | --- tcp_listen: 5901 97 | --- tcp_reply: RFB 98 | --- request 99 | GET /websockify 100 | --- more_headers 101 | Origin:http://0 102 | Sec-WebSocket-Key:n2wfgJF+qto2ahU4+aoNkQ== 103 | Sec-WebSocket-Protocol: unsupported 104 | Sec-WebSocket-Version:13 105 | Upgrade:websocket 106 | --- error_code: 400 107 | --- abort 108 | -------------------------------------------------------------------------------- /t/transport.t: -------------------------------------------------------------------------------- 1 | use IO::Socket::INET; 2 | use IO::Select; 3 | use Protocol::WebSocket::Client; 4 | use Test::Nginx::Socket; 5 | #use Test::More; 6 | use Scope::OnExit; 7 | 8 | my $XTcpServerPid; 9 | 10 | sub kill_tcp_server() { 11 | if (defined $XTcpServerPid) { 12 | Test::Nginx::Util::kill_process($XTcpServerPid, 1); 13 | undef $XTcpServerPid; 14 | } 15 | } 16 | 17 | # tcp_listen only respones once, make one myself. echo server only 18 | # x_tcp_listen {{{ 19 | add_block_preprocessor(sub { 20 | my $block = shift; 21 | 22 | $block->set_value("request", "GET /"); 23 | 24 | if (defined $block->x_tcp_listen) { 25 | kill_tcp_server(); 26 | 27 | my $pid = fork(); 28 | 29 | if (!defined $pid) { 30 | bail_out($block->name . " fork() failed: $!"); 31 | 32 | } elsif ($pid == 0) { 33 | 34 | my $server = IO::Socket::INET->new ( 35 | Proto => 'tcp', 36 | LocalHost => '127.0.0.1', 37 | LocalPort => $block->x_tcp_listen, 38 | Listen => 5, 39 | Reuse => 1, 40 | ) or bail_out("cannot create listen to " . $block->x_tcp_listen); 41 | 42 | #$server->autoflush(1); 43 | 44 | while (1) { 45 | my $client = $server->accept(); 46 | 47 | while(1) { 48 | my $buf; 49 | $client->recv($buf, 4096); 50 | last if not $client->send($buf); 51 | sleep 1; 52 | } 53 | } 54 | 55 | } else { 56 | # main 57 | $XTcpServerPid = $pid; 58 | } 59 | } 60 | 61 | return $block; 62 | }); 63 | # }}} 64 | 65 | add_response_body_check(sub { 66 | my ($block, $body, $req_idx, $repeated_req_idx, $dry_run) = @_; 67 | 68 | on_scope_exit { kill_tcp_server(); }; 69 | 70 | return unless defined $block->websockify_url; 71 | 72 | my $url = $block->websockify_url; 73 | 74 | my $socket = IO::Socket::INET->new ( 75 | PeerHost => '127.0.0.1', 76 | PeerPort => $ENV{TEST_NGINX_SERVER_PORT}, 77 | Proto => 'tcp', 78 | ) or bail_out ("cannont connect to server"); 79 | 80 | my $client = Protocol::WebSocket::Client->new(url => 'ws://127.0.0.1:' . $ENV{TEST_NGINX_SERVER_PORT} . $url); 81 | 82 | my $protocol = "binary"; 83 | $protocol = $block->websockify_protocol if defined $block->websockify_protocol; 84 | 85 | my @expect_resp = @{ $block->websockify_frame_response }; 86 | 87 | $client->on( 88 | read => sub { 89 | my $client = shift; 90 | my ($resp) = @_; 91 | 92 | my $expect = shift @expect_resp; 93 | 94 | is_str($resp, $expect); 95 | } 96 | ); 97 | 98 | $client->on( 99 | error => sub { 100 | my $client = shift; 101 | my ($error) = @_; 102 | 103 | bail_out($error); 104 | } 105 | ); 106 | 107 | $client->on( 108 | write => sub { 109 | my $client = shift; 110 | my ($buf) = @_; 111 | 112 | if (!$client->{hs}->is_done) { 113 | $buf =~ s/(Sec-WebSocket-Version.*)\r/$1\r\nSec-WebSocket-Protocol: $protocol/; 114 | } 115 | 116 | $socket->send($buf); 117 | } 118 | ); 119 | 120 | $client->connect(); 121 | 122 | my $buf; 123 | 124 | foreach (@{ $block->websockify_frame_request}) { 125 | # send 126 | $client->write($_); 127 | 128 | # recv 129 | $socket->recv($buf, 65535 + 4 + 4); # max frame size 130 | $client->read($buf); 131 | } 132 | 133 | while ( @expect_resp ) { 134 | $socket->recv($buf, 65535 + 4 + 4); # max frame size 135 | $client->read($buf); 136 | 137 | } 138 | }); 139 | 140 | # a bit stupid 141 | plan tests => 23; 142 | 143 | run_tests(); 144 | 145 | __DATA__ 146 | === TEST 1: send and recv binary data 147 | --- config 148 | location /websockify { 149 | websockify_pass 127.0.0.1:5901; 150 | } 151 | --- websockify_url: /websockify 152 | --- x_tcp_listen: 5901 153 | --- websockify_frame_request eval 154 | ["hello", "world"] 155 | --- websockify_frame_response eval 156 | ["hello", "world"] 157 | 158 | 159 | === TEST 2: send and recv base64 data 160 | --- config 161 | location /websockify { 162 | websockify_pass 127.0.0.1:5901; 163 | } 164 | --- websockify_url: /websockify 165 | --- x_tcp_listen: 5901 166 | --- websockify_protocol: base64 167 | --- websockify_frame_request eval 168 | ["aGVsbG8=", "d29ybGQ="] 169 | --- websockify_frame_response eval 170 | ["aGVsbG8=", "d29ybGQ="] 171 | 172 | 173 | === TEST 3: big packet 174 | --- config 175 | location /websockify { 176 | websockify_pass 127.0.0.1:5901; 177 | } 178 | --- websockify_url: /websockify 179 | --- x_tcp_listen: 5901 180 | --- websockify_frame_request eval 181 | ["0" x 65535, "0"] 182 | --- websockify_frame_response eval 183 | [ ("0" x 4096) x 16 ] 184 | -------------------------------------------------------------------------------- /websocket.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Nginx Websockify Module 3 | * Embed websockify into Nginx 4 | * https://github.com/tg123/websockify-nginx-module 5 | * 6 | * WebSocket to TCP Protocol Bridge/Proxy 7 | * SEE ALSO: websockify https://github.com/kanaka/websockify 8 | * 9 | * Copyright (C) 2014 - 2015 10 | * 11 | * The MIT License (MIT) 12 | */ 13 | 14 | #ifndef _WEBSOCKET_H_INCLUDED_ 15 | #define _WEBSOCKET_H_INCLUDED_ 16 | 17 | #include 18 | #include 19 | 20 | 21 | #define HYBI_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 22 | #define HYBI10_ACCEPTHDRLEN 29 23 | 24 | #define MAX_WEBSOCKET_FRAME_SIZE 65535 25 | 26 | 27 | // https://tools.ietf.org/html/rfc6455#section-5.1 28 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 29 | // +-+-+-+-+-------+-+-------------+-------------------------------+ 30 | // |F|R|R|R| opcode|M| Payload len | Extended payload length | 31 | // |I|S|S|S| (4) |A| (7) | (16/64) | 32 | // |N|V|V|V| |S| | (if payload len==126/127) | 33 | // | |1|2|3| |K| | | 34 | // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 35 | // | Extended payload length continued, if payload len == 127 | 36 | // + - - - - - - - - - - - - - - - +-------------------------------+ 37 | // | |Masking-key, if MASK set to 1 | 38 | // +-------------------------------+-------------------------------+ 39 | // | Masking-key (continued) | Payload Data | 40 | // +-------------------------------- - - - - - - - - - - - - - - - + 41 | // : Payload Data continued ... : 42 | // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 43 | // | Payload Data continued ... | 44 | // +---------------------------------------------------------------+ 45 | 46 | 47 | #define WEBSOCKET_OPCODE_CONTINUATION 0x0 48 | #define WEBSOCKET_OPCODE_TEXT 0x1 49 | #define WEBSOCKET_OPCODE_BINARY 0x2 50 | 51 | #define WEBSOCKET_OPCODE_CLOSE 0x8 52 | #define WEBSOCKET_OPCODE_PING 0x9 53 | #define WEBSOCKET_OPCODE_PONG 0xA 54 | 55 | 56 | typedef struct websocket_frame_s { 57 | u_char opcode; 58 | size_t payload_length; 59 | 60 | u_char *mask; 61 | u_char *payload; 62 | } websocket_frame_t; 63 | 64 | #define MIN_WEBSOCKET_FRAME_HEADER_SIZE 2 65 | #define MAX_WEBSOCKET_FRAME_HEADER_SIZE 4 66 | #define WEBSOCKET_FRAME_MASK_SIZE 4 67 | 68 | 69 | // for server encode only 70 | #define websocket_server_encoded_header_length(size) (((size) <= 125) ? (MIN_WEBSOCKET_FRAME_HEADER_SIZE) : (MAX_WEBSOCKET_FRAME_HEADER_SIZE)) 71 | #define websocket_server_encoded_length(size) (websocket_server_encoded_header_length(size) + size) 72 | 73 | #define websocket_server_decoded_header_length(size) (websocket_server_encoded_header_length(size) + WEBSOCKET_FRAME_MASK_SIZE) 74 | 75 | // 1 char frame 76 | #define MIN_SERVER_FRAME_BASE64_SIZE (websocket_server_encoded_header_length(1) + ngx_base64_encoded_length(1)) 77 | #define MIN_SERVER_FRAME_BINARY_SIZE (websocket_server_encoded_header_length(1) + 1) 78 | #define MIN_SERVER_FRAME_SIZE (ngx_max(MIN_SERVER_FRAME_BASE64_SIZE, MIN_SERVER_FRAME_BINARY_SIZE)) 79 | 80 | #define websocket_payload_consume_size(size) ( (size) <= ( websocket_server_encoded_header_length(125) + 125 ) ? ( size - MIN_WEBSOCKET_FRAME_HEADER_SIZE ) : ( size - MAX_WEBSOCKET_FRAME_HEADER_SIZE) ) 81 | 82 | static ngx_inline void 83 | websocket_server_write_frame_header(u_char *dst, u_char opcode, 84 | size_t payload_length) 85 | { 86 | dst[0] = (u_char)((opcode & 0x0F) | 0x80); 87 | if ( payload_length <= 125 ) { 88 | dst[1] = (u_char)(payload_length); 89 | } else { 90 | dst[1] = (u_char) 126; 91 | *(u_short *)&(dst[2]) = htons((u_short)(payload_length)); 92 | } 93 | } 94 | 95 | static ngx_inline ssize_t 96 | websocket_server_decode_next_frame(websocket_frame_t *frame, u_char *src, 97 | size_t size) 98 | { 99 | size_t header_length; 100 | size_t payload_length; 101 | 102 | if (size < MIN_WEBSOCKET_FRAME_HEADER_SIZE) { 103 | return NGX_AGAIN; 104 | } 105 | 106 | frame->opcode = src[0] & 0x0F; 107 | 108 | payload_length = src[1] & 0x7F; 109 | 110 | if (payload_length == 126) { 111 | payload_length = (src[2] << 8) + src[3]; 112 | } else if (payload_length == 127) { 113 | // only max frame payload 65535 support at this time 114 | return NGX_ERROR; 115 | } 116 | 117 | frame->payload_length = payload_length; 118 | 119 | header_length = websocket_server_decoded_header_length(payload_length); 120 | 121 | // no enough body 122 | if (header_length + payload_length > size) { 123 | return NGX_AGAIN; 124 | } 125 | 126 | frame->mask = src + header_length - WEBSOCKET_FRAME_MASK_SIZE; 127 | frame->payload = src + header_length; 128 | 129 | return header_length; 130 | } 131 | 132 | static ngx_inline void 133 | websocket_server_decode_unmask_payload(websocket_frame_t *frame) 134 | { 135 | size_t i; 136 | for (i = 0; i < frame->payload_length; i++) { 137 | frame->payload[i] ^= frame->mask[i % 4]; 138 | } 139 | } 140 | 141 | #endif /* _WEBSOCKET_H_INCLUDED_ */ 142 | --------------------------------------------------------------------------------