├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── lib └── resty │ └── websocket │ └── proxy.lua ├── lua-resty-websocket-proxy-0.0.1-1.rockspec ├── misc └── nginx.conf └── t ├── 01-basic_proxy.t ├── 02-timeouts.t ├── 03-on_frame_callback.t ├── 04-fragmented_frames.t ├── 05-wss.t ├── 06-error_handling.t ├── 07-invalid_usage.t ├── 08-limits.t ├── Tests.pm └── certs ├── cert.pem └── key.pem /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | tests: 15 | name: Tests 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | openresty: [1.19.9.1] 21 | openssl: [1.1.1l] 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Setup OpenResty 25 | uses: thibaultcha/setup-openresty@main 26 | with: 27 | version: ${{ matrix.openresty }} 28 | opt: --without-stream 29 | openssl-version: ${{ matrix.openssl }} 30 | - run: prove -r t/ 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | misc/servroot 2 | t/servroot* 3 | *.tar.gz 4 | *.rock 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "misc/lua-resty-websocket"] 2 | path = misc/lua-resty-websocket 3 | url = git@github.com:openresty/lua-resty-websocket.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-websocket-proxy 2 | 3 | Reverse-proxying of websocket frames with in-flight inspection/update/drop and 4 | frame aggregation support. 5 | 6 | Resources: 7 | 8 | - [RFC-6455](https://datatracker.ietf.org/doc/html/rfc6455) 9 | - [lua-resty-websocket](https://github.com/openresty/lua-resty-websocket) 10 | 11 | # Table of Contents 12 | 13 | - [Status](#status) 14 | - [Synopsis](#synopsis) 15 | - [Limitations](#limitations) 16 | - [TODO](#todo) 17 | - [License](#license) 18 | 19 | # Status 20 | 21 | This library is usable although still under active development. 22 | 23 | The Lua API may change without notice. 24 | 25 | [Back to TOC](#table-of-contents) 26 | 27 | # Synopsis 28 | 29 | ```lua 30 | http { 31 | server { 32 | listen 9000; 33 | 34 | location / { 35 | content_by_lua_block { 36 | local ws_proxy = require "resty.websocket.proxy" 37 | 38 | local proxy, err = ws_proxy.new({ 39 | aggregate_fragments = true, 40 | on_frame = function(proxy, role, typ, payload, last, code) 41 | -- proxy: [table] the proxy instance 42 | -- role: [string] "client" or "upstream" 43 | -- typ: [string] "text", "binary", "ping", "pong", "close" 44 | -- payload: [string|nil] payload if any 45 | -- last: [boolean] fin flag for fragmented frames; true if aggregate_fragments is on 46 | -- code: [number|nil] code for "close" frames 47 | 48 | if update_payload then 49 | -- change payload + code before forwarding 50 | return "new payload", 1001 51 | end 52 | 53 | -- forward as-is 54 | return payload 55 | end 56 | }) 57 | if not proxy then 58 | ngx.log(ngx.ERR, "failed to create proxy: ", err) 59 | return ngx.exit(444) 60 | end 61 | 62 | local ok, err = proxy:connect("ws://127.0.0.1:9001") 63 | if not ok then 64 | ngx.log(ngx.ERR, err) 65 | return ngx.exit(444) 66 | end 67 | 68 | -- Start a bi-directional websocket proxy between 69 | -- this client and the upstream 70 | local done, err = proxy:execute() 71 | if not done then 72 | ngx.log(ngx.ERR, "failed proxying: ", err) 73 | return ngx.exit(444) 74 | end 75 | } 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | [Back to TOC](#table-of-contents) 82 | 83 | # Limitations 84 | 85 | * Built with [lua-resty-websocket](https://github.com/openresty/lua-resty-websocket) 86 | which only supports `Sec-Websocket-Version: 13` (no extensions) and denotes 87 | its client component a 88 | [work-in-progress](https://github.com/openresty/lua-resty-websocket/blob/master/lib/resty/websocket/client.lua#L4-L5). 89 | 90 | [Back to TOC](#table-of-contents) 91 | 92 | # TODO 93 | 94 | - [ ] Limits on fragmented messages buffering (number of frames/payload size) 95 | - [ ] Performance/latency analysis 96 | - [ ] Peer review 97 | 98 | [Back to TOC](#table-of-contents) 99 | 100 | # License 101 | 102 | Copyright 2022 Kong Inc. 103 | 104 | Licensed under the Apache License, Version 2.0 (the "License"); 105 | you may not use this file except in compliance with the License. 106 | You may obtain a copy of the License at 107 | 108 | http://www.apache.org/licenses/LICENSE-2.0 109 | 110 | Unless required by applicable law or agreed to in writing, software 111 | distributed under the License is distributed on an "AS IS" BASIS, 112 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 113 | See the License for the specific language governing permissions and 114 | limitations under the License. 115 | 116 | [Back to TOC](#table-of-contents) 117 | -------------------------------------------------------------------------------- /lib/resty/websocket/proxy.lua: -------------------------------------------------------------------------------- 1 | local new_tab = require "table.new" 2 | local clear_tab = require "table.clear" 3 | local ws_client = require "resty.websocket.client" 4 | local ws_server = require "resty.websocket.server" 5 | 6 | 7 | local type = type 8 | local setmetatable = setmetatable 9 | local insert = table.insert 10 | local concat = table.concat 11 | local yield = coroutine.yield 12 | local fmt = string.format 13 | local sub = string.sub 14 | local gsub = string.gsub 15 | local find = string.find 16 | local log = ngx.log 17 | 18 | 19 | local _DEBUG_PAYLOAD_MAX_LEN = 24 20 | local _STATES = { 21 | INIT = 1, 22 | ESTABLISHED = 2, 23 | CLOSING = 3, 24 | } 25 | 26 | local _TYP2OPCODE = { 27 | ["continuation"] = 0x0, 28 | ["text"] = 0x1, 29 | ["binary"] = 0x2, 30 | ["close"] = 0x8, 31 | ["ping"] = 0x9, 32 | ["pong"] = 0xa, 33 | } 34 | 35 | 36 | local _M = { 37 | _VERSION = "0.0.1", 38 | } 39 | 40 | local _mt = { __index = _M } 41 | 42 | 43 | function _M.new(opts) 44 | if opts == nil then 45 | opts = new_tab(0, 0) 46 | end 47 | 48 | if type(opts) ~= "table" then 49 | error("opts must be a table", 2) 50 | end 51 | 52 | if opts.on_frame ~= nil and type(opts.on_frame) ~= "function" then 53 | error("opts.on_frame must be a function", 2) 54 | end 55 | 56 | if opts.recv_timeout ~= nil and type(opts.recv_timeout) ~= "number" then 57 | error("opts.recv_timeout must be a number", 2) 58 | end 59 | 60 | if opts.client_max_frame_size ~= nil 61 | and (type(opts.client_max_frame_size) ~= "number" 62 | or opts.client_max_frame_size < 1) 63 | then 64 | error("opts.client_max_frame_size must be a number >= 1", 2) 65 | end 66 | 67 | if opts.client_max_fragments ~= nil 68 | and (type(opts.client_max_fragments) ~= "number" 69 | or opts.client_max_fragments < 1) 70 | then 71 | error("opts.client_max_fragments must be a number >= 1", 2) 72 | end 73 | 74 | if opts.upstream_max_frame_size ~= nil 75 | and (type(opts.upstream_max_frame_size) ~= "number" 76 | or opts.upstream_max_frame_size < 1) 77 | then 78 | error("opts.upstream_max_frame_size must be a number >= 1", 2) 79 | end 80 | 81 | if opts.upstream_max_fragments ~= nil 82 | and (type(opts.upstream_max_fragments) ~= "number" 83 | or opts.upstream_max_fragments < 1) 84 | then 85 | error("opts.upstream_max_fragments must be a number >= 1", 2) 86 | end 87 | 88 | 89 | -- TODO: provide a means of passing options through to the 90 | -- resty.websocket.client constructor (like `max_payload_len`) 91 | local client, err = ws_client:new() 92 | if not client then 93 | return nil, "failed to create client: " .. err 94 | end 95 | 96 | local self = { 97 | client = client, 98 | server = nil, 99 | upstream_uri = nil, 100 | on_frame = opts.on_frame, 101 | recv_timeout = opts.recv_timeout, 102 | client_max_frame_size = opts.client_max_frame_size, 103 | client_max_fragments = opts.client_max_fragments, 104 | upstream_max_frame_size = opts.upstream_max_frame_size, 105 | upstream_max_fragments = opts.upstream_max_fragments, 106 | aggregate_fragments = opts.aggregate_fragments, 107 | debug = opts.debug, 108 | client_state = _STATES.INIT, 109 | upstream_state = _STATES.INIT, 110 | co_client = nil, 111 | co_server = nil, 112 | } 113 | 114 | return setmetatable(self, _mt) 115 | end 116 | 117 | 118 | function _M:dd(...) 119 | if self.debug then 120 | return log(ngx.DEBUG, ...) 121 | end 122 | end 123 | 124 | 125 | local function send_close_frame(self, ws, role, code, data) 126 | self:dd(role, fmt(" closing:\ncode: %s\nreason: %q", code, data)) 127 | 128 | local ok, err = ws:send_close(code, data) 129 | if not ok then 130 | log(ngx.ERR, "failed sending close frame to ", role, ": ", err) 131 | end 132 | end 133 | 134 | 135 | local function close(self, role, code, data, peer_code, peer_data) 136 | local self_ws, peer_ws 137 | local self_state = role .. "_state" 138 | local peer 139 | 140 | if role == "client" then 141 | self_ws = self.server 142 | peer_ws = self.client 143 | peer = "upstream" 144 | else 145 | -- role == "upstream" 146 | self_ws = self.client 147 | peer_ws = self.server 148 | peer = "client" 149 | end 150 | 151 | send_close_frame(self, self_ws, role, code, data) 152 | self[self_state] = _STATES.CLOSING 153 | send_close_frame(self, peer_ws, peer, peer_code, peer_data) 154 | end 155 | 156 | 157 | local function forwarder(self, ctx) 158 | local role = ctx.role 159 | local buf = ctx.buf 160 | local self_ws, peer_ws 161 | local self_state, peer_state 162 | local frame_typ 163 | local frame_size, frame_count = 0, 0 164 | local on_frame = self.on_frame 165 | local max_frame_size = ctx.max_frame_size 166 | local max_fragments = ctx.max_fragments 167 | 168 | self_state = role .. "_state" 169 | 170 | --assert(self[self_state] == _STATES.ESTABLISHED) 171 | 172 | if role == "client" then 173 | self_ws = self.server 174 | peer_ws = self.client 175 | peer_state = "upstream_state" 176 | 177 | else 178 | -- role == "upstream" 179 | self_ws = self.client 180 | peer_ws = self.server 181 | peer_state = "client_state" 182 | end 183 | 184 | while true do 185 | if self[peer_state] == _STATES.CLOSING then 186 | return 187 | end 188 | 189 | if self.recv_timeout then 190 | self_ws:set_timeout(self.recv_timeout) 191 | end 192 | 193 | self:dd(role, " receiving frame...") 194 | 195 | local data, typ, err = self_ws:recv_frame() 196 | if not data then 197 | if find(err, "timeout", 1, true) then 198 | log(ngx.INFO, fmt("timeout receiving frame from %s, reopening", 199 | role)) 200 | -- continue 201 | 202 | elseif find(err, "closed", 1, true) then 203 | self[self_state] = _STATES.CLOSING 204 | return role 205 | 206 | else 207 | log(ngx.ERR, fmt("failed receiving frame from %s: %s", 208 | role, err)) 209 | self[self_state] = _STATES.CLOSING 210 | return role, err 211 | end 212 | end 213 | 214 | -- special flags 215 | 216 | local code 217 | local opcode = _TYP2OPCODE[typ] 218 | local fin = true 219 | if err == "again" then 220 | fin = false 221 | err = nil 222 | end 223 | 224 | if typ then 225 | if not opcode then 226 | log(ngx.EMERG, "NYI - unknown frame type: ", typ, 227 | " (dropping connection)") 228 | return 229 | end 230 | 231 | if typ == "close" then 232 | code = err 233 | end 234 | 235 | -- debug 236 | 237 | if self.debug and (not err or typ == "close") then 238 | local extra = "" 239 | local arrow 240 | 241 | if typ == "close" then 242 | arrow = role == "client" and "--x" or "x--" 243 | 244 | else 245 | arrow = role == "client" and "-->" or "<--" 246 | end 247 | 248 | local payload = data and gsub(data, "\n", "\\n") or "" 249 | if #payload > _DEBUG_PAYLOAD_MAX_LEN then 250 | payload = sub(payload, 1, _DEBUG_PAYLOAD_MAX_LEN) .. "[...]" 251 | end 252 | 253 | if code then 254 | extra = fmt("\n code: %d", code) 255 | end 256 | 257 | if frame_typ then 258 | extra = fmt("\n initial type: \"%s\"", frame_typ) 259 | end 260 | 261 | self:dd(fmt("\n[frame] downstream %s resty.proxy %s upstream\n" .. 262 | " aggregating: %s\n" .. 263 | " type: \"%s\"%s\n" .. 264 | " payload: %s (len: %d)\n" .. 265 | " fin: %s", 266 | arrow, arrow, 267 | self.aggregate_fragments, 268 | typ, extra, 269 | fmt("%q", payload), data and #data or 0, 270 | fin)) 271 | end 272 | 273 | local bytes 274 | local forward = true 275 | local data_frame = typ == "text" 276 | or typ == "binary" 277 | or typ == "continuation" 278 | 279 | -- limits 280 | 281 | if data_frame then 282 | frame_size = frame_size + #data 283 | 284 | if max_frame_size and frame_size > max_frame_size then 285 | log(ngx.INFO, fmt("%s frame size (%s) exceeds limit, closing", 286 | role, frame_size)) 287 | close(self, role, 1009, "Payload Too Large", 1001, "") 288 | 289 | return role 290 | end 291 | 292 | frame_count = frame_count + 1 293 | 294 | if max_fragments and frame_count > max_fragments then 295 | log(ngx.INFO, fmt("%s frame count (%s) exceeds limit, closing", 296 | role, frame_count)) 297 | close(self, role, 1009, "Payload Too Large", 1001, "") 298 | 299 | return role 300 | end 301 | end 302 | 303 | 304 | -- fragmentation 305 | 306 | if self.aggregate_fragments and data_frame then 307 | if not fin then 308 | self:dd(role, " received fragmented frame, buffering") 309 | insert(buf, data) 310 | forward = false 311 | 312 | -- stash data frame type of initial fragment 313 | frame_typ = frame_typ or typ 314 | 315 | -- continue 316 | 317 | elseif #buf > 0 then 318 | self:dd(role, " received last fragmented frame, forwarding") 319 | insert(buf, data) 320 | data = concat(buf, "") 321 | clear_tab(buf) 322 | 323 | -- restore initial fragment type and opcode 324 | typ = frame_typ 325 | frame_typ = nil 326 | opcode = _TYP2OPCODE[typ] 327 | end 328 | end 329 | 330 | -- forward 331 | 332 | if forward then 333 | 334 | -- callback 335 | 336 | if on_frame then 337 | local updated, updated_code = on_frame(self, role, typ, 338 | data, fin, code) 339 | if updated ~= nil then 340 | if type(updated) ~= "string" then 341 | error("opts.on_frame return value must be " .. 342 | "nil or a string") 343 | end 344 | end 345 | 346 | data = updated 347 | 348 | if typ == "close" and updated_code ~= nil then 349 | if type(updated_code) ~= "number" then 350 | error("opts.on_frame status code return value " .. 351 | "must be nil or a number") 352 | end 353 | 354 | code = updated_code 355 | end 356 | end 357 | 358 | if on_frame and data == nil then 359 | self:dd(role, " dropping ", typ, " frame after on_frame handler requested it") 360 | 361 | -- continue: while true 362 | 363 | else 364 | if typ == "close" then 365 | log(ngx.INFO, "forwarding close with code: ", code, ", payload: ", 366 | data) 367 | 368 | bytes, err = peer_ws:send_close(code, data) 369 | 370 | else 371 | bytes, err = peer_ws:send_frame(fin, opcode, data) 372 | end 373 | 374 | if not bytes then 375 | log(ngx.ERR, fmt("failed forwarding a frame from %s: %s", 376 | role, err)) 377 | -- continue 378 | end 379 | end 380 | 381 | 382 | if data_frame then 383 | frame_size = 0 384 | frame_count = 0 385 | end 386 | end 387 | 388 | -- continue: while true 389 | end 390 | 391 | self:dd(role, " yielding") 392 | 393 | yield(self) 394 | end 395 | end 396 | 397 | 398 | function _M:connect_upstream(uri, opts) 399 | if self.upstream_state == _STATES.ESTABLISHED then 400 | log(ngx.WARN, fmt("connection with upstream at %q already established", 401 | self.upstream_uri)) 402 | return true 403 | end 404 | 405 | self:dd("connecting to \"", uri, "\" upstream") 406 | 407 | local ok, err, res = self.client:connect(uri, opts) 408 | if not ok then 409 | return nil, err 410 | end 411 | 412 | self:dd("connected to \"", uri, "\" upstream") 413 | 414 | self.upstream_uri = uri 415 | self.upstream_state = _STATES.ESTABLISHED 416 | 417 | return true, nil, res 418 | end 419 | 420 | 421 | function _M:connect_client() 422 | if self.client_state == _STATES.ESTABLISHED then 423 | log(ngx.WARN, "client handshake already completed") 424 | return true 425 | end 426 | 427 | self:dd("completing client handshake") 428 | 429 | local server, err = ws_server:new() 430 | if not server then 431 | return nil, err 432 | end 433 | 434 | self:dd("completed client handshake") 435 | 436 | self.server = server 437 | self.client_state = _STATES.ESTABLISHED 438 | 439 | return true 440 | end 441 | 442 | 443 | function _M:connect(uri, upstream_opts) 444 | local ok, err = self:connect_upstream(uri, upstream_opts) 445 | if not ok then 446 | return nil, "failed connecting to upstream: " .. err 447 | end 448 | 449 | ok, err = self:connect_client() 450 | if not ok then 451 | return nil, "failed client handshake: " .. err 452 | end 453 | 454 | return true 455 | end 456 | 457 | 458 | function _M:execute() 459 | if self.client_state ~= _STATES.ESTABLISHED then 460 | return nil, "client handshake not complete" 461 | end 462 | 463 | if self.upstream_state ~= _STATES.ESTABLISHED then 464 | return nil, "upstream connection not established" 465 | end 466 | 467 | self.co_client = ngx.thread.spawn(forwarder, self, { 468 | role = "client", 469 | buf = new_tab(0, 0), 470 | max_frame_size = self.client_max_frame_size, 471 | max_fragments = self.client_max_fragments, 472 | }) 473 | 474 | self.co_server = ngx.thread.spawn(forwarder, self, { 475 | role = "upstream", 476 | buf = new_tab(0, 0), 477 | max_frame_size = self.upstream_max_frame_size, 478 | max_fragments = self.upstream_max_fragments, 479 | }) 480 | 481 | local ok, res, err = ngx.thread.wait(self.co_client, self.co_server) 482 | if not ok then 483 | log(ngx.ERR, "failed to wait for websocket proxy threads: ", err) 484 | 485 | elseif res == "client" then 486 | --assert(self.client_state == _STATES.CLOSING) 487 | 488 | self:dd(res, " thread terminated, killing server thread") 489 | 490 | ngx.thread.kill(self.co_server) 491 | 492 | self:dd("closing \"", self.upstream_uri, "\" upstream websocket") 493 | 494 | self.client:close() 495 | 496 | elseif res == "upstream" then 497 | --assert(self.upstream_state == _STATES.CLOSING) 498 | 499 | self:dd(res, " thread terminated, killing client thread") 500 | 501 | ngx.thread.kill(self.co_client) 502 | end 503 | 504 | self.co_client = nil 505 | self.co_server = nil 506 | self.client_state = _STATES.INIT 507 | self.upstream_state = _STATES.INIT 508 | 509 | if err then 510 | return nil, err 511 | end 512 | 513 | return true 514 | end 515 | 516 | 517 | return _M 518 | -------------------------------------------------------------------------------- /lua-resty-websocket-proxy-0.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-websocket-proxy" 2 | version = "0.0.1-1" 3 | source = { 4 | url = "git://github.com/Kong/lua-resty-websocket-proxy", 5 | tag = "0.0.1", 6 | } 7 | description = { 8 | summary = "Reverse-proxying of websocket frames", 9 | detailed = [[ 10 | Reverse-proxying of websocket frames with in-flight inspection/update/drop 11 | and frame aggregation support. 12 | ]], 13 | license = "Apache 2.0", 14 | homepage = "https://github.com/Kong/lua-resty-websocket-proxy", 15 | } 16 | dependencies = {} 17 | build = { 18 | type = "builtin", 19 | modules = { 20 | ["resty.websocket.proxy"] = "lib/resty/websocket/proxy.lua", 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /misc/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | master_process off; 3 | worker_processes 1; 4 | error_log /dev/stderr debug; 5 | 6 | events {} 7 | 8 | http { 9 | access_log off; 10 | lua_package_path '$prefix/../../lib/?.lua;$prefix/../lua-resty-websocket/lib/?.lua;;'; 11 | lua_socket_log_errors off; 12 | 13 | upstream ws_server { 14 | server 127.0.0.1:9001; 15 | } 16 | 17 | server { 18 | listen 9000; 19 | 20 | location ~ ^/(?.*) { 21 | rewrite_by_lua_block { 22 | local ws_proxy = require "resty.websocket.proxy" 23 | local proxy, err 24 | 25 | proxy = ngx.ctx.proxy 26 | if proxy == nil then 27 | proxy, err = ws_proxy.new({ 28 | upstream = "ws://127.0.0.1:9001", 29 | debug = true, 30 | }) 31 | if not proxy then 32 | ngx.log(ngx.ERR, "failed to create proxy: ", err) 33 | return ngx.exit(444) 34 | end 35 | 36 | ngx.ctx.proxy = proxy 37 | end 38 | 39 | proxy:execute() 40 | } 41 | 42 | #proxy_pass http://ws_server/$path; 43 | #proxy_http_version 1.1; 44 | #proxy_set_header Connection 'upgrade'; 45 | #proxy_set_header Upgrade 'websocket'; 46 | } 47 | } 48 | 49 | server { 50 | listen 9001; 51 | 52 | location / { 53 | content_by_lua_block { 54 | local ws_server = require "resty.websocket.server" 55 | local server, err 56 | 57 | server = ngx.ctx.server 58 | if server == nil then 59 | server, err = ws_server:new() 60 | if not server then 61 | ngx.log(ngx.ERR, "failed to create server: ", err) 62 | return ngx.exit(444) 63 | end 64 | 65 | ngx.ctx.server = server 66 | end 67 | 68 | while true do 69 | local data, typ, err = server:recv_frame() 70 | if not data then 71 | if not string.find(err, "closed", 1, true) 72 | and not string.find(err, "timeout", 1, true) 73 | then 74 | ngx.log(ngx.ERR, "failed to receive a frame: ", err) 75 | else 76 | ngx.log(ngx.DEBUG, err) 77 | end 78 | 79 | return ngx.exit(444) 80 | end 81 | 82 | ngx.log(ngx.INFO, "received a frame of type \"", typ, 83 | "\" and payload \"", 84 | data and string.gsub(data, "\n", "\\n") 85 | or "", "\"") 86 | 87 | if typ == "close" then 88 | return 89 | 90 | elseif typ == "ping" then 91 | local bytes, err = server:send_pong(data) 92 | if not bytes then 93 | ngx.log(ngx.ERR, "failed to send frame: ", err) 94 | return 95 | end 96 | 97 | elseif typ == "pong" then 98 | -- NOP 99 | 100 | elseif typ == "text" then 101 | local bytes, err = server:send_text("hello from server") 102 | if not bytes then 103 | ngx.log(ngx.ERR, "failed to send frame: ", err) 104 | return ngx.exit(444) 105 | end 106 | 107 | else 108 | ngx.log(ngx.EMERG, "NYI - frame type \"", typ, "\"") 109 | end 110 | end 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /t/01-basic_proxy.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: forward a text frame back and forth 13 | --- http_config eval: $t::Tests::HttpConfig 14 | --- config 15 | location /proxy { 16 | content_by_lua_block { 17 | local proxy = require "resty.websocket.proxy" 18 | 19 | local wp, err = proxy.new() 20 | if not wp then 21 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 22 | return ngx.exit(444) 23 | end 24 | 25 | local ok, err = wp:connect(proxy._tests.echo) 26 | if not ok then 27 | ngx.log(ngx.ERR, err) 28 | return ngx.exit(444) 29 | end 30 | 31 | local done, err = wp:execute() 32 | if not done then 33 | ngx.log(ngx.ERR, "failed proxying: ", err) 34 | return ngx.exit(444) 35 | end 36 | } 37 | } 38 | 39 | location /t { 40 | content_by_lua_block { 41 | local client = require "resty.websocket.client" 42 | local wb = assert(client:new()) 43 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 44 | 45 | assert(wb:connect(uri)) 46 | assert(wb:send_text("hello world!")) 47 | local data = assert(wb:recv_frame()) 48 | ngx.say(data) 49 | } 50 | } 51 | --- response_body 52 | hello world! 53 | --- grep_error_log eval: qr/\[lua\].*/ 54 | --- grep_error_log_out eval 55 | qr/frame type: text, payload: "hello world!"/ 56 | --- no_error_log 57 | [error] 58 | 59 | 60 | 61 | === TEST 2: forward a ping/pong exchange 62 | --- http_config eval: $t::Tests::HttpConfig 63 | --- config 64 | location /proxy { 65 | content_by_lua_block { 66 | local proxy = require "resty.websocket.proxy" 67 | 68 | local wp, err = proxy.new() 69 | if not wp then 70 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 71 | return ngx.exit(444) 72 | end 73 | 74 | local ok, err = wp:connect(proxy._tests.pong) 75 | if not ok then 76 | ngx.log(ngx.ERR, err) 77 | return ngx.exit(444) 78 | end 79 | 80 | local done, err = wp:execute() 81 | if not done then 82 | ngx.log(ngx.ERR, "failed proxying: ", err) 83 | return ngx.exit(444) 84 | end 85 | } 86 | } 87 | 88 | location /t { 89 | content_by_lua_block { 90 | local client = require "resty.websocket.client" 91 | local wb = assert(client:new()) 92 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 93 | 94 | assert(wb:connect(uri)) 95 | assert(wb:send_ping("heartbeat client")) 96 | local data, opcode = assert(wb:recv_frame()) 97 | ngx.say(opcode, ": ", data) 98 | } 99 | } 100 | --- response_body 101 | pong: heartbeat server 102 | --- grep_error_log eval: qr/\[lua\].*/ 103 | --- grep_error_log_out eval 104 | qr/frame type: ping, payload: "heartbeat client"/ 105 | --- no_error_log 106 | [error] 107 | 108 | 109 | 110 | === TEST 3: forward a binary frame back and forth 111 | --- http_config eval: $t::Tests::HttpConfig 112 | --- config 113 | location /upstream { 114 | content_by_lua_block { 115 | local server = require "resty.websocket.server" 116 | 117 | local wb, err = server:new() 118 | if not wb then 119 | ngx.log(ngx.ERR, "failed creating server: ", err) 120 | return ngx.exit(444) 121 | end 122 | 123 | local data, typ, err = wb:recv_frame() 124 | if not data then 125 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 126 | return ngx.exit(444) 127 | end 128 | 129 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 130 | 131 | local bytes, err = wb:send_binary(data) 132 | if not bytes then 133 | ngx.log(ngx.ERR, "failed sending frame: ", err) 134 | return ngx.exit(444) 135 | end 136 | } 137 | } 138 | 139 | location /proxy { 140 | content_by_lua_block { 141 | local proxy = require "resty.websocket.proxy" 142 | 143 | local wp, err = proxy.new() 144 | if not wp then 145 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 146 | return ngx.exit(444) 147 | end 148 | 149 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 150 | local ok, err = wp:connect(uri) 151 | if not ok then 152 | ngx.log(ngx.ERR, err) 153 | return ngx.exit(444) 154 | end 155 | 156 | local done, err = wp:execute() 157 | if not done then 158 | ngx.log(ngx.ERR, "failed proxying: ", err) 159 | return ngx.exit(444) 160 | end 161 | } 162 | } 163 | 164 | location /t { 165 | content_by_lua_block { 166 | local client = require "resty.websocket.client" 167 | local wb = assert(client:new()) 168 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 169 | 170 | assert(wb:connect(uri)) 171 | assert(wb:send_binary("你好, WebSocket!")) 172 | local data, opcode = assert(wb:recv_frame()) 173 | ngx.say(opcode, ": ", data) 174 | } 175 | } 176 | --- response_body 177 | binary: 你好, WebSocket! 178 | --- grep_error_log eval: qr/\[lua\].*/ 179 | --- grep_error_log_out eval 180 | qr/frame type: binary, payload: "你好, WebSocket!"/ 181 | --- no_error_log 182 | [error] 183 | 184 | 185 | 186 | === TEST 4: forward close frame exchange from the client 187 | --- http_config eval: $t::Tests::HttpConfig 188 | --- config 189 | location /upstream { 190 | content_by_lua_block { 191 | local server = require "resty.websocket.server" 192 | 193 | local wb, err = server:new() 194 | if not wb then 195 | ngx.log(ngx.ERR, "failed creating server: ", err) 196 | return ngx.exit(444) 197 | end 198 | 199 | local data, typ, err = wb:recv_frame() 200 | if not data then 201 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 202 | return ngx.exit(444) 203 | end 204 | 205 | ngx.log(ngx.INFO, "frame type: ", typ, 206 | ", code: ", err, 207 | ", payload: \"", data, "\"") 208 | 209 | local bytes, err = wb:send_close() 210 | if not bytes then 211 | ngx.log(ngx.ERR, "failed sending close frame: ", err) 212 | return ngx.exit(444) 213 | end 214 | } 215 | } 216 | 217 | location /proxy { 218 | content_by_lua_block { 219 | local proxy = require "resty.websocket.proxy" 220 | 221 | local wp, err = proxy.new() 222 | if not wp then 223 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 224 | return ngx.exit(444) 225 | end 226 | 227 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 228 | local ok, err = wp:connect(uri) 229 | if not ok then 230 | ngx.log(ngx.ERR, err) 231 | return ngx.exit(444) 232 | end 233 | 234 | local done, err = wp:execute() 235 | if not done then 236 | ngx.log(ngx.ERR, "failed proxying: ", err) 237 | return ngx.exit(444) 238 | end 239 | } 240 | } 241 | 242 | location /t { 243 | content_by_lua_block { 244 | local client = require "resty.websocket.client" 245 | local wb = assert(client:new()) 246 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 247 | 248 | assert(wb:connect(uri)) 249 | assert(wb:send_close(1000, "goodbye")) 250 | local data, opcode = assert(wb:recv_frame()) 251 | ngx.say(opcode, ": ", data) 252 | wb:close() 253 | } 254 | } 255 | --- ignore_response_body 256 | --- grep_error_log eval: qr/\[lua\].*/ 257 | --- grep_error_log_out eval 258 | qr/frame type: close, code: 1000, payload: "goodbye"/ 259 | --- no_error_log 260 | [crit] 261 | [error] 262 | 263 | 264 | 265 | === TEST 5: forward close frame exchange from upstream 266 | --- http_config eval: $t::Tests::HttpConfig 267 | --- config 268 | location /upstream { 269 | content_by_lua_block { 270 | local server = require "resty.websocket.server" 271 | 272 | local wb, err = server:new() 273 | if not wb then 274 | ngx.log(ngx.ERR, "failed creating server: ", err) 275 | return ngx.exit(444) 276 | end 277 | 278 | local bytes, err = wb:send_close() 279 | if not bytes then 280 | ngx.log(ngx.ERR, "failed sending close frame: ", err) 281 | return ngx.exit(444) 282 | end 283 | } 284 | } 285 | 286 | location /proxy { 287 | content_by_lua_block { 288 | local proxy = require "resty.websocket.proxy" 289 | 290 | local wp, err = proxy.new() 291 | if not wp then 292 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 293 | return ngx.exit(444) 294 | end 295 | 296 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 297 | local ok, err = wp:connect(uri) 298 | if not ok then 299 | ngx.log(ngx.ERR, err) 300 | return ngx.exit(444) 301 | end 302 | 303 | local done, err = wp:execute() 304 | if not done then 305 | ngx.log(ngx.ERR, "failed proxying: ", err) 306 | return ngx.exit(444) 307 | end 308 | } 309 | } 310 | 311 | location /t { 312 | content_by_lua_block { 313 | local client = require "resty.websocket.client" 314 | local wb = assert(client:new()) 315 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 316 | 317 | assert(wb:connect(uri)) 318 | local data, opcode = assert(wb:recv_frame()) 319 | ngx.say(opcode) 320 | } 321 | } 322 | --- response_body 323 | close 324 | --- grep_error_log eval: qr/\[lua\].*/ 325 | --- grep_error_log_out eval 326 | qr/forwarding close with code: nil/ 327 | --- no_error_log 328 | [error] 329 | 330 | 331 | 332 | === TEST 6: handshake with client before upstream 333 | --- http_config eval: $t::Tests::HttpConfig 334 | --- config 335 | location /upstream { 336 | content_by_lua_block { 337 | local server = require "resty.websocket.server" 338 | 339 | local wb, err = server:new() 340 | if not wb then 341 | ngx.log(ngx.ERR, "failed creating server: ", err) 342 | return ngx.exit(444) 343 | end 344 | 345 | local data, typ, err = wb:recv_frame() 346 | if not data then 347 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 348 | return ngx.exit(444) 349 | end 350 | 351 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 352 | 353 | local bytes, err = wb:send_text(data) 354 | if not bytes then 355 | ngx.log(ngx.ERR, "failed sending frame: ", err) 356 | return ngx.exit(444) 357 | end 358 | } 359 | } 360 | 361 | location /proxy { 362 | content_by_lua_block { 363 | local proxy = require "resty.websocket.proxy" 364 | 365 | local wp, err = proxy.new() 366 | if not wp then 367 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 368 | return ngx.exit(444) 369 | end 370 | 371 | local ok, err = wp:connect_client() 372 | if not ok then 373 | ngx.log(ngx.ERR, "failed client handshake: ", err) 374 | return ngx.exit(444) 375 | end 376 | 377 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 378 | local ok, err = wp:connect_upstream(uri) 379 | if not ok then 380 | ngx.log(ngx.ERR, "failed connecting to upstream: ", err) 381 | return ngx.exit(444) 382 | end 383 | 384 | local done, err = wp:execute() 385 | if not done then 386 | ngx.log(ngx.ERR, "failed proxying: ", err) 387 | return ngx.exit(444) 388 | end 389 | } 390 | } 391 | 392 | location /t { 393 | content_by_lua_block { 394 | local client = require "resty.websocket.client" 395 | local wb = assert(client:new()) 396 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 397 | 398 | assert(wb:connect(uri)) 399 | assert(wb:send_text("hello world!")) 400 | local data = assert(wb:recv_frame()) 401 | ngx.say(data) 402 | } 403 | } 404 | --- response_body 405 | hello world! 406 | --- grep_error_log eval: qr/\[lua\].*/ 407 | --- grep_error_log_out eval 408 | qr/frame type: text, payload: "hello world!"/ 409 | --- no_error_log 410 | [error] 411 | -------------------------------------------------------------------------------- /t/02-timeouts.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: on client recv_frame timeout 13 | --- http_config eval: $t::Tests::HttpConfig 14 | --- config 15 | lua_socket_log_errors off; 16 | 17 | location /proxy { 18 | content_by_lua_block { 19 | local proxy = require "resty.websocket.proxy" 20 | 21 | local wb, err = proxy.new({ 22 | recv_timeout = 80, 23 | }) 24 | if not wb then 25 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 26 | return ngx.exit(444) 27 | end 28 | 29 | local ok, err = wb:connect(proxy._tests.echo) 30 | if not ok then 31 | ngx.log(ngx.ERR, err) 32 | return ngx.exit(444) 33 | end 34 | 35 | local done, err = wb:execute() 36 | if not done then 37 | ngx.log(ngx.ERR, "failed proxying: ", err) 38 | return ngx.exit(444) 39 | end 40 | } 41 | } 42 | 43 | location /t { 44 | content_by_lua_block { 45 | local client = require "resty.websocket.client" 46 | local wb = assert(client:new()) 47 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 48 | 49 | assert(wb:connect(uri)) 50 | ngx.sleep(0.1) 51 | assert(wb:send_text("hello world!")) 52 | local data = assert(wb:recv_frame()) 53 | ngx.say(data) 54 | } 55 | } 56 | --- response_body 57 | hello world! 58 | --- grep_error_log eval: qr/\[lua\].*/ 59 | --- grep_error_log_out eval 60 | qr/.*?timeout receiving frame from client, reopening.* 61 | .*?timeout receiving frame from upstream, reopening.* 62 | .*?frame type: text, payload: "hello world!".*/ 63 | --- no_error_log 64 | [crit] 65 | 66 | 67 | 68 | === TEST 2: on upstream recv_frame timeout 69 | --- http_config eval: $t::Tests::HttpConfig 70 | --- config 71 | lua_socket_log_errors off; 72 | 73 | location /upstream { 74 | content_by_lua_block { 75 | local server = require "resty.websocket.server" 76 | 77 | local wb, err = server:new() 78 | if not wb then 79 | ngx.log(ngx.ERR, "failed creating server: ", err) 80 | return ngx.exit(444) 81 | end 82 | 83 | local data, typ, err = wb:recv_frame() 84 | if not data then 85 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 86 | return ngx.exit(444) 87 | end 88 | 89 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 90 | 91 | ngx.sleep(0.1) 92 | 93 | local bytes, err = wb:send_text(data) 94 | if not bytes then 95 | ngx.log(ngx.ERR, "failed sending frame: ", err) 96 | return ngx.exit(444) 97 | end 98 | } 99 | } 100 | 101 | location /proxy { 102 | content_by_lua_block { 103 | local proxy = require "resty.websocket.proxy" 104 | 105 | local wb, err = proxy.new({ 106 | recv_timeout = 80, 107 | }) 108 | if not wb then 109 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 110 | return ngx.exit(444) 111 | end 112 | 113 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 114 | local ok, err = wb:connect(uri) 115 | if not ok then 116 | ngx.log(ngx.ERR, err) 117 | return ngx.exit(444) 118 | end 119 | 120 | local done, err = wb:execute() 121 | if not done then 122 | ngx.log(ngx.ERR, "failed proxying: ", err) 123 | return ngx.exit(444) 124 | end 125 | } 126 | } 127 | 128 | location /t { 129 | content_by_lua_block { 130 | local client = require "resty.websocket.client" 131 | local wb = assert(client:new()) 132 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 133 | 134 | assert(wb:connect(uri)) 135 | assert(wb:send_text("hello world!")) 136 | local data = assert(wb:recv_frame()) 137 | ngx.say(data) 138 | } 139 | } 140 | --- response_body 141 | hello world! 142 | --- grep_error_log eval: qr/\[lua\].*/ 143 | --- grep_error_log_out eval 144 | qr/.*?frame type: text, payload: "hello world!".* 145 | .*?timeout receiving frame from upstream, reopening.*/ 146 | --- no_error_log 147 | [crit] 148 | -------------------------------------------------------------------------------- /t/03-on_frame_callback.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: invokes opts.on_frame function on each client/upstream frame 13 | --- http_config eval: $t::Tests::HttpConfig 14 | --- config 15 | location /proxy { 16 | content_by_lua_block { 17 | local proxy = require "resty.websocket.proxy" 18 | 19 | local function on_frame(_, role, typ, data, fin, code) 20 | ngx.log(ngx.INFO, "from: ", role, ", type: ", typ, 21 | ", payload: ", data, 22 | ", fin: ", fin, ", code: ", code, 23 | ", context: ", ngx.get_phase()) 24 | -- test: only return data (code == nil) 25 | return data 26 | end 27 | 28 | local wp, err = proxy.new({ on_frame = on_frame }) 29 | if not wp then 30 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 31 | return ngx.exit(444) 32 | end 33 | 34 | local ok, err = wp:connect(proxy._tests.echo) 35 | if not ok then 36 | ngx.log(ngx.ERR, err) 37 | return ngx.exit(444) 38 | end 39 | 40 | local done, err = wp:execute() 41 | if not done then 42 | ngx.log(ngx.ERR, "failed proxying: ", err) 43 | return ngx.exit(444) 44 | end 45 | } 46 | } 47 | 48 | location /t { 49 | content_by_lua_block { 50 | local client = require "resty.websocket.client" 51 | local wb = assert(client:new()) 52 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 53 | 54 | assert(wb:connect(uri)) 55 | assert(wb:send_text("hello world!")) 56 | local data = assert(wb:recv_frame()) 57 | ngx.say(data) 58 | } 59 | } 60 | --- response_body 61 | hello world! 62 | --- grep_error_log eval: qr/\[lua\].*?from:.*/ 63 | --- grep_error_log_out eval 64 | qr/.*?from: client, type: text, payload: hello world!, fin: true, code: nil, context: content.* 65 | .*?from: upstream, type: text, payload: hello world!, fin: true, code: nil, context: content.*/ 66 | --- no_error_log 67 | [error] 68 | 69 | 70 | 71 | === TEST 2: opts.on_frame can update an upstream text frame payload 72 | --- http_config eval: $t::Tests::HttpConfig 73 | --- config 74 | location /proxy { 75 | content_by_lua_block { 76 | local proxy = require "resty.websocket.proxy" 77 | 78 | local function on_frame(_, role, typ, data, fin, code) 79 | ngx.log(ngx.INFO, "from: ", role, ", type: ", typ, 80 | ", payload: ", data, ", fin: ", fin) 81 | 82 | return "updated " .. role .. " frame", code 83 | end 84 | 85 | local wp, err = proxy.new({ on_frame = on_frame }) 86 | if not wp then 87 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 88 | return ngx.exit(444) 89 | end 90 | 91 | local ok, err = wp:connect(proxy._tests.echo) 92 | if not ok then 93 | ngx.log(ngx.ERR, err) 94 | return ngx.exit(444) 95 | end 96 | 97 | local done, err = wp:execute() 98 | if not done then 99 | ngx.log(ngx.ERR, "failed proxying: ", err) 100 | return ngx.exit(444) 101 | end 102 | } 103 | } 104 | 105 | location /t { 106 | content_by_lua_block { 107 | local client = require "resty.websocket.client" 108 | local wb = assert(client:new()) 109 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 110 | 111 | assert(wb:connect(uri)) 112 | assert(wb:send_text("hello world!")) 113 | local data = assert(wb:recv_frame()) 114 | ngx.say(data) 115 | } 116 | } 117 | --- response_body 118 | updated upstream frame 119 | --- grep_error_log eval: qr/\[lua\].*from:.*/ 120 | --- grep_error_log_out eval 121 | qr/.*?from: client, type: text, payload: hello world!, fin: true.* 122 | .*?from: upstream, type: text, payload: updated client frame, fin: true.*/ 123 | --- no_error_log 124 | [error] 125 | 126 | 127 | 128 | === TEST 3: opts.on_frame can update an upstream binary frame payload 129 | --- http_config eval: $t::Tests::HttpConfig 130 | --- config 131 | location /upstream { 132 | content_by_lua_block { 133 | local server = require "resty.websocket.server" 134 | 135 | local wb, err = server:new() 136 | if not wb then 137 | ngx.log(ngx.ERR, "failed creating server: ", err) 138 | return ngx.exit(444) 139 | end 140 | 141 | local data, typ, err = wb:recv_frame() 142 | if not data then 143 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 144 | return ngx.exit(444) 145 | end 146 | 147 | local bytes, err = wb:send_binary(data) 148 | if not bytes then 149 | ngx.log(ngx.ERR, "failed sending frame: ", err) 150 | return ngx.exit(444) 151 | end 152 | } 153 | } 154 | 155 | location /proxy { 156 | content_by_lua_block { 157 | local proxy = require "resty.websocket.proxy" 158 | 159 | local function on_frame(_, role, typ, data, fin, code) 160 | return "updated " .. role .. " frame (" .. typ .. ")" 161 | end 162 | 163 | local wp, err = proxy.new({ on_frame = on_frame }) 164 | if not wp then 165 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 166 | return ngx.exit(444) 167 | end 168 | 169 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 170 | local ok, err = wp:connect(uri) 171 | if not ok then 172 | ngx.log(ngx.ERR, err) 173 | return ngx.exit(444) 174 | end 175 | 176 | local done, err = wp:execute() 177 | if not done then 178 | ngx.log(ngx.ERR, "failed proxying: ", err) 179 | return ngx.exit(444) 180 | end 181 | } 182 | } 183 | 184 | location /t { 185 | content_by_lua_block { 186 | local client = require "resty.websocket.client" 187 | local wb = assert(client:new()) 188 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 189 | 190 | assert(wb:connect(uri)) 191 | assert(wb:send_binary("你好, WebSocket!")) 192 | local data, opcode = assert(wb:recv_frame()) 193 | ngx.say(opcode, ": ", data) 194 | } 195 | } 196 | --- response_body 197 | binary: updated upstream frame (binary) 198 | --- no_error_log 199 | [error] 200 | [crit] 201 | 202 | 203 | 204 | === TEST 4: opts.on_frame can update an upstream close frame payload 205 | --- log_level: debug 206 | --- http_config eval: $t::Tests::HttpConfig 207 | --- config 208 | location /upstream { 209 | content_by_lua_block { 210 | local server = require "resty.websocket.server" 211 | 212 | local wb, err = server:new() 213 | if not wb then 214 | ngx.log(ngx.ERR, "failed creating server: ", err) 215 | return ngx.exit(444) 216 | end 217 | 218 | local bytes, err = wb:send_close(1000, "server close") 219 | if not bytes then 220 | ngx.log(ngx.ERR, "failed sending close frame: ", err) 221 | return ngx.exit(444) 222 | end 223 | } 224 | } 225 | 226 | location /proxy { 227 | content_by_lua_block { 228 | local proxy = require "resty.websocket.proxy" 229 | local fmt = string.format 230 | 231 | local function on_frame(_, role, typ, data, fin, code) 232 | local msg = fmt("updated %s frame (typ: %s, code: %d)", role, typ, code) 233 | 234 | ngx.log(ngx.DEBUG, fmt("updated %s frame payload from %s to %s", 235 | role, fmt("%q", data), fmt("%q", msg))) 236 | 237 | return msg, code 238 | end 239 | 240 | local wp, err = proxy.new({ on_frame = on_frame }) 241 | if not wp then 242 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 243 | return ngx.exit(444) 244 | end 245 | 246 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 247 | local ok, err = wp:connect(uri) 248 | if not ok then 249 | ngx.log(ngx.ERR, err) 250 | return ngx.exit(444) 251 | end 252 | 253 | local done, err = wp:execute() 254 | if not done then 255 | ngx.log(ngx.ERR, "failed proxying: ", err) 256 | return ngx.exit(444) 257 | end 258 | } 259 | } 260 | 261 | location /t { 262 | content_by_lua_block { 263 | local client = require "resty.websocket.client" 264 | local wb = assert(client:new()) 265 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 266 | 267 | assert(wb:connect(uri)) 268 | local data, typ, err = assert(wb:recv_frame()) 269 | ngx.say(typ) 270 | ngx.say(data) 271 | ngx.say(err) 272 | } 273 | } 274 | --- response_body 275 | close 276 | updated upstream frame (typ: close, code: 1000) 277 | 1000 278 | --- no_error_log 279 | [error] 280 | [crit] 281 | 282 | 283 | 284 | === TEST 5: opts.on_frame can update an upstream close frame status code 285 | --- log_level: debug 286 | --- http_config eval: $::HttpConfig 287 | --- config 288 | location /upstream { 289 | content_by_lua_block { 290 | local server = require "resty.websocket.server" 291 | 292 | local wb, err = server:new() 293 | if not wb then 294 | ngx.log(ngx.ERR, "failed creating server: ", err) 295 | return ngx.exit(444) 296 | end 297 | 298 | local bytes, err = wb:send_close(1000, "server close") 299 | if not bytes then 300 | ngx.log(ngx.ERR, "failed sending close frame: ", err) 301 | return ngx.exit(444) 302 | end 303 | } 304 | } 305 | 306 | location /proxy { 307 | content_by_lua_block { 308 | local proxy = require "resty.websocket.proxy" 309 | local fmt = string.format 310 | 311 | local function on_frame(_, role, typ, data, fin, code) 312 | local updated = 1001 313 | 314 | ngx.log(ngx.DEBUG, fmt("updated %s [%s] status from %s to %s", 315 | role, typ, code, updated)) 316 | 317 | return data, updated 318 | end 319 | 320 | local wp, err = proxy.new({ on_frame = on_frame }) 321 | if not wp then 322 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 323 | return ngx.exit(444) 324 | end 325 | 326 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 327 | local ok, err = wp:connect(uri) 328 | if not ok then 329 | ngx.log(ngx.ERR, err) 330 | return ngx.exit(444) 331 | end 332 | 333 | local done, err = wp:execute() 334 | if not done then 335 | ngx.log(ngx.ERR, "failed proxying: ", err) 336 | return ngx.exit(444) 337 | end 338 | } 339 | } 340 | 341 | location /t { 342 | content_by_lua_block { 343 | local client = require "resty.websocket.client" 344 | local wb = assert(client:new()) 345 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 346 | 347 | assert(wb:connect(uri)) 348 | local data, typ, err = assert(wb:recv_frame()) 349 | ngx.say(typ) 350 | ngx.say(data) 351 | ngx.say(err) 352 | } 353 | } 354 | --- response_body 355 | close 356 | server close 357 | 1001 358 | --- no_error_log 359 | [error] 360 | [crit] 361 | 362 | 363 | 364 | === TEST 6: opts.on_frame can drop an upstream frame 365 | --- log_level: debug 366 | --- http_config eval: $::HttpConfig 367 | --- config 368 | location /upstream { 369 | content_by_lua_block { 370 | local server = require "resty.websocket.server" 371 | 372 | local wb, err = server:new() 373 | if not wb then 374 | ngx.log(ngx.ERR, "failed creating server: ", err) 375 | return ngx.exit(444) 376 | end 377 | 378 | local payloads = { "a", "b", "drop me", "c"} 379 | 380 | for _, data in ipairs(payloads) do 381 | local ok, err = wb:send_text(data) 382 | if not ok then 383 | ngx.log(ngx.ERR, "failed sending payload: ", err) 384 | return ngx.exit(444) 385 | end 386 | end 387 | 388 | local bytes, err = wb:send_close(1000, "server close") 389 | if not bytes then 390 | ngx.log(ngx.ERR, "failed sending close frame: ", err) 391 | return ngx.exit(444) 392 | end 393 | } 394 | } 395 | 396 | location /proxy { 397 | content_by_lua_block { 398 | local proxy = require "resty.websocket.proxy" 399 | 400 | local function on_frame(_, role, typ, data, fin, code) 401 | if typ == "text" and data == "drop me" then 402 | ngx.log(ngx.DEBUG, "dropping 'drop me' frame") 403 | data = nil 404 | end 405 | 406 | return data, code 407 | end 408 | 409 | local wp, err = proxy.new({ on_frame = on_frame }) 410 | if not wp then 411 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 412 | return ngx.exit(444) 413 | end 414 | 415 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 416 | local ok, err = wp:connect(uri) 417 | if not ok then 418 | ngx.log(ngx.ERR, err) 419 | return ngx.exit(444) 420 | end 421 | 422 | local done, err = wp:execute() 423 | if not done then 424 | ngx.log(ngx.ERR, "failed proxying: ", err) 425 | return ngx.exit(444) 426 | end 427 | } 428 | } 429 | 430 | location /t { 431 | content_by_lua_block { 432 | local client = require "resty.websocket.client" 433 | local fmt = string.format 434 | local wb = assert(client:new()) 435 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 436 | 437 | assert(wb:connect(uri)) 438 | 439 | repeat 440 | local data, typ, err = assert(wb:recv_frame()) 441 | ngx.say(fmt("typ: %s, data: %q, err/code: %s", typ, data, err)) 442 | until typ == "close" 443 | } 444 | } 445 | --- response_body 446 | typ: text, data: "a", err/code: nil 447 | typ: text, data: "b", err/code: nil 448 | typ: text, data: "c", err/code: nil 449 | typ: close, data: "server close", err/code: 1000 450 | --- error_log 451 | dropping 'drop me' frame 452 | --- no_error_log 453 | [error] 454 | -------------------------------------------------------------------------------- /t/04-fragmented_frames.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: forwards fragmented frames by default 13 | --- http_config eval: $::HttpConfig 14 | --- config 15 | location /upstream { 16 | content_by_lua_block { 17 | local server = require "resty.websocket.server" 18 | 19 | local wb, err = server:new() 20 | if not wb then 21 | ngx.log(ngx.ERR, "failed creating server: ", err) 22 | return ngx.exit(444) 23 | end 24 | 25 | for i = 1, 2 do 26 | local data, typ, err = wb:recv_frame() 27 | if not data then 28 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 29 | return ngx.exit(444) 30 | end 31 | 32 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 33 | 34 | local bytes, err = wb:send_text(data) 35 | if not bytes then 36 | ngx.log(ngx.ERR, "failed sending frame: ", err) 37 | return ngx.exit(444) 38 | end 39 | end 40 | } 41 | } 42 | 43 | location /proxy { 44 | content_by_lua_block { 45 | local proxy = require "resty.websocket.proxy" 46 | 47 | local wp, err = proxy.new() 48 | if not wp then 49 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 50 | return ngx.exit(444) 51 | end 52 | 53 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 54 | local ok, err = wp:connect(uri) 55 | if not ok then 56 | ngx.log(ngx.ERR, err) 57 | return ngx.exit(444) 58 | end 59 | 60 | local done, err = wp:execute() 61 | if not done then 62 | ngx.log(ngx.ERR, "failed proxying: ", err) 63 | return ngx.exit(444) 64 | end 65 | } 66 | } 67 | 68 | location /t { 69 | content_by_lua_block { 70 | local client = require "resty.websocket.client" 71 | local wb = assert(client:new()) 72 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 73 | 74 | assert(wb:connect(uri)) 75 | 76 | for i = 1, 2 do 77 | if i == 1 then 78 | assert(wb:send_frame(false, 0x1, "hello")) 79 | else 80 | assert(wb:send_frame(true, 0x0, "world")) 81 | end 82 | 83 | local data = assert(wb:recv_frame()) 84 | ngx.say(data) 85 | end 86 | 87 | wb:close() 88 | } 89 | } 90 | --- response_body 91 | hello 92 | world 93 | --- grep_error_log eval: qr/\[lua\].*/ 94 | --- grep_error_log_out eval 95 | qr/.*?frame type: text, payload: "hello".* 96 | .*?frame type: continuation, payload: "world".*/ 97 | --- no_error_log 98 | [error] 99 | 100 | 101 | 102 | === TEST 2: opts.aggregate_fragments assembles fragmented client frames 103 | --- http_config eval: $::HttpConfig 104 | --- config 105 | location /proxy { 106 | content_by_lua_block { 107 | local proxy = require "resty.websocket.proxy" 108 | 109 | local wp, err = proxy.new({ aggregate_fragments = true }) 110 | if not wp then 111 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 112 | return ngx.exit(444) 113 | end 114 | 115 | local ok, err = wp:connect(proxy._tests.echo) 116 | if not ok then 117 | ngx.log(ngx.ERR, err) 118 | return ngx.exit(444) 119 | end 120 | 121 | local done, err = wp:execute() 122 | if not done then 123 | ngx.log(ngx.ERR, "failed proxying: ", err) 124 | return ngx.exit(444) 125 | end 126 | } 127 | } 128 | 129 | location /t { 130 | content_by_lua_block { 131 | local client = require "resty.websocket.client" 132 | local wb = assert(client:new()) 133 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 134 | 135 | assert(wb:connect(uri)) 136 | assert(wb:send_frame(false, 0x1, "hello")) 137 | assert(wb:send_frame(true, 0x0, " world")) 138 | local data = assert(wb:recv_frame()) 139 | ngx.say(data) 140 | wb:close() 141 | } 142 | } 143 | --- response_body 144 | hello world 145 | --- grep_error_log eval: qr/\[lua\].*/ 146 | --- grep_error_log_out eval 147 | qr/.*?frame type: text, payload: "hello world".*/ 148 | --- no_error_log 149 | [error] 150 | 151 | 152 | 153 | === TEST 3: opts.aggregate_fragments assembles fragmented server frames 154 | --- http_config eval: $::HttpConfig 155 | --- config 156 | location /upstream { 157 | content_by_lua_block { 158 | local server = require "resty.websocket.server" 159 | 160 | local wb, err = server:new() 161 | if not wb then 162 | ngx.log(ngx.ERR, "failed creating server: ", err) 163 | return ngx.exit(444) 164 | end 165 | 166 | local data, typ, err = wb:recv_frame() 167 | if not data then 168 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 169 | return ngx.exit(444) 170 | end 171 | 172 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 173 | 174 | local bytes, err = wb:send_frame(false, 0x1, "") 175 | if not bytes then 176 | ngx.log(ngx.ERR, "failed sending initial fragment: ", err) 177 | return ngx.exit(444) 178 | end 179 | 180 | for word in string.gmatch(data, "[^%s]+") do 181 | local bytes, err = wb:send_frame(false, 0x0, word) 182 | if not bytes then 183 | ngx.log(ngx.ERR, "failed sending fragment: ", err) 184 | return ngx.exit(444) 185 | end 186 | end 187 | 188 | local bytes, err = wb:send_frame(true, 0x0, "") 189 | if not bytes then 190 | ngx.log(ngx.ERR, "failed sending last fragment: ", err) 191 | return ngx.exit(444) 192 | end 193 | } 194 | } 195 | 196 | location /proxy { 197 | content_by_lua_block { 198 | local proxy = require "resty.websocket.proxy" 199 | 200 | local wp, err = proxy.new({ aggregate_fragments = true }) 201 | if not wp then 202 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 203 | return ngx.exit(444) 204 | end 205 | 206 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 207 | local ok, err = wp:connect(uri) 208 | if not ok then 209 | ngx.log(ngx.ERR, err) 210 | return ngx.exit(444) 211 | end 212 | 213 | local done, err = wp:execute() 214 | if not done then 215 | ngx.log(ngx.ERR, "failed proxying: ", err) 216 | return ngx.exit(444) 217 | end 218 | } 219 | } 220 | 221 | location /t { 222 | content_by_lua_block { 223 | local client = require "resty.websocket.client" 224 | local wb = assert(client:new()) 225 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 226 | 227 | assert(wb:connect(uri)) 228 | assert(wb:send_text("hello world")) 229 | local data = assert(wb:recv_frame()) 230 | ngx.say(data) 231 | wb:close() 232 | } 233 | } 234 | --- response_body 235 | helloworld 236 | --- grep_error_log eval: qr/\[lua\].*/ 237 | --- grep_error_log_out eval 238 | qr/.*?frame type: text, payload: "hello world".*/ 239 | --- no_error_log 240 | [error] 241 | 242 | 243 | 244 | === TEST 4: opts.aggregate_fragments assembles fragmented frames consecutively 245 | --- http_config eval: $::HttpConfig 246 | --- config 247 | location /upstream { 248 | content_by_lua_block { 249 | local server = require "resty.websocket.server" 250 | 251 | local wb, err = server:new() 252 | if not wb then 253 | ngx.log(ngx.ERR, "failed creating server: ", err) 254 | return ngx.exit(444) 255 | end 256 | 257 | for i = 1, 2 do 258 | local data, typ, err = wb:recv_frame() 259 | if not data then 260 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 261 | return ngx.exit(444) 262 | end 263 | 264 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 265 | 266 | local bytes, err = wb:send_frame(false, 0x1, "") 267 | if not bytes then 268 | ngx.log(ngx.ERR, "failed sending initial fragment: ", err) 269 | return ngx.exit(444) 270 | end 271 | 272 | for word in string.gmatch(data, "[^%s]+") do 273 | local bytes, err = wb:send_frame(false, 0x0, word) 274 | if not bytes then 275 | ngx.log(ngx.ERR, "failed sending fragment: ", err) 276 | return ngx.exit(444) 277 | end 278 | end 279 | 280 | local bytes, err = wb:send_frame(true, 0x0, "") 281 | if not bytes then 282 | ngx.log(ngx.ERR, "failed sending last fragment: ", err) 283 | return ngx.exit(444) 284 | end 285 | end 286 | } 287 | } 288 | 289 | location /proxy { 290 | content_by_lua_block { 291 | local proxy = require "resty.websocket.proxy" 292 | 293 | local wp, err = proxy.new({ aggregate_fragments = true }) 294 | if not wp then 295 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 296 | return ngx.exit(444) 297 | end 298 | 299 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 300 | local ok, err = wp:connect(uri) 301 | if not ok then 302 | ngx.log(ngx.ERR, err) 303 | return ngx.exit(444) 304 | end 305 | 306 | local done, err = wp:execute() 307 | if not done then 308 | ngx.log(ngx.ERR, "failed proxying: ", err) 309 | return ngx.exit(444) 310 | end 311 | } 312 | } 313 | 314 | location /t { 315 | content_by_lua_block { 316 | local client = require "resty.websocket.client" 317 | local wb = assert(client:new()) 318 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 319 | 320 | assert(wb:connect(uri)) 321 | assert(wb:send_frame(false, 0x1, "hello")) 322 | assert(wb:send_frame(true, 0x0, " world")) 323 | local data = assert(wb:recv_frame()) 324 | ngx.say(data) 325 | 326 | assert(wb:send_frame(false, 0x1, "goodbye")) 327 | assert(wb:send_frame(true, 0x0, " world")) 328 | local data = assert(wb:recv_frame()) 329 | ngx.say(data) 330 | 331 | wb:close() 332 | } 333 | } 334 | --- response_body 335 | helloworld 336 | goodbyeworld 337 | --- grep_error_log eval: qr/\[lua\].*/ 338 | --- grep_error_log_out eval 339 | qr/.*?frame type: text, payload: "hello world".* 340 | .*?frame type: text, payload: "goodbye world".*/ 341 | --- no_error_log 342 | [error] 343 | 344 | 345 | 346 | === TEST 5: opts.on_frame with opts.aggregate_fragments 347 | --- http_config eval: $::HttpConfig 348 | --- config 349 | location /upstream { 350 | content_by_lua_block { 351 | local server = require "resty.websocket.server" 352 | 353 | local wb, err = server:new() 354 | if not wb then 355 | ngx.log(ngx.ERR, "failed creating server: ", err) 356 | return ngx.exit(444) 357 | end 358 | 359 | for i = 1, 2 do 360 | local data, typ, err = wb:recv_frame() 361 | if not data then 362 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 363 | return ngx.exit(444) 364 | end 365 | 366 | local bytes, err = wb:send_frame(false, 0x1, "") 367 | if not bytes then 368 | ngx.log(ngx.ERR, "failed sending initial fragment: ", err) 369 | return ngx.exit(444) 370 | end 371 | 372 | for word in string.gmatch(data, "[^%s]+") do 373 | local bytes, err = wb:send_frame(false, 0x0, word) 374 | if not bytes then 375 | ngx.log(ngx.ERR, "failed sending fragment: ", err) 376 | return ngx.exit(444) 377 | end 378 | end 379 | 380 | local bytes, err = wb:send_frame(true, 0x0, "") 381 | if not bytes then 382 | ngx.log(ngx.ERR, "failed sending last fragment: ", err) 383 | return ngx.exit(444) 384 | end 385 | end 386 | } 387 | } 388 | 389 | location /proxy { 390 | content_by_lua_block { 391 | local proxy = require "resty.websocket.proxy" 392 | 393 | local function on_frame(_, role, typ, data, fin, code) 394 | ngx.log(ngx.INFO, "from: ", role, ", type: ", typ, 395 | ", payload: ", data, ", fin: ", fin) 396 | 397 | return "updated " .. role .. " frame", code 398 | end 399 | 400 | local wp, err = proxy.new({ 401 | aggregate_fragments = true, 402 | on_frame = on_frame, 403 | }) 404 | if not wp then 405 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 406 | return ngx.exit(444) 407 | end 408 | 409 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 410 | local ok, err = wp:connect(uri) 411 | if not ok then 412 | ngx.log(ngx.ERR, err) 413 | return ngx.exit(444) 414 | end 415 | 416 | local done, err = wp:execute() 417 | if not done then 418 | ngx.log(ngx.ERR, "failed proxying: ", err) 419 | return ngx.exit(444) 420 | end 421 | } 422 | } 423 | 424 | location /t { 425 | content_by_lua_block { 426 | local client = require "resty.websocket.client" 427 | local wb = assert(client:new()) 428 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 429 | 430 | assert(wb:connect(uri)) 431 | assert(wb:send_frame(false, 0x1, "hello")) 432 | assert(wb:send_frame(true, 0x0, " world")) 433 | local data = assert(wb:recv_frame()) 434 | ngx.say(data) 435 | 436 | assert(wb:send_frame(false, 0x1, "goodbye")) 437 | assert(wb:send_frame(true, 0x0, " world")) 438 | local data = assert(wb:recv_frame()) 439 | ngx.say(data) 440 | 441 | wb:close() 442 | } 443 | } 444 | --- response_body 445 | updated upstream frame 446 | updated upstream frame 447 | --- grep_error_log eval: qr/\[lua\].*/ 448 | --- grep_error_log_out eval 449 | qr/.*?from: client, type: text, payload: hello world, fin: true.* 450 | .*?from: upstream, type: text, payload: updatedclientframe, fin: true.* 451 | .*?from: client, type: text, payload: goodbye world, fin: true.* 452 | .*?from: upstream, type: text, payload: updatedclientframe, fin: true.*/ 453 | --- no_error_log 454 | [error] 455 | 456 | 457 | 458 | === TEST 6: opts.on_frame without opts.aggregate_fragments 459 | --- http_config eval: $::HttpConfig 460 | --- config 461 | location /upstream { 462 | content_by_lua_block { 463 | local server = require "resty.websocket.server" 464 | 465 | local wb, err = server:new() 466 | if not wb then 467 | ngx.log(ngx.ERR, "failed creating server: ", err) 468 | return ngx.exit(444) 469 | end 470 | 471 | for i = 1, 2 do 472 | local data, typ, err = wb:recv_frame() 473 | if not data then 474 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 475 | return ngx.exit(444) 476 | end 477 | 478 | local bytes, err = wb:send_frame(false, 0x1, "") 479 | if not bytes then 480 | ngx.log(ngx.ERR, "failed sending initial fragment: ", err) 481 | return ngx.exit(444) 482 | end 483 | 484 | for word in string.gmatch(data, "[^%s]+") do 485 | local bytes, err = wb:send_frame(false, 0x0, word) 486 | if not bytes then 487 | ngx.log(ngx.ERR, "failed sending fragment: ", err) 488 | return ngx.exit(444) 489 | end 490 | end 491 | 492 | local bytes, err = wb:send_frame(true, 0x0, "") 493 | if not bytes then 494 | ngx.log(ngx.ERR, "failed sending last fragment: ", err) 495 | return ngx.exit(444) 496 | end 497 | end 498 | } 499 | } 500 | 501 | location /proxy { 502 | content_by_lua_block { 503 | local proxy = require "resty.websocket.proxy" 504 | 505 | local function on_frame(_, role, typ, data, fin, code) 506 | ngx.log(ngx.INFO, "from: ", role, ", type: ", typ, 507 | ", payload: ", data, ", fin: ", fin) 508 | 509 | return "updated " .. role .. " frame", code 510 | end 511 | 512 | local wp, err = proxy.new({ 513 | aggregate_fragments = false, 514 | on_frame = on_frame, 515 | }) 516 | if not wp then 517 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 518 | return ngx.exit(444) 519 | end 520 | 521 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 522 | local ok, err = wp:connect(uri) 523 | if not ok then 524 | ngx.log(ngx.ERR, err) 525 | return ngx.exit(444) 526 | end 527 | 528 | local done, err = wp:execute() 529 | if not done then 530 | ngx.log(ngx.ERR, "failed proxying: ", err) 531 | return ngx.exit(444) 532 | end 533 | } 534 | } 535 | 536 | location /t { 537 | content_by_lua_block { 538 | local client = require "resty.websocket.client" 539 | local wb = assert(client:new()) 540 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 541 | 542 | assert(wb:connect(uri)) 543 | assert(wb:send_frame(false, 0x1, "hello")) 544 | assert(wb:send_frame(true, 0x0, " world")) 545 | local data = assert(wb:recv_frame()) 546 | ngx.say(data) 547 | 548 | assert(wb:send_frame(false, 0x1, "goodbye")) 549 | assert(wb:send_frame(true, 0x0, " world")) 550 | local data = assert(wb:recv_frame()) 551 | ngx.say(data) 552 | 553 | wb:close() 554 | } 555 | } 556 | --- response_body 557 | updated upstream frame 558 | updated upstream frame 559 | --- grep_error_log eval: qr/\[lua\].*/ 560 | --- grep_error_log_out eval 561 | qr/.*?from: client, type: text, payload: hello, fin: false.* 562 | .*?from: client, type: continuation, payload: world, fin: true.* 563 | .*?from: upstream, type: text, payload: , fin: false.* 564 | .*?from: upstream, type: continuation, payload: updated, fin: false.* 565 | .*?from: upstream, type: continuation, payload: client, fin: false.* 566 | .*?from: upstream, type: continuation, payload: frame, fin: false.* 567 | .*?from: upstream, type: continuation, payload: , fin: true.* 568 | .*?from: upstream, type: text, payload: , fin: false.* 569 | .*?from: upstream, type: continuation, payload: updated, fin: false.* 570 | .*?from: upstream, type: continuation, payload: client, fin: false.* 571 | .*?from: upstream, type: continuation, payload: frame, fin: false.* 572 | .*?from: upstream, type: continuation, payload: , fin: true.*/ 573 | --- no_error_log 574 | [error] 575 | 576 | 577 | 578 | === TEST 7: control frames interleaved with fragmented data frames (opts.aggregate_fragments off) 579 | --- http_config eval: $::HttpConfig 580 | --- config 581 | location /upstream { 582 | content_by_lua_block { 583 | local server = require "resty.websocket.server" 584 | 585 | local function check(ok, err, msg) 586 | if not ok then 587 | ngx.log(ngx.ERR, msg, ": ", err) 588 | return ngx.exit(444) 589 | end 590 | end 591 | 592 | local wb, err = server:new() 593 | check(wb, err, "failed creating server") 594 | 595 | local ok, err = wb:send_frame(false, 0x1, "") 596 | check(ok, err, "failed sending initial fragment") 597 | 598 | local payloads = { "a", "b", "c" } 599 | 600 | for i, data in ipairs(payloads) do 601 | ok, err = wb:send_frame(false, 0x0, data) 602 | check(ok, err, "failed sending partial data frame") 603 | 604 | ok, err = wb:send_ping(i) 605 | check(ok, err, "failed sending ping") 606 | end 607 | 608 | ok, err = wb:send_frame(true, 0x0, "") 609 | check(ok, err, "failed sending last fragment") 610 | 611 | local bytes, err = wb:send_close(1000, "server close") 612 | check(bytes, err, "failed sending close frame") 613 | } 614 | } 615 | 616 | location /proxy { 617 | content_by_lua_block { 618 | local proxy = require "resty.websocket.proxy" 619 | 620 | local function check(ok, err, msg) 621 | if not ok then 622 | ngx.log(ngx.ERR, msg, ": ", err) 623 | return ngx.exit(444) 624 | end 625 | end 626 | 627 | local wp, err = proxy.new({ aggregate_fragments = false }) 628 | check(wp, err, "failed creating proxy") 629 | 630 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 631 | local ok, err = wp:connect_upstream(uri) 632 | check(ok, err, "failed connecting to upstream") 633 | 634 | ok, err = wp:connect_client() 635 | check(ok, err, "failed client handshake") 636 | 637 | local done, err = wp:execute() 638 | check(done, err, "failed proxying") 639 | } 640 | } 641 | 642 | location /t { 643 | content_by_lua_block { 644 | local client = require "resty.websocket.client" 645 | local fmt = string.format 646 | 647 | local wb = assert(client:new()) 648 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 649 | 650 | assert(wb:connect(uri)) 651 | 652 | repeat 653 | local data, typ, err = wb:recv_frame() 654 | ngx.say(fmt("typ: %s, data: %q, err/code: %s", typ, data, err)) 655 | until typ == "close" 656 | } 657 | } 658 | --- response_body 659 | typ: text, data: "", err/code: again 660 | typ: continuation, data: "a", err/code: again 661 | typ: ping, data: "1", err/code: nil 662 | typ: continuation, data: "b", err/code: again 663 | typ: ping, data: "2", err/code: nil 664 | typ: continuation, data: "c", err/code: again 665 | typ: ping, data: "3", err/code: nil 666 | typ: continuation, data: "", err/code: nil 667 | typ: close, data: "server close", err/code: 1000 668 | --- no_error_log 669 | [error] 670 | [crit] 671 | 672 | 673 | 674 | === TEST 8: control frames interleaved with fragmented data frames (opts.aggregate_fragments on) 675 | --- http_config eval: $::HttpConfig 676 | --- config 677 | location /upstream { 678 | content_by_lua_block { 679 | local server = require "resty.websocket.server" 680 | 681 | local function check(ok, err, msg) 682 | if not ok then 683 | ngx.log(ngx.ERR, msg, ": ", err) 684 | return ngx.exit(444) 685 | end 686 | end 687 | 688 | local wb, err = server:new() 689 | check(wb, err, "failed creating server") 690 | 691 | local ok, err = wb:send_frame(false, 0x1, "") 692 | check(ok, err, "failed sending initial fragment") 693 | 694 | local payloads = { "a", "b", "c" } 695 | 696 | for i, data in ipairs(payloads) do 697 | ok, err = wb:send_frame(false, 0x0, data) 698 | check(ok, err, "failed sending partial data frame") 699 | 700 | ok, err = wb:send_ping(i) 701 | check(ok, err, "failed sending ping") 702 | end 703 | 704 | ok, err = wb:send_frame(true, 0x0, "") 705 | check(ok, err, "failed sending final fragment") 706 | 707 | local bytes, err = wb:send_close(1000, "server close") 708 | check(bytes, err, "failed sending close frame") 709 | } 710 | } 711 | 712 | location /proxy { 713 | content_by_lua_block { 714 | local proxy = require "resty.websocket.proxy" 715 | 716 | local function check(ok, err, msg) 717 | if not ok then 718 | ngx.log(ngx.ERR, msg, ": ", err) 719 | return ngx.exit(444) 720 | end 721 | end 722 | 723 | local wp, err = proxy.new({ aggregate_fragments = true }) 724 | check(wp, err, "failed creating proxy") 725 | 726 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 727 | local ok, err = wp:connect_upstream(uri) 728 | check(ok, err, "failed connecting to upstream") 729 | 730 | ok, err = wp:connect_client() 731 | check(ok, err, "failed client handshake") 732 | 733 | local done, err = wp:execute() 734 | check(done, err, "failed proxying") 735 | } 736 | } 737 | 738 | location /t { 739 | content_by_lua_block { 740 | local client = require "resty.websocket.client" 741 | local fmt = string.format 742 | 743 | local wb = assert(client:new()) 744 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 745 | 746 | assert(wb:connect(uri)) 747 | 748 | repeat 749 | local data, typ, err = wb:recv_frame() 750 | ngx.say(fmt("typ: %s, data: %q, err/code: %s", typ, data, err)) 751 | until typ == "close" 752 | } 753 | } 754 | --- response_body 755 | typ: ping, data: "1", err/code: nil 756 | typ: ping, data: "2", err/code: nil 757 | typ: ping, data: "3", err/code: nil 758 | typ: text, data: "abc", err/code: nil 759 | typ: close, data: "server close", err/code: 1000 760 | --- no_error_log 761 | [error] 762 | [crit] 763 | -------------------------------------------------------------------------------- /t/05-wss.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: forward a text frame back and forth over wss 13 | --- http_config eval 14 | qq{ 15 | $t::Tests::HttpConfig 16 | 17 | server { 18 | listen $ENV{TEST_NGINX_PORT2} ssl; 19 | ssl_certificate $ENV{TEST_NGINX_CERT_DIR}/cert.pem; 20 | ssl_certificate_key $ENV{TEST_NGINX_CERT_DIR}/key.pem; 21 | 22 | location /upstream { 23 | content_by_lua_block { 24 | local server = require "resty.websocket.server" 25 | 26 | local wb, err = server:new() 27 | if not wb then 28 | ngx.log(ngx.ERR, "failed creating server: ", err) 29 | return ngx.exit(444) 30 | end 31 | 32 | local data, typ, err = wb:recv_frame() 33 | if not data then 34 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 35 | return ngx.exit(444) 36 | end 37 | 38 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: ", data) 39 | 40 | local bytes, err = wb:send_text(data) 41 | if not bytes then 42 | ngx.log(ngx.ERR, "failed sending frame: ", err) 43 | return ngx.exit(444) 44 | end 45 | } 46 | } 47 | } 48 | } 49 | --- config 50 | location /proxy { 51 | content_by_lua_block { 52 | local proxy = require "resty.websocket.proxy" 53 | 54 | local wp, err = proxy.new() 55 | if not wp then 56 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 57 | return ngx.exit(444) 58 | end 59 | 60 | local ok, err = wp:connect("wss://127.0.0.1:9001/upstream") 61 | if not ok then 62 | ngx.log(ngx.ERR, err) 63 | return ngx.exit(444) 64 | end 65 | 66 | local done, err = wp:execute() 67 | if not done then 68 | ngx.log(ngx.ERR, "failed proxying: ", err) 69 | return ngx.exit(444) 70 | end 71 | } 72 | } 73 | 74 | location /t { 75 | content_by_lua_block { 76 | local client = require "resty.websocket.client" 77 | local wb = assert(client:new()) 78 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 79 | 80 | assert(wb:connect(uri)) 81 | assert(wb:send_text("hello world!")) 82 | local data = assert(wb:recv_frame()) 83 | ngx.say(data) 84 | } 85 | } 86 | --- response_body 87 | hello world! 88 | --- grep_error_log eval: qr/\[lua\].*/ 89 | --- grep_error_log_out eval 90 | qr/frame type: text, payload: hello world!/ 91 | --- no_error_log 92 | [error] 93 | -------------------------------------------------------------------------------- /t/06-error_handling.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: wss:// proxy over ws:// upstream 13 | --- http_config eval 14 | qq{ 15 | $t::Tests::HttpConfig 16 | 17 | server { 18 | listen $ENV{TEST_NGINX_PORT2}; 19 | 20 | location /upstream { 21 | content_by_lua_block { 22 | local server = require "resty.websocket.server" 23 | 24 | local wb, err = server:new() 25 | if not wb then 26 | ngx.log(ngx.ERR, "failed creating server: ", err) 27 | return ngx.exit(444) 28 | end 29 | 30 | local data, typ, err = wb:recv_frame() 31 | if not data then 32 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 33 | return ngx.exit(444) 34 | end 35 | 36 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: ", data) 37 | 38 | local bytes, err = wb:send_text(data) 39 | if not bytes then 40 | ngx.log(ngx.ERR, "failed sending frame: ", err) 41 | return ngx.exit(444) 42 | end 43 | } 44 | } 45 | } 46 | } 47 | --- config 48 | location /proxy { 49 | content_by_lua_block { 50 | local proxy = require "resty.websocket.proxy" 51 | 52 | local wp, err = proxy.new() 53 | if not wp then 54 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 55 | return ngx.exit(444) 56 | end 57 | 58 | local ok, err = wp:connect("wss://127.0.0.1:9001/upstream") 59 | if not ok then 60 | ngx.log(ngx.ERR, err) 61 | return ngx.exit(444) 62 | end 63 | 64 | local done, err = wp:execute() 65 | if not done then 66 | ngx.log(ngx.ERR, "failed proxying: ", err) 67 | return ngx.exit(444) 68 | end 69 | } 70 | } 71 | 72 | location /t { 73 | content_by_lua_block { 74 | local client = require "resty.websocket.client" 75 | local wb = assert(client:new()) 76 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 77 | 78 | wb:connect(uri) 79 | } 80 | } 81 | --- ignore_response_body 82 | --- error_log 83 | SSL_do_handshake() failed 84 | failed connecting to upstream: ssl handshake failed: handshake failed 85 | --- no_error_log 86 | runtime error 87 | -------------------------------------------------------------------------------- /t/07-invalid_usage.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: calling connect_upstream() while already established logs a warning 13 | --- http_config eval: $t::Tests::HttpConfig 14 | --- config 15 | location /proxy { 16 | content_by_lua_block { 17 | local proxy = require "resty.websocket.proxy" 18 | 19 | local wp, err = proxy.new() 20 | if not wp then 21 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 22 | return ngx.exit(444) 23 | end 24 | 25 | local ok, err = wp:connect(proxy._tests.echo) 26 | if not ok then 27 | ngx.log(ngx.ERR, err) 28 | return ngx.exit(444) 29 | end 30 | 31 | assert(wp:connect_upstream(uri)) 32 | 33 | local done, err = wp:execute() 34 | if not done then 35 | ngx.log(ngx.ERR, "failed proxying: ", err) 36 | end 37 | } 38 | } 39 | 40 | location /t { 41 | content_by_lua_block { 42 | local client = require "resty.websocket.client" 43 | local wb = assert(client:new()) 44 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 45 | 46 | assert(wb:connect(uri)) 47 | assert(wb:send_text("hello world!")) 48 | local data = assert(wb:recv_frame()) 49 | ngx.say(data) 50 | } 51 | } 52 | --- response_body 53 | hello world! 54 | --- grep_error_log eval: qr/\[(info|warn)\].*/ 55 | --- grep_error_log_out eval 56 | qr/\A\[warn\] .*? connection with upstream at "ws:.*?" already established.* 57 | \[info\] .*? frame type: text, payload: "hello world!"/ 58 | --- no_error_log 59 | [error] 60 | 61 | 62 | 63 | === TEST 2: calling connect_client() while client handshake already completed logs a warning 64 | --- http_config eval: $t::Tests::HttpConfig 65 | --- config 66 | location /proxy { 67 | content_by_lua_block { 68 | local proxy = require "resty.websocket.proxy" 69 | 70 | local wp, err = proxy.new() 71 | if not wp then 72 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 73 | return ngx.exit(444) 74 | end 75 | 76 | local ok, err = wp:connect(proxy._tests.echo) 77 | if not ok then 78 | ngx.log(ngx.ERR, err) 79 | return ngx.exit(444) 80 | end 81 | 82 | assert(wp:connect_client(uri)) 83 | 84 | local done, err = wp:execute() 85 | if not done then 86 | ngx.log(ngx.ERR, "failed proxying: ", err) 87 | end 88 | } 89 | } 90 | 91 | location /t { 92 | content_by_lua_block { 93 | local client = require "resty.websocket.client" 94 | local wb = assert(client:new()) 95 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 96 | 97 | assert(wb:connect(uri)) 98 | assert(wb:send_text("hello world!")) 99 | local data = assert(wb:recv_frame()) 100 | ngx.say(data) 101 | } 102 | } 103 | --- response_body 104 | hello world! 105 | --- grep_error_log eval: qr/\[(info|warn)\].*/ 106 | --- grep_error_log_out eval 107 | qr/\A\[warn\] .*? client handshake already completed.* 108 | \[info\] .*? frame type: text, payload: "hello world!"/ 109 | --- no_error_log 110 | [error] 111 | 112 | 113 | 114 | === TEST 3: calling execute() without having completed the client handshake 115 | --- http_config eval: $t::Tests::HttpConfig 116 | --- config 117 | location /proxy { 118 | content_by_lua_block { 119 | local proxy = require "resty.websocket.proxy" 120 | 121 | local wp, err = proxy.new() 122 | if not wp then 123 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 124 | return ngx.exit(444) 125 | end 126 | 127 | local ok, err = wp:connect_upstream(proxy._tests.echo) 128 | if not ok then 129 | ngx.log(ngx.ERR, "failed connecting to upstream: ", err) 130 | return ngx.exit(444) 131 | end 132 | 133 | local done, err = wp:execute() 134 | if not done then 135 | ngx.log(ngx.ERR, "failed proxying: ", err) 136 | return ngx.exit(444) 137 | end 138 | } 139 | } 140 | 141 | location /t { 142 | content_by_lua_block { 143 | local client = require "resty.websocket.client" 144 | local wb = assert(client:new()) 145 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 146 | 147 | assert(wb:connect(uri)) 148 | } 149 | } 150 | --- error_code: 500 151 | --- ignore_response_body 152 | --- grep_error_log eval: qr/\[error\].*/ 153 | --- grep_error_log_out eval 154 | qr/\A\[error\] .*? failed proxying: client handshake not complete.*/ 155 | --- no_error_log 156 | [crit] 157 | [emerg] 158 | 159 | 160 | 161 | === TEST 4: calling execute() without having established the upstream connection 162 | --- http_config eval: $t::Tests::HttpConfig 163 | --- config 164 | location /proxy { 165 | content_by_lua_block { 166 | local proxy = require "resty.websocket.proxy" 167 | 168 | local wp, err = proxy.new({debug = true}) 169 | if not wp then 170 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 171 | return ngx.exit(444) 172 | end 173 | 174 | local ok, err = wp:connect_client() 175 | if not ok then 176 | ngx.log(ngx.ERR, "failed client handshake: ", err) 177 | return ngx.exit(444) 178 | end 179 | 180 | local done, err = wp:execute() 181 | if not done then 182 | ngx.log(ngx.ERR, "failed proxying: ", err) 183 | return ngx.exit(444) 184 | end 185 | } 186 | } 187 | 188 | location /t { 189 | content_by_lua_block { 190 | local client = require "resty.websocket.client" 191 | local wb = assert(client:new()) 192 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 193 | 194 | assert(wb:connect(uri)) 195 | } 196 | } 197 | --- response_body 198 | --- grep_error_log eval: qr/\[error\].*/ 199 | --- grep_error_log_out eval 200 | qr/\A\[error\] .*? failed proxying: upstream connection not established.*/ 201 | --- no_error_log 202 | [crit] 203 | -------------------------------------------------------------------------------- /t/08-limits.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use lib '.'; 4 | use t::Tests; 5 | 6 | plan tests => repeat_each() * (blocks() * 4); 7 | 8 | run_tests(); 9 | 10 | __DATA__ 11 | 12 | === TEST 1: limiting individual frame size (client) 13 | --- http_config eval: $t::Tests::HttpConfig 14 | --- config 15 | location /proxy { 16 | content_by_lua_block { 17 | local proxy = require "resty.websocket.proxy" 18 | 19 | local wp, err = proxy.new({ client_max_frame_size = 10 }) 20 | if not wp then 21 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 22 | return ngx.exit(444) 23 | end 24 | 25 | local ok, err = wp:connect(proxy._tests.echo .. "?repeat=1") 26 | if not ok then 27 | ngx.log(ngx.ERR, err) 28 | return ngx.exit(444) 29 | end 30 | 31 | local done, err = wp:execute() 32 | if not done then 33 | ngx.log(ngx.ERR, "failed proxying: ", err) 34 | return ngx.exit(444) 35 | end 36 | } 37 | } 38 | 39 | location /t { 40 | content_by_lua_block { 41 | local client = require "resty.websocket.client" 42 | local wb = assert(client:new()) 43 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 44 | 45 | assert(wb:connect(uri)) 46 | assert(wb:send_text("this is way too long")) 47 | local data, typ, err = wb:recv_frame() 48 | ngx.say(string.format("data: %q, typ: %s, err: %s", data, typ, err)) 49 | } 50 | } 51 | --- response_body 52 | data: "Payload Too Large", typ: close, err: 1009 53 | --- grep_error_log eval: qr/\[lua\].*/ 54 | --- grep_error_log_out eval 55 | qr/frame type: close, payload: ""/ 56 | --- no_error_log 57 | [error] 58 | 59 | 60 | 61 | === TEST 2: limiting individual frame size (upstream) 62 | --- http_config eval: $t::Tests::HttpConfig 63 | --- config 64 | location /proxy { 65 | content_by_lua_block { 66 | local proxy = require "resty.websocket.proxy" 67 | 68 | local wp, err = proxy.new({ upstream_max_frame_size = 10 }) 69 | if not wp then 70 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 71 | return ngx.exit(444) 72 | end 73 | 74 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 75 | local ok, err = wp:connect(uri) 76 | if not ok then 77 | ngx.log(ngx.ERR, err) 78 | return ngx.exit(444) 79 | end 80 | 81 | local done, err = wp:execute() 82 | if not done then 83 | ngx.log(ngx.ERR, "failed proxying: ", err) 84 | return ngx.exit(444) 85 | end 86 | } 87 | } 88 | 89 | location /upstream { 90 | content_by_lua_block { 91 | local server = require "resty.websocket.server" 92 | 93 | local wb, err = server:new() 94 | if not wb then 95 | ngx.log(ngx.ERR, "failed creating server: ", err) 96 | return ngx.exit(444) 97 | end 98 | 99 | local bytes, err = wb:send_text("this is way too long") 100 | if not bytes then 101 | ngx.log(ngx.ERR, "failed sending frame: ", err) 102 | return ngx.exit(444) 103 | end 104 | 105 | local data, typ, err = wb:recv_frame() 106 | if not data then 107 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 108 | return ngx.exit(444) 109 | end 110 | 111 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\"") 112 | } 113 | } 114 | 115 | location /t { 116 | content_by_lua_block { 117 | local client = require "resty.websocket.client" 118 | local wb = assert(client:new()) 119 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 120 | 121 | assert(wb:connect(uri)) 122 | assert(wb:send_text("hello")) 123 | local data, typ, err = wb:recv_frame() 124 | ngx.say(string.format("data: %q, typ: %s, err: %s", data, typ, err)) 125 | } 126 | } 127 | --- response_body 128 | data: "", typ: close, err: 1001 129 | --- grep_error_log eval: qr/\[lua\].*/ 130 | --- grep_error_log_out eval 131 | qr/frame type: close, payload: "Payload Too Large"/ 132 | --- no_error_log 133 | [error] 134 | 135 | 136 | 137 | === TEST 3: limiting aggregated frame size (client) 138 | --- http_config eval: $t::Tests::HttpConfig 139 | --- config 140 | location /proxy { 141 | content_by_lua_block { 142 | local proxy = require "resty.websocket.proxy" 143 | 144 | local wp, err = proxy.new({ 145 | client_max_frame_size = 10, 146 | aggregate_fragments = true, 147 | }) 148 | if not wp then 149 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 150 | return ngx.exit(444) 151 | end 152 | 153 | local ok, err = wp:connect(proxy._tests.pong .. "?repeat=1") 154 | if not ok then 155 | ngx.log(ngx.ERR, err) 156 | return ngx.exit(444) 157 | end 158 | 159 | local done, err = wp:execute() 160 | if not done then 161 | ngx.log(ngx.ERR, "failed proxying: ", err) 162 | return ngx.exit(444) 163 | end 164 | } 165 | } 166 | 167 | location /t { 168 | content_by_lua_block { 169 | local client = require "resty.websocket.client" 170 | local wb = assert(client:new()) 171 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 172 | 173 | assert(wb:connect(uri)) 174 | 175 | local fmt = string.format 176 | 177 | local function ping_pong(i) 178 | local bytes, err = wb:send_ping(i) 179 | if not bytes then 180 | return nil, fmt("send failed: %s", err) 181 | end 182 | 183 | local data, typ, err = wb:recv_frame() 184 | if not data then 185 | return nil, fmt("recv failed: %s", err) 186 | elseif typ ~= "pong" then 187 | return nil, fmt("unexpected frame %s => %q", typ, data) 188 | end 189 | 190 | return true 191 | end 192 | 193 | for i = 1, 5 do 194 | local opcode = (i == 1 and 0x1) or 0x0 195 | local bytes, err = wb:send_frame(false, opcode, "11") 196 | if not bytes then 197 | ngx.log(ngx.ERR, "failed sending fragment ", i, ": ", err) 198 | return ngx.exit(500) 199 | end 200 | 201 | local ok, err = ping_pong(i) 202 | if not ok then 203 | ngx.log(ngx.ERR, "failed ping-pong: ", err) 204 | return ngx.exit(500) 205 | end 206 | end 207 | 208 | local bytes, err = wb:send_frame(false, 0x0, "1") 209 | if not bytes then 210 | ngx.log(ngx.ERR, "failed sending final fragment: ", err) 211 | return ngx.exit(500) 212 | end 213 | 214 | local data, typ, err = wb:recv_frame() 215 | ngx.say(string.format("data: %q, typ: %s, err: %s", data, typ, err)) 216 | } 217 | } 218 | --- response_body 219 | data: "Payload Too Large", typ: close, err: 1009 220 | --- grep_error_log eval: qr/\[lua\].*/ 221 | --- grep_error_log_out eval 222 | qr/frame type: close, payload: ""/ 223 | --- no_error_log 224 | [error] 225 | 226 | 227 | 228 | === TEST 4: limiting aggregated frame size (upstream) 229 | --- http_config eval: $t::Tests::HttpConfig 230 | --- config 231 | location /proxy { 232 | content_by_lua_block { 233 | local proxy = require "resty.websocket.proxy" 234 | 235 | local wp, err = proxy.new({ 236 | aggregate_fragments = true, 237 | upstream_max_frame_size = 10 238 | }) 239 | if not wp then 240 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 241 | return ngx.exit(444) 242 | end 243 | 244 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 245 | local ok, err = wp:connect(uri) 246 | if not ok then 247 | ngx.log(ngx.ERR, err) 248 | return ngx.exit(444) 249 | end 250 | 251 | local done, err = wp:execute() 252 | if not done then 253 | ngx.log(ngx.ERR, "failed proxying: ", err) 254 | return ngx.exit(444) 255 | end 256 | } 257 | } 258 | 259 | location /upstream { 260 | content_by_lua_block { 261 | local server = require "resty.websocket.server" 262 | 263 | local wb, err = server:new() 264 | if not wb then 265 | ngx.log(ngx.ERR, "failed creating server: ", err) 266 | return ngx.exit(444) 267 | end 268 | 269 | for i = 1, 5 do 270 | local opcode = (i == 1 and 0x1) or 0x0 271 | local bytes, err = wb:send_frame(false, opcode, "11") 272 | if not bytes then 273 | ngx.log(ngx.ERR, "failed sending fragment ", i, ": ", err) 274 | return ngx.exit(444) 275 | end 276 | end 277 | 278 | local bytes, err = wb:send_frame(false, 0x0, "1") 279 | if not bytes then 280 | ngx.log(ngx.ERR, "failed sending final fragment: ", err) 281 | return ngx.exit(444) 282 | end 283 | 284 | local data, typ, err = wb:recv_frame() 285 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\", code: ", err) 286 | } 287 | } 288 | 289 | location /t { 290 | content_by_lua_block { 291 | local client = require "resty.websocket.client" 292 | local wb = assert(client:new()) 293 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 294 | 295 | assert(wb:connect(uri)) 296 | repeat 297 | local data, typ, err = wb:recv_frame() 298 | ngx.say(string.format("data: %q, typ: %s, err: %s", 299 | data, typ, err)) 300 | until typ == "close" or not data 301 | } 302 | } 303 | --- response_body 304 | data: "", typ: close, err: 1001 305 | --- grep_error_log eval: qr/\[lua\].*/ 306 | --- grep_error_log_out eval 307 | qr/frame type: close, payload: "Payload Too Large", code: 1009/ 308 | --- no_error_log 309 | [error] 310 | 311 | 312 | 313 | === TEST 5: control frames are not subject to max_frame_size 314 | --- http_config eval: $t::Tests::HttpConfig 315 | --- config 316 | location /proxy { 317 | content_by_lua_block { 318 | local proxy = require "resty.websocket.proxy" 319 | 320 | local wp, err = proxy.new({ 321 | client_max_frame_size = 2, 322 | upstream_max_frame_size = 2, 323 | }) 324 | if not wp then 325 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 326 | return ngx.exit(444) 327 | end 328 | 329 | local ok, err = wp:connect(proxy._tests.pong .. "?repeat=1") 330 | if not ok then 331 | ngx.log(ngx.ERR, err) 332 | return ngx.exit(444) 333 | end 334 | 335 | local done, err = wp:execute() 336 | if not done then 337 | ngx.log(ngx.ERR, "failed proxying: ", err) 338 | return ngx.exit(444) 339 | end 340 | } 341 | } 342 | 343 | location /t { 344 | content_by_lua_block { 345 | local client = require "resty.websocket.client" 346 | local wb = assert(client:new()) 347 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 348 | 349 | assert(wb:connect(uri)) 350 | 351 | for i = 1, 2 do 352 | local sent, err = wb:send_ping("test-" .. i) 353 | if not sent then 354 | ngx.log(ngx.ERR, "failed sending ping: ", err) 355 | return ngx.exit(500) 356 | end 357 | 358 | local data, typ, err = wb:recv_frame() 359 | if not data then 360 | ngx.log(ngx.ERR, "failed to receive pong: ", err) 361 | return ngx.exit(500) 362 | 363 | elseif typ ~= "pong" then 364 | ngx.log(ngx.ERR, "unexpecting response to ping: ", typ) 365 | return ngx.exit(500) 366 | 367 | elseif #data <= 2 then 368 | ngx.log(ngx.ERR, "broken test--pong frame is too short") 369 | return ngx.exit(500) 370 | end 371 | 372 | ngx.say(string.format("data: %q, typ: %s", data, typ)) 373 | end 374 | 375 | assert(wb:send_close(1002, "goodbye")) 376 | } 377 | } 378 | --- response_body 379 | data: "heartbeat server", typ: pong 380 | data: "heartbeat server", typ: pong 381 | --- grep_error_log eval: qr/\[lua\].*/ 382 | --- grep_error_log_out eval 383 | qr/.*?frame type: ping, payload: "test-1".* 384 | .*?frame type: ping, payload: "test-2".* 385 | .*?forwarding close with code: 1002.* 386 | .*?frame type: close, payload: "goodbye".*/ 387 | --- no_error_log 388 | [error] 389 | 390 | 391 | 392 | === TEST 6: limiting the number of fragments (client) 393 | --- http_config eval: $t::Tests::HttpConfig 394 | --- config 395 | location /proxy { 396 | content_by_lua_block { 397 | local proxy = require "resty.websocket.proxy" 398 | 399 | local wp, err = proxy.new({ 400 | client_max_fragments = 5, 401 | aggregate_fragments = true, 402 | debug = true, 403 | }) 404 | if not wp then 405 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 406 | return ngx.exit(444) 407 | end 408 | 409 | local ok, err = wp:connect(proxy._tests.pong .. "?repeat=1") 410 | if not ok then 411 | ngx.log(ngx.ERR, err) 412 | return ngx.exit(444) 413 | end 414 | 415 | local done, err = wp:execute() 416 | if not done then 417 | ngx.log(ngx.ERR, "failed proxying: ", err) 418 | return ngx.exit(444) 419 | end 420 | } 421 | } 422 | 423 | location /t { 424 | content_by_lua_block { 425 | local client = require "resty.websocket.client" 426 | local wb = assert(client:new()) 427 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 428 | 429 | assert(wb:connect(uri)) 430 | 431 | local fmt = string.format 432 | 433 | local function ping_pong(i) 434 | local bytes, err = wb:send_ping(i) 435 | if not bytes then 436 | return nil, fmt("send failed: %s", err) 437 | end 438 | 439 | local data, typ, err = wb:recv_frame() 440 | if not data then 441 | return nil, fmt("recv failed: %s", err) 442 | elseif typ ~= "pong" then 443 | return nil, fmt("unexpected frame %s => %q", typ, data) 444 | end 445 | 446 | return true 447 | end 448 | 449 | for i = 1, 5 do 450 | local opcode = (i == 1 and 0x1) or 0x0 451 | local bytes, err = wb:send_frame(false, opcode, "11") 452 | if not bytes then 453 | ngx.log(ngx.ERR, "failed sending fragment ", i, ": ", err) 454 | return ngx.exit(500) 455 | end 456 | 457 | local ok, err = ping_pong(i) 458 | if not ok then 459 | ngx.log(ngx.ERR, "failed ping-pong: ", err) 460 | return ngx.exit(500) 461 | end 462 | end 463 | 464 | local bytes, err = wb:send_frame(false, 0x0, "1") 465 | if not bytes then 466 | ngx.log(ngx.ERR, "failed sending final fragment: ", err) 467 | return ngx.exit(500) 468 | end 469 | 470 | local data, typ, err = wb:recv_frame() 471 | ngx.say(string.format("data: %q, typ: %s, err: %s", data, typ, err)) 472 | } 473 | } 474 | --- response_body 475 | data: "Payload Too Large", typ: close, err: 1009 476 | --- grep_error_log eval: qr/\[lua\].*/ 477 | --- grep_error_log_out eval 478 | qr/frame type: close, payload: ""/ 479 | --- no_error_log 480 | [error] 481 | 482 | 483 | 484 | === TEST 7: limiting the number of fragments (upstream) 485 | --- http_config eval: $t::Tests::HttpConfig 486 | --- config 487 | location /proxy { 488 | content_by_lua_block { 489 | local proxy = require "resty.websocket.proxy" 490 | 491 | local wp, err = proxy.new({ 492 | aggregate_fragments = true, 493 | upstream_max_fragments = 5, 494 | }) 495 | if not wp then 496 | ngx.log(ngx.ERR, "failed creating proxy: ", err) 497 | return ngx.exit(444) 498 | end 499 | 500 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/upstream" 501 | local ok, err = wp:connect(uri) 502 | if not ok then 503 | ngx.log(ngx.ERR, err) 504 | return ngx.exit(444) 505 | end 506 | 507 | local done, err = wp:execute() 508 | if not done then 509 | ngx.log(ngx.ERR, "failed proxying: ", err) 510 | return ngx.exit(444) 511 | end 512 | } 513 | } 514 | 515 | location /upstream { 516 | content_by_lua_block { 517 | local server = require "resty.websocket.server" 518 | 519 | local wb, err = server:new() 520 | if not wb then 521 | ngx.log(ngx.ERR, "failed creating server: ", err) 522 | return ngx.exit(444) 523 | end 524 | 525 | for i = 1, 5 do 526 | local opcode = (i == 1 and 0x1) or 0x0 527 | local bytes, err = wb:send_frame(false, opcode, "11") 528 | if not bytes then 529 | ngx.log(ngx.ERR, "failed sending fragment ", i, ": ", err) 530 | return ngx.exit(444) 531 | end 532 | end 533 | 534 | local bytes, err = wb:send_frame(false, 0x0, "1") 535 | if not bytes then 536 | ngx.log(ngx.ERR, "failed sending final fragment: ", err) 537 | return ngx.exit(444) 538 | end 539 | 540 | local data, typ, err = wb:recv_frame() 541 | ngx.log(ngx.INFO, "frame type: ", typ, ", payload: \"", data, "\", code: ", err) 542 | } 543 | } 544 | 545 | location /t { 546 | content_by_lua_block { 547 | local client = require "resty.websocket.client" 548 | local wb = assert(client:new()) 549 | local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/proxy" 550 | 551 | assert(wb:connect(uri)) 552 | repeat 553 | local data, typ, err = wb:recv_frame() 554 | ngx.say(string.format("data: %q, typ: %s, err: %s", 555 | data, typ, err)) 556 | until typ == "close" or not data 557 | } 558 | } 559 | --- response_body 560 | data: "", typ: close, err: 1001 561 | --- grep_error_log eval: qr/\[lua\].*/ 562 | --- grep_error_log_out eval 563 | qr/frame type: close, payload: "Payload Too Large", code: 1009/ 564 | --- no_error_log 565 | [error] -------------------------------------------------------------------------------- /t/Tests.pm: -------------------------------------------------------------------------------- 1 | package t::Tests; 2 | 3 | use strict; 4 | use Test::Nginx::Socket::Lua -Base; 5 | use Cwd qw(cwd); 6 | 7 | our $pwd = cwd(); 8 | 9 | # TODO: switch to unix sockets once supported by lua-resty-websocket 10 | # -> will conflict when using TEST_NGINX_RANDOMIZE 11 | $ENV{TEST_NGINX_PORT_UPSTREAM} ||= 1985; 12 | $ENV{TEST_NGINX_PORT2} ||= 9001; 13 | $ENV{TEST_NGINX_CERT_DIR} ||= File::Spec->catdir(server_root(), '..', 'certs'); 14 | 15 | our $HttpConfig = qq{ 16 | lua_package_path "$pwd/lib/?.lua;$pwd/misc/lua-resty-websocket/lib/?.lua;;"; 17 | 18 | init_worker_by_lua_block { 19 | local proxy = require "resty.websocket.proxy" 20 | 21 | proxy._tests = { 22 | echo = "ws://127.0.0.1:$ENV{TEST_NGINX_PORT_UPSTREAM}/echo", 23 | pong = "ws://127.0.0.1:$ENV{TEST_NGINX_PORT_UPSTREAM}/pong", 24 | } 25 | } 26 | 27 | server { 28 | listen $ENV{TEST_NGINX_PORT_UPSTREAM}; 29 | 30 | location /echo { 31 | content_by_lua_block { 32 | local server = require "resty.websocket.server" 33 | 34 | local wb, err = server:new() 35 | if not wb then 36 | ngx.log(ngx.ERR, "failed creating server: ", err) 37 | return ngx.exit(444) 38 | end 39 | 40 | local once = not ngx.var.arg_repeat 41 | 42 | repeat 43 | local data, typ, err = wb:recv_frame() 44 | if not data then 45 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 46 | return ngx.exit(444) 47 | end 48 | 49 | ngx.log(ngx.INFO, "frame type: ", typ, 50 | ", payload: \\"", data, 51 | "\\"") 52 | 53 | local bytes, err 54 | if typ == "close" then 55 | bytes, err = wb:send_close(err, data) 56 | else 57 | bytes, err = wb:send_text(data) 58 | end 59 | 60 | if not bytes then 61 | ngx.log(ngx.ERR, "failed sending frame: ", err) 62 | return ngx.exit(444) 63 | end 64 | until typ == "close" or once 65 | } 66 | } 67 | 68 | location /pong { 69 | content_by_lua_block { 70 | local server = require "resty.websocket.server" 71 | 72 | local wb, err = server:new() 73 | if not wb then 74 | ngx.log(ngx.ERR, "failed creating server: ", err) 75 | return ngx.exit(444) 76 | end 77 | 78 | local once = not ngx.var.arg_repeat 79 | 80 | repeat 81 | local data, typ, err = wb:recv_frame() 82 | if not data then 83 | ngx.log(ngx.ERR, "failed receiving frame: ", err) 84 | return ngx.exit(444) 85 | end 86 | 87 | ngx.log(ngx.INFO, "frame type: ", typ, 88 | ", payload: \\"", data, "\\"") 89 | 90 | local bytes, err = wb:send_pong("heartbeat server") 91 | if not bytes then 92 | ngx.log(ngx.ERR, "failed sending frame: ", err) 93 | return ngx.exit(444) 94 | end 95 | until typ == "close" or once 96 | } 97 | } 98 | } 99 | }; 100 | 101 | our @EXPORT = qw( 102 | $pwd 103 | $HttpConfig 104 | ); 105 | 106 | add_block_preprocessor(sub { 107 | my $block = shift; 108 | 109 | if (!defined $block->request) { 110 | $block->set_value("request", "GET /t"); 111 | } 112 | }); 113 | 114 | log_level('info'); 115 | no_long_string(); 116 | 117 | 1; 118 | -------------------------------------------------------------------------------- /t/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfGgAwIBAgIUG/V46duZqnXkisF9s+Vs0iMhHzAwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMTAyOTAxMjcwMVoXDTMxMTAy 4 | NzAxMjcwMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEAtM8uxNfetZHmupQ2dlIb81gFm78jeRMJmjDaX0B7HADo 6 | X7XER7SQMgH5We2XGXxcFFg/q2dQyMO85DiWizij4Zh37NeFQXAHhhVXKNvS2RPT 7 | +x2SY5iFKzwdxPT7d6fBJwOpAuHRVYAl3c/tWEqHGwoQBlVF7SksSJTIoP8+Ar0s 8 | lFr6/SnoHMUjXxqRTVG+3RGASV2wo25YdZbwyGhmoe9rYSWIdt5gKh8F84JyRGdu 9 | BSaSOfpV9P56Et08d26Bgy6xK3G5Q50+aqqHNbt36DS866aKlnxgRqbq10NPjcTb 10 | asHnE5YebodJU6clnreSb1xMuqHcSzyJJAkQYIOttwIDAQABo1MwUTAdBgNVHQ4E 11 | FgQUI+SBnc0cXEAG0h/iQWBWNhf52K0wHwYDVR0jBBgwFoAUI+SBnc0cXEAG0h/i 12 | QWBWNhf52K0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAL/h8 13 | eD4iLRY+GLOBAnB14UYTCD7kEzgsXALPI0TVh66Gt89wMtzMC/4//xhpRtysMctn 14 | 510pbJrvpLmX1hp+cXL3wKpKgHT2NO7F/5bHt6mAttm9LWgKRsH2pqngIjDEROFt 15 | 9IWNsAVEAOAm+p/45rkR4Ca3DGenMxSTw9nDr8rODhnN62smC9T1QU/7QPPJTRYA 16 | XyFnPxj5uOJ5/9CHVOtftG862Q85P/p8Jm9OEBWQLJ911rGnCfKPoN1aCm5OnZjG 17 | rhk00FOe6GnWWwO0EBE+B9TkQ614o9q0a5z9xwnt0zoPrI87O6jVVcbB7X2yuIY5 18 | gEPKlPyPwYaW9eGi9g== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /t/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0zy7E1961kea6 3 | lDZ2UhvzWAWbvyN5EwmaMNpfQHscAOhftcRHtJAyAflZ7ZcZfFwUWD+rZ1DIw7zk 4 | OJaLOKPhmHfs14VBcAeGFVco29LZE9P7HZJjmIUrPB3E9Pt3p8EnA6kC4dFVgCXd 5 | z+1YSocbChAGVUXtKSxIlMig/z4CvSyUWvr9KegcxSNfGpFNUb7dEYBJXbCjblh1 6 | lvDIaGah72thJYh23mAqHwXzgnJEZ24FJpI5+lX0/noS3Tx3boGDLrErcblDnT5q 7 | qoc1u3foNLzrpoqWfGBGpurXQ0+NxNtqwecTlh5uh0lTpyWet5JvXEy6odxLPIkk 8 | CRBgg623AgMBAAECggEABxtdqz8Q8fIaL5tDyMLRdm8vST/qFQg6iGUDMFtIe4FN 9 | oPV0EZ13TX/mXTKJjebld6dAWWxmMH4BHrdas342ctebXyPZiQjuQsbz9hct4np1 10 | CpnO9zs3gMuMIAHBIKLnZLEwI3zczY+L7XwOyDmltdpfZuBTL08+/ltax3lA5rqX 11 | 0vti0HdgTtNWY8ur4sDpRWNP86gvoLn8wljI/bnnQ8Qjh+XNMaJ4dMl0jm8pR6aE 12 | JaGn95xrJfuoPfxAjYoskxC+JTDZ56gUS+QFrG2Ys2mvI8s/PDGpeT7JCNTHo5kZ 13 | M7xHB1TKyRWNQZOabRqQb9dgwGQTufxBPCzWsxYYAQKBgQDvp1wRe3x0ka/7Zauu 14 | BZjEUdPud8GcJ5g7E/6aCong10z3B2DqJnTZUKFfDSxqxRyH6zVQfL/2mc6Ae1hA 15 | Qanvz8hRzG3N9/fqMQZwmvl/+dyUcPUIA9qPSocQPOq23D06ml0YM2WY3a4/DrGN 16 | KdMeYcbwBqJbPlESQ+155pRF9wKBgQDBJFQzD8U5r0qzMOFnaW6kDfcTguNObikM 17 | QT4rKbSvXWCJalTO3fYKGn5QQJmipXlto7N7pogC5Niyq75faSyCExH6m4mS/yAq 18 | fS/bujvkv/7caCuk0I4sH+AGXiQT4r+ZLamzfG5S5RZsJRKdKBLPPhlHQp+w8eP9 19 | MWRx6Y7mQQKBgB1XiFY7oElFbR6CnDp6RPIEcsZHs1TDJEhXcly53ZfBxJKaPKtu 20 | efABh0B0BHQuHggmLCjmnTo8KqRgdbWoVH4gGo6pUOhe6+OojPlMgC/DD9a83cNV 21 | dXUF0vOSAcrRvE1oiO1lnZLR0Xu2+NYnwMl/fzP0G/y/7H0oA/Ng39aRAoGACBY1 22 | UE4AvAGS5x3M0j+f2k6bYb3BNq92GIVCzRvZ7N3EBPzmwCKbAqFJWKLby+uRwf1w 23 | rmSynSxnxlNajTQiIPAbeJq6j+UOu7iyIEkT6OgBC8lwyl3RFhffkWSvrgV4cDlx 24 | OYqkM+RTpAOJW/spjnPTKyCm/wmhuiAvTHWy/gECgYEA0owJNvNNNK/RCPCS8N1H 25 | KoSeMRZIQHd7jBy9a1ZhZGSjkIQLC5GOpJacnIThiTSjy1wcKrJW2m2j0HjruVdL 26 | gzE4GsDant2ZX4+kXCrL0qvCI7IZ+STNCC/Fdya3rnbUOuVBoiHu5XHjltH2drCX 27 | SJqZyEN5i/QzQ6lWhsnhYOo= 28 | -----END PRIVATE KEY----- 29 | --------------------------------------------------------------------------------