├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── README.md ├── edoc-info ├── elli_bstr.md ├── elli_example_websocket.md ├── elli_proplists.md ├── elli_websocket.md ├── elli_websocket_handler.md ├── elli_ws_http.md ├── elli_ws_protocol.md ├── elli_ws_request_adapter.md ├── erlang.png └── stylesheet.css ├── elvis.config ├── include └── elli_websocket.hrl ├── rebar.config ├── rebar.config.script ├── rebar.lock ├── src ├── elli_bstr.erl ├── elli_example_websocket.erl ├── elli_proplists.erl ├── elli_websocket.app.src ├── elli_websocket.erl ├── elli_websocket_handler.erl ├── elli_ws_http.erl ├── elli_ws_protocol.erl └── elli_ws_request_adapter.erl └── test ├── elli_bstr_tests.erl ├── elli_ws_http_tests.erl └── ws_test.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: erlang 3 | install: true 4 | before_script: 5 | - wget https://s3.amazonaws.com/rebar3/rebar3 6 | - chmod +x rebar3 7 | env: PATH=$PATH:. 8 | cache: 9 | directories: 10 | - $HOME/.cache/rebar3/ 11 | otp_release: 12 | - 21.0 13 | - 20.2 14 | - 19.3 15 | - 19.0 16 | - 18.3 17 | - 18.0 18 | script: 19 | - rebar3 as test do xref, dialyzer, eunit 20 | - rebar3 as test coveralls send 21 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elli_websocket 2 | 3 | *A WebSocket handler for [elli][]* 4 | 5 | [![Hex.pm][hex badge]][hex package] 6 | [![Erlang][erlang badge]][erlang downloads] 7 | [![Travis CI][travis badge]][travis builds] 8 | [![Coverage Status][coveralls badge]][coveralls link] 9 | [![Apache License][license badge]](LICENSE) 10 | 11 | [elli]: https://github.com/elli-lib/elli 12 | [hex badge]: https://img.shields.io/hexpm/v/elli_websocket.svg 13 | [hex package]: https://hex.pm/packages/elli_websocket 14 | [erlang badge]: https://img.shields.io/badge/erlang-%E2%89%A518.0-red.svg 15 | [erlang downloads]: http://www.erlang.org/downloads 16 | [travis builds]: https://travis-ci.org/elli-lib/elli_websocket 17 | [travis badge]: https://travis-ci.org/elli-lib/elli_websocket.svg 18 | [coveralls badge]: https://coveralls.io/repos/github/elli-lib/elli_websocket/badge.svg?branch=develop 19 | [coveralls link]: https://coveralls.io/github/elli-lib/elli_websocket?branch=develop 20 | [license badge]: https://img.shields.io/hexpm/l/elli_websocket.svg 21 | 22 | 23 | ## Installation 24 | 25 | You can add `elli_websocket` to your application by adding it as a dependency alongside [elli][]. 26 | 27 | ```erlang 28 | {deps, [ 29 | {elli, "3.1.0"}, 30 | {elli_websocket, "0.1.1"} 31 | ]}. 32 | ``` 33 | 34 | 35 | ## Examples 36 | 37 | ### Callback Module 38 | 39 | See [elli_example_websocket.erl](./src/elli_example_websocket.erl) for details. 40 | 41 | ```erlang 42 | -module(elli_echo_websocket_handler). 43 | 44 | -export([websocket_init/1, 45 | websocket_handle/3, 46 | websocket_info/3, 47 | websocket_handle_event/3]). 48 | 49 | 50 | websocket_init(Req, Opts) -> 51 | State = undefined, 52 | {ok, [], State}. 53 | 54 | 55 | websocket_handle(_Req, {text, Data}, State) -> 56 | {reply, {text, Data}, State}; 57 | websocket_handle(_Req, {binary, Data}, State) -> 58 | {reply, {binary, Data}, State}; 59 | websocket_handle(_Req, _Frame, State) -> 60 | {ok, State}. 61 | 62 | 63 | websocket_info(Req, Message, State) -> 64 | {ok, State}. 65 | 66 | 67 | websocket_handle_event(Name, EventArgs, State) -> 68 | ok. 69 | ``` 70 | 71 | 72 | ### Upgrading to a WebSocket Connection 73 | 74 | ```erlang 75 | -module(elli_echo_websocket). 76 | 77 | -export([init/2, handle/2, handle_event/3]). 78 | 79 | -include_lib("elli/include/elli.hrl"). 80 | 81 | -behaviour(elli_handler). 82 | 83 | 84 | init(Req, Args) -> 85 | Method = case elli_request:get_header(<<"Upgrade">>, Req) of 86 | <<"websocket">> -> 87 | init_ws(elli_request:path(Req), Req, Args); 88 | _ -> 89 | ignore 90 | end. 91 | 92 | 93 | handle(Req, Args) -> 94 | Method = case elli_request:get_header(<<"Upgrade">>, Req) of 95 | <<"websocket">> -> 96 | websocket; 97 | _ -> 98 | elli_request:method(Req) 99 | end, 100 | handle(Method, elli_request:path(Req), Req, Args). 101 | 102 | 103 | handle_event(_Event, _Data, _Args) -> 104 | ok. 105 | 106 | 107 | %% 108 | %% Helpers 109 | %% 110 | 111 | init_ws([<<"echo_websocket">>], _Req, _Args) -> 112 | {ok, handover}; 113 | init_ws(_, _, _) -> 114 | ignore. 115 | 116 | 117 | handle('websocket', [<<"echo_websocket">>], Req, Args) -> 118 | %% Upgrade to a websocket connection. 119 | elli_websocket:upgrade(Req, Args), 120 | 121 | %% websocket is closed: 122 | %% See RFC-6455 (https://tools.ietf.org/html/rfc6455) for a list of 123 | %% valid WS status codes than can be used on a close frame. 124 | %% Note that the second element is the reason and is abitrary but should be meaningful 125 | %% in regards to your server and sub-protocol. 126 | {<<"1000">>, <<"Closed">>}; 127 | 128 | handle('GET', [<<"echo_websocket">>], _Req, _Args) -> 129 | %% We got a normal request, request was not upgraded. 130 | {200, [], <<"Use an upgrade request">>}; 131 | handle(_,_,_,_) -> 132 | ignore. 133 | ``` 134 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # The elli_websocket application # 4 | 5 | 6 | ## Modules ## 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
elli_bstr
elli_example_websocket
elli_proplists
elli_websocket
elli_websocket_handler
elli_ws_http
elli_ws_protocol
elli_ws_request_adapter
18 | 19 | -------------------------------------------------------------------------------- /doc/edoc-info: -------------------------------------------------------------------------------- 1 | %% encoding: UTF-8 2 | {application,elli_websocket}. 3 | {modules,[elli_bstr,elli_example_websocket,elli_proplists,elli_websocket, 4 | elli_websocket_handler,elli_ws_http,elli_ws_protocol, 5 | elli_ws_request_adapter]}. 6 | -------------------------------------------------------------------------------- /doc/elli_bstr.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_bstr # 4 | * [Description](#description) 5 | * [Data Types](#types) 6 | * [Function Index](#index) 7 | * [Function Details](#functions) 8 | 9 | Binary String Helper Functions. 10 | 11 | Copyright (c) 2013, Maas-Maarten Zeeman; 2018, elli-lib team 12 | 13 | __Authors:__ Maas-Maarten Zeeman ([`mmzeeman@xs4all.nl`](mailto:mmzeeman@xs4all.nl)). 14 | 15 | 16 | 17 | ## Data Types ## 18 | 19 | 20 | 21 | 22 | ### ascii_char() ### 23 | 24 | 25 |

 26 | ascii_char() = 0..127
 27 | 
28 | 29 | 30 | 31 | ## Function Index ## 32 | 33 | 34 |
is_equal_ci/2Compare two binary values.
lchr/1convert character to lowercase.
to_lower/1Convert ascii Bin to lowercase.
trim/1Remove leading and trailing whitespace.
trim_left/1Remove leading whitespace from Bin.
trim_right/1Remove trailing whitespace from Bin.
35 | 36 | 37 | 38 | 39 | ## Function Details ## 40 | 41 | 42 | 43 | ### is_equal_ci/2 ### 44 | 45 |

 46 | is_equal_ci(Bin::binary(), Bin2::binary()) -> boolean()
 47 | 
48 |
49 | 50 | Compare two binary values. 51 | Return true iff they are equal by a caseless compare. 52 | 53 | 54 | 55 | ### lchr/1 ### 56 | 57 |

 58 | lchr(Chr::ascii_char()) -> ascii_char()
 59 | 
60 |
61 | 62 | convert character to lowercase. 63 | 64 | 65 | 66 | ### to_lower/1 ### 67 | 68 |

 69 | to_lower(Bin::binary()) -> binary()
 70 | 
71 |
72 | 73 | Convert ascii Bin to lowercase 74 | 75 | 76 | 77 | ### trim/1 ### 78 | 79 |

 80 | trim(Bin::binary()) -> binary()
 81 | 
82 |
83 | 84 | Remove leading and trailing whitespace. 85 | 86 | 87 | 88 | ### trim_left/1 ### 89 | 90 |

 91 | trim_left(Bin::binary()) -> binary()
 92 | 
93 |
94 | 95 | Remove leading whitespace from Bin 96 | 97 | 98 | 99 | ### trim_right/1 ### 100 | 101 |

102 | trim_right(Bin::binary()) -> binary()
103 | 
104 |
105 | 106 | Remove trailing whitespace from Bin 107 | 108 | -------------------------------------------------------------------------------- /doc/elli_example_websocket.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_example_websocket # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`elli_handler`](https://github.com/elli-lib/elli/blob/develop/doc/elli_handler.md), [`elli_websocket_handler`](elli_websocket_handler.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
handle/2
handle_event/3
init/2
websocket_handle/3
websocket_handle_event/3
websocket_info/3
websocket_init/2.
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### handle/2 ### 24 | 25 | `handle(Req, Args) -> any()` 26 | 27 | 28 | 29 | ### handle_event/3 ### 30 | 31 | `handle_event(Name, EventArgs, ElliArgs) -> any()` 32 | 33 | 34 | 35 | ### init/2 ### 36 | 37 | `init(Req, Args) -> any()` 38 | 39 | 40 | 41 | ### websocket_handle/3 ### 42 | 43 | `websocket_handle(Req, Message, State) -> any()` 44 | 45 | 46 | 47 | ### websocket_handle_event/3 ### 48 | 49 | `websocket_handle_event(X1, X2, X3) -> any()` 50 | 51 | 52 | 53 | ### websocket_info/3 ### 54 | 55 | `websocket_info(Req, Message, State) -> any()` 56 | 57 | 58 | 59 | ### websocket_init/2 ### 60 | 61 | `websocket_init(Req, Opts) -> any()` 62 | 63 | -------------------------------------------------------------------------------- /doc/elli_proplists.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_proplists # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
get_all_values_ci/2
get_value_ci/2
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### get_all_values_ci/2 ### 22 | 23 | `get_all_values_ci(Key, Proplist) -> any()` 24 | 25 | 26 | 27 | ### get_value_ci/2 ### 28 | 29 | `get_value_ci(Key, Rest) -> any()` 30 | 31 | -------------------------------------------------------------------------------- /doc/elli_websocket.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_websocket # 4 | * [Description](#description) 5 | * [Data Types](#types) 6 | * [Function Index](#index) 7 | * [Function Details](#functions) 8 | 9 | Elli Websocket Handler. 10 | 11 | Copyright (c) 2012-2013, Maas-Maarten Zeeman; 2018, elli-lib team 12 | 13 | __Authors:__ Maas-Maarten Zeeman ([`mmzeeman@xs4all.nl`](mailto:mmzeeman@xs4all.nl)). 14 | 15 | 16 | 17 | ## Data Types ## 18 | 19 | 20 | 21 | 22 | ### event() ### 23 | 24 | 25 |

26 | event() = websocket_open | websocket_close | websocket_throw | websocket_error | websocket_exit
27 | 
28 | 29 | 30 | 31 | 32 | ### message() ### 33 | 34 | 35 |

36 | message() = {text, payload()} | {binary, payload()} | {ping, payload()} | {pong, payload()}
37 | 
38 | 39 | 40 | 41 | 42 | ### payload() ### 43 | 44 | 45 |

46 | payload() = binary() | iolist()
47 | 
48 | 49 | 50 | 51 | ## Function Index ## 52 | 53 | 54 |
upgrade/2Upgrade the request to a websocket, will respond with 55 | bad request when something is wrong.
56 | 57 | 58 | 59 | 60 | ## Function Details ## 61 | 62 | 63 | 64 | ### upgrade/2 ### 65 | 66 |

67 | upgrade(Req::elli:req(), Args::list()) -> ok
68 | 
69 |
70 | 71 | Upgrade the request to a websocket, will respond with 72 | bad request when something is wrong. 73 | 74 | -------------------------------------------------------------------------------- /doc/elli_websocket_handler.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_websocket_handler # 4 | * [Description](#description) 5 | 6 | Elli WebSocket Handler Behaviour. 7 | 8 | Copyright (c) 2013, Maas-Maarten Zeeman; 2018, elli-lib team 9 | 10 | __This module defines the `elli_websocket_handler` behaviour.__
Required callback functions: `websocket_init/2`, `websocket_handle/3`, `websocket_info/3`, `websocket_handle_event/3`. 11 | 12 | __Authors:__ Maas-Maarten Zeeman ([`mmzeeman@xs4all.nl`](mailto:mmzeeman@xs4all.nl)). 13 | 14 | -------------------------------------------------------------------------------- /doc/elli_ws_http.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_ws_http # 4 | * [Description](#description) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | Some simple parsing routines. 9 | 10 | 11 | 12 | ## Function Index ## 13 | 14 | 15 |
tokens/1Parse tokens.
16 | 17 | 18 | 19 | 20 | ## Function Details ## 21 | 22 | 23 | 24 | ### tokens/1 ### 25 | 26 |

27 | tokens(L::binary() | [binary() | [binary() | list()]]) -> [bitstring()]
28 | 
29 |
30 | 31 | Parse tokens 32 | 33 | -------------------------------------------------------------------------------- /doc/elli_ws_protocol.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_ws_protocol # 4 | * [Description](#description) 5 | * [Data Types](#types) 6 | * [Function Index](#index) 7 | * [Function Details](#functions) 8 | 9 | Websocket protocol implementation. 10 | 11 | 12 | 13 | ## Data Types ## 14 | 15 | 16 | 17 | 18 | ### close_code() ### 19 | 20 | 21 |

22 | close_code() = 1000..4999
23 | 
24 | 25 | 26 | 27 | 28 | ### frame() ### 29 | 30 | 31 |

32 | frame() = close | ping | pong | {text | binary | close | ping | pong, iodata()} | {close, close_code(), iodata()}
33 | 
34 | 35 | 36 | 37 | ## Function Index ## 38 | 39 | 40 |
upgrade/4Upgrade an HTTP request to the Websocket protocol.
41 | 42 | 43 | 44 | 45 | ## Function Details ## 46 | 47 | 48 | 49 | ### upgrade/4 ### 50 | 51 |

52 | upgrade(Req, Env, Handler::module(), HandlerOpts::any()) -> {ok, Req, Env} | {error, 400, Req} | {suspend, module(), atom(), [any()]}
53 | 
54 | 55 | 56 | 57 | Upgrade an HTTP request to the Websocket protocol. 58 | 59 | You do not need to call this function manually. To upgrade to the Websocket 60 | protocol, you simply need to return _{upgrade, protocol, elli_ws_protocol}_ 61 | in your _cowboy_http_handler:init/3_ handler function. 62 | 63 | __To do__
* Remove when we support only R16B+. 64 | 65 | -------------------------------------------------------------------------------- /doc/elli_ws_request_adapter.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module elli_ws_request_adapter # 4 | * [Description](#description) 5 | * [Data Types](#types) 6 | * [Function Index](#index) 7 | * [Function Details](#functions) 8 | 9 | Elli WebSocket Request Adapter. 10 | 11 | Copyright (c) 2013, Maas-Maarten Zeeman; 2018, elli-lib team 12 | 13 | __Authors:__ Maas-Maarten Zeeman ([`mmzeeman@xs4all.nl`](mailto:mmzeeman@xs4all.nl)). 14 | 15 | 16 | 17 | ## Data Types ## 18 | 19 | 20 | 21 | 22 | ### req() ### 23 | 24 | 25 |

 26 | req() = #req_adapter{req = elli:req(), resp_compress = boolean(), resp_headers = elli:headers(), sent_upgrade_reply = boolean(), websocket_version = undefined | integer(), websocket_compress = boolean()}
 27 | 
28 | 29 | 30 | 31 | ## Function Index ## 32 | 33 | 34 |
ensure_response/2Mimics cowboy_req:ensure_response/2.
get/2Mimics cowboy_req:get/2.
header/2Mimics cowboy_req:header/2.
init/2Initialize the request helper.
maybe_reply/2Mimics cowboy_req:maybe_reply/2.
messages/1Atoms used to identify messages in {active, once | true} mode.
parse_header/2Mimics cowboy_req:parse_header/3 {ok, ParsedHeaders, Req}.
set_meta/3Mimics cowboy_req:set_meta/3.
upgrade_reply/3
websocket_handler_callback/5Calls websocket_info en websocket_handle callbacks.
websocket_handler_handle_event/5Report an event...
websocket_handler_init/3Call the websocket_init callback of the websocket handler.
35 | 36 | 37 | 38 | 39 | ## Function Details ## 40 | 41 | 42 | 43 | ### ensure_response/2 ### 44 | 45 |

 46 | ensure_response(ReqAdapter::req(), X2::400) -> ok
 47 | 
48 |
49 | 50 | Mimics cowboy_req:ensure_response/2 51 | 52 | 53 | 54 | ### get/2 ### 55 | 56 |

 57 | get(L::atom() | list(), ReqAdapter::req()) -> any() | list()
 58 | 
59 |
60 | 61 | Mimics cowboy_req:get/2 62 | 63 | 64 | 65 | ### header/2 ### 66 | 67 | `header(X1, Req_adapter) -> any()` 68 | 69 | Mimics cowboy_req:header/2 70 | 71 | 72 | 73 | ### init/2 ### 74 | 75 |

 76 | init(Req::elli:req(), RespCompress::boolean()) -> req()
 77 | 
78 |
79 | 80 | Initialize the request helper 81 | 82 | 83 | 84 | ### maybe_reply/2 ### 85 | 86 |

 87 | maybe_reply(X1::400, ReqAdapter::req()) -> ok
 88 | 
89 |
90 | 91 | Mimics cowboy_req:maybe_reply/2 92 | 93 | 94 | 95 | ### messages/1 ### 96 | 97 |

 98 | messages(RA::req()) -> {tcp, tcp_closed, tcp_error} | {ssl, ssl_closed, ssl_error}
 99 | 
100 |
101 | 102 | Atoms used to identify messages in {active, once | true} mode. 103 | 104 | 105 | 106 | ### parse_header/2 ### 107 | 108 | `parse_header(X1, Req_adapter) -> any()` 109 | 110 | Mimics cowboy_req:parse_header/3 {ok, ParsedHeaders, Req} 111 | 112 | 113 | 114 | ### set_meta/3 ### 115 | 116 | `set_meta(X1, Version, ReqAdapter) -> any()` 117 | 118 | Mimics cowboy_req:set_meta/3 119 | 120 | 121 | 122 | ### upgrade_reply/3 ### 123 | 124 |

125 | upgrade_reply(X1::101, Headers::elli:headers(), Req_adapter::req()) -> {ok, req()}
126 | 
127 |
128 | 129 | 130 | 131 | ### websocket_handler_callback/5 ### 132 | 133 |

134 | websocket_handler_callback(Req, Handler, Callback, Message, HandlerState) -> Result
135 | 
136 | 137 | 138 | 139 | Calls websocket_info en websocket_handle callbacks. 140 | 141 | 142 | 143 | ### websocket_handler_handle_event/5 ### 144 | 145 |

146 | websocket_handler_handle_event(Req_adapter::req(), Handler::module(), Name::atom(), EventArgs::list(), HandlerOpts::any()) -> ok
147 | 
148 |
149 | 150 | Report an event... 151 | 152 | 153 | 154 | ### websocket_handler_init/3 ### 155 | 156 |

157 | websocket_handler_init(Req_adapter::req(), Handler::module(), HandlerState::any()) -> {shutdown, req()} | {ok, req(), any()} | {ok, req(), any(), hibernate} | {ok, req(), any(), Timeout::non_neg_integer()} | {ok, req(), any(), Timeout::non_neg_integer(), hibernate}
158 | 
159 |
160 | 161 | Call the websocket_init callback of the websocket handler. 162 | 163 | calls websocket_init(Req, HandlerOpts) -> 164 | {ok, Headers, HandlerState} 165 | We can upgrade, headers are added to the upgrade response. 166 | {ok, Headers, hibernate, HandlerState} 167 | We can upgrade, but this process will hibernate, headers 168 | are added to the upgrade response 169 | {ok, Headers, Timeout, HandlerState} 170 | We can upgrade, we will timout, headers are added to the upgrade respose. 171 | {ok, Headers, hibernate, Timeout, HandlerState} 172 | We can upgrade, set a timeout and hibernate. 173 | Headers are added to the response. 174 | {shutdown, Headers} 175 | We can't upgrade, a bad request response will be sent to the client. 176 | 177 | -------------------------------------------------------------------------------- /doc/erlang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elli-lib/elli_websocket/5153fffeedc31b33c1ab69f7004927d70b5aee29/doc/erlang.png -------------------------------------------------------------------------------- /doc/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* standard EDoc style sheet */ 2 | body { 3 | font-family: Verdana, Arial, Helvetica, sans-serif; 4 | margin-left: .25in; 5 | margin-right: .2in; 6 | margin-top: 0.2in; 7 | margin-bottom: 0.2in; 8 | color: #000000; 9 | background-color: #ffffff; 10 | } 11 | h1,h2 { 12 | margin-left: -0.2in; 13 | } 14 | div.navbar { 15 | background-color: #add8e6; 16 | padding: 0.2em; 17 | } 18 | h2.indextitle { 19 | padding: 0.4em; 20 | background-color: #add8e6; 21 | } 22 | h3.function,h3.typedecl { 23 | background-color: #add8e6; 24 | padding-left: 1em; 25 | } 26 | div.spec { 27 | margin-left: 2em; 28 | background-color: #eeeeee; 29 | } 30 | a.module { 31 | text-decoration:none 32 | } 33 | a.module:hover { 34 | background-color: #eeeeee; 35 | } 36 | ul.definitions { 37 | list-style-type: none; 38 | } 39 | ul.index { 40 | list-style-type: none; 41 | background-color: #eeeeee; 42 | } 43 | 44 | /* 45 | * Minor style tweaks 46 | */ 47 | ul { 48 | list-style-type: square; 49 | } 50 | table { 51 | border-collapse: collapse; 52 | } 53 | td { 54 | padding: 3 55 | } 56 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => [ 7 | "src" 8 | %% TODO: "test" 9 | ], 10 | filter => "*.erl", 11 | files => [ 12 | #{path => "src/elli_bstr.erl"}, 13 | #{path => "src/elli_example_websocket.erl"}, 14 | #{path => "src/elli_proplists.erl"}, 15 | #{path => "src/elli_websocket.erl"}, 16 | #{path => "src/elli_websocket_handler.erl"}, 17 | #{path => "src/elli_ws_http.erl"}, 18 | %% #{path => "./src/elli_ws_protocol.erl"} 19 | #{path => "src/elli_ws_request_adapter.erl"} 20 | ], 21 | rules => [ 22 | {elvis_style, line_length, 23 | #{limit => 100}}, 24 | {elvis_style, no_tabs}, 25 | {elvis_style, no_trailing_whitespace}, 26 | {elvis_style, no_if_expression}, 27 | {elvis_style, no_nested_try_catch}, 28 | {elvis_style, invalid_dynamic_call, 29 | #{ignore => [ 30 | elli_ws_request_adapter 31 | ]}}, 32 | {elvis_style, used_ignored_variable}, 33 | {elvis_style, no_behavior_info}, 34 | {elvis_style, state_record_and_type}, 35 | {elvis_style, no_spec_with_records}, 36 | {elvis_style, dont_repeat_yourself, 37 | #{min_complexity => 14}}, 38 | {elvis_style, no_debug_call} 39 | ], 40 | ruleset => erl_files 41 | }, 42 | #{dirs => ["."], 43 | filter => "Makefile", 44 | ruleset => makefiles 45 | }, 46 | #{dirs => ["."], 47 | filter => "rebar.config", 48 | ruleset => rebar_config 49 | }, 50 | #{dirs => ["."], 51 | filter => "elvis.config", 52 | ruleset => elvis_config 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ]. 59 | -------------------------------------------------------------------------------- /include/elli_websocket.hrl: -------------------------------------------------------------------------------- 1 | -ifndef(elli_websocket_hrl). 2 | -define(elli_websocket_hrl, 1). 3 | 4 | % -type payload() :: elli_websocket:payload(). 5 | % -type elli_websocket_message() :: elli_websocket:elli_websocket_message(). 6 | % -type elli_websocket_event() :: elli_websocket:elli_websocket_event(). 7 | 8 | -endif. -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | {platform_define, "^2[1-9]", post20} 4 | ]}. 5 | 6 | {xref_checks, [undefined_function_calls, locals_not_used]}. 7 | 8 | {dialyzer, [{base_plt_apps, [elli]}]}. 9 | 10 | {profiles, [ 11 | {test, [ 12 | {erl_first_files, [ 13 | "src/elli_websocket_handler" 14 | ]}, 15 | {deps, [{elli, "3.1.0"}]} 16 | ]}, 17 | {docs, [ 18 | {deps, [ 19 | {elli, "3.1.0"}, 20 | {edown, "0.8.1"} 21 | ]} 22 | ]} 23 | ]}. 24 | 25 | {edoc_opts, [ 26 | {application, elli_websocket}, 27 | {subpackages, false}, 28 | {doclet, edown_doclet}, 29 | {todo, true}, 30 | {doc_path, [ 31 | "http://raw.github.com/elli-lib/elli/develop/doc" 32 | ]}, 33 | {source_path, [ 34 | "src", 35 | "_build/test/lib/elli" 36 | ]} 37 | ]}. 38 | 39 | {project_plugins, [ 40 | {coveralls, "1.5.0"}, 41 | {rebar3_lint, "0.1.10"} 42 | ]}. 43 | 44 | {provider_hooks, [{pre, [{eunit, lint}]}]}. 45 | 46 | {cover_enabled, true}. 47 | {cover_export_enabled, true}. 48 | %% {cover_excl_mods, []}. 49 | {coveralls_coverdata, "_build/test/cover/eunit.coverdata"}. 50 | {coveralls_service_name, "travis-ci"}. 51 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case os:getenv("TRAVIS") of 2 | "true" -> 3 | JobId = os:getenv("TRAVIS_JOB_ID"), 4 | lists:keystore(coveralls_service_job_id, 1, CONFIG, 5 | {coveralls_service_job_id, JobId}); 6 | _ -> CONFIG 7 | end. 8 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/elli_bstr.erl: -------------------------------------------------------------------------------- 1 | %% @author Maas-Maarten Zeeman 2 | %% @copyright 2013, Maas-Maarten Zeeman; 2018, elli-lib team 3 | %% 4 | %% @doc Binary String Helper Functions 5 | %% @end 6 | %% 7 | %% Copyright 2013 Maas-Maarten Zeeman 8 | %% Copyright 2018 elli-lib team 9 | %% 10 | %% Licensed under the Apache License, Version 2.0 (the "License"); 11 | %% you may not use this file except in compliance with the License. 12 | %% You may obtain a copy of the License at 13 | %% 14 | %% http://www.apache.org/licenses/LICENSE-2.0 15 | %% 16 | %% Unless required by applicable law or agreed to in writing, software 17 | %% distributed under the License is distributed on an "AS IS" BASIS, 18 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | %% See the License for the specific language governing permissions and 20 | %% limitations under the License. 21 | 22 | -module(elli_bstr). 23 | 24 | -export([ 25 | to_lower/1, 26 | is_equal_ci/2, 27 | lchr/1, 28 | 29 | trim_left/1, 30 | trim_right/1, 31 | trim/1 32 | ]). 33 | 34 | 35 | -define(IS_WS(C), (C =:= $\s orelse C=:=$\t orelse C=:= $\r orelse C =:= $\n)). 36 | 37 | 38 | %% 39 | %% Types 40 | %% 41 | 42 | -type ascii_char() :: 0..127. 43 | 44 | 45 | %% 46 | %% Functions 47 | %% 48 | 49 | %% @doc Convert ascii Bin to lowercase 50 | -spec to_lower(Bin :: binary()) -> binary(). 51 | to_lower(Bin) -> 52 | << <<(lchr(C))>> || <> <= Bin >>. 53 | 54 | 55 | %% @doc Compare two binary values. 56 | %% Return true iff they are equal by a caseless compare. 57 | -spec is_equal_ci(binary(), binary()) -> boolean(). 58 | is_equal_ci(Bin, Bin) -> 59 | %% Quick match with an Erlang pattern match 60 | true; 61 | is_equal_ci(Bin1, Bin2) when is_binary(Bin1) andalso is_binary(Bin2) 62 | andalso size(Bin1) =:= size(Bin2) -> 63 | %% Both binaries are the same length, do a good check 64 | equal_ci(Bin1, Bin2); 65 | is_equal_ci(_, _) -> 66 | false. 67 | 68 | 69 | %% @doc convert character to lowercase. 70 | -spec lchr(ascii_char()) -> ascii_char(). 71 | lchr($A) -> $a; 72 | lchr($B) -> $b; 73 | lchr($C) -> $c; 74 | lchr($D) -> $d; 75 | lchr($E) -> $e; 76 | lchr($F) -> $f; 77 | lchr($G) -> $g; 78 | lchr($H) -> $h; 79 | lchr($I) -> $i; 80 | lchr($J) -> $j; 81 | lchr($K) -> $k; 82 | lchr($L) -> $l; 83 | lchr($M) -> $m; 84 | lchr($N) -> $n; 85 | lchr($O) -> $o; 86 | lchr($P) -> $p; 87 | lchr($Q) -> $q; 88 | lchr($R) -> $r; 89 | lchr($S) -> $s; 90 | lchr($T) -> $t; 91 | lchr($U) -> $u; 92 | lchr($V) -> $v; 93 | lchr($W) -> $w; 94 | lchr($X) -> $x; 95 | lchr($Y) -> $y; 96 | lchr($Z) -> $z; 97 | lchr(Chr) -> Chr. 98 | 99 | 100 | %% @doc Remove leading whitespace from Bin 101 | -spec trim_left(binary()) -> binary(). 102 | trim_left(<>) when ?IS_WS(C) -> 103 | trim_left(Rest); 104 | trim_left(Bin) -> 105 | Bin. 106 | 107 | 108 | %% @doc Remove trailing whitespace from Bin 109 | -spec trim_right(binary()) -> binary(). 110 | trim_right(<<>>) -> <<>>; 111 | trim_right(Bin) -> 112 | case binary:last(Bin) of 113 | C when ?IS_WS(C) -> 114 | trim_right(binary:part(Bin, {0, size(Bin)-1})); 115 | _ -> 116 | Bin 117 | end. 118 | 119 | 120 | %% @doc Remove leading and trailing whitespace. 121 | -spec trim(binary()) -> binary(). 122 | trim(Bin) -> 123 | trim_left(trim_right(Bin)). 124 | 125 | 126 | %% 127 | %% Helpers 128 | %% 129 | 130 | -spec equal_ci(binary(), binary()) -> boolean(). 131 | equal_ci(<<>>, <<>>) -> 132 | true; 133 | equal_ci(<>, <>) -> 134 | equal_ci(Rest1, Rest2); 135 | equal_ci(<>, <>) -> 136 | case lchr(C1) =:= lchr(C2) of 137 | true -> 138 | equal_ci(Rest1, Rest2); 139 | false -> 140 | false 141 | end. 142 | -------------------------------------------------------------------------------- /src/elli_example_websocket.erl: -------------------------------------------------------------------------------- 1 | -module(elli_example_websocket). 2 | -author("Maas-Maarten Zeeman "). 3 | 4 | -export([init/2, handle/2, handle_event/3]). 5 | 6 | %% Websocket callbacks. 7 | -export([ 8 | websocket_init/2, 9 | websocket_info/3, 10 | websocket_handle/3, 11 | 12 | websocket_handle_event/3 13 | ]). 14 | 15 | -include_lib("elli/include/elli.hrl"). 16 | 17 | %% Serves as elli and websocket handler in one module. 18 | 19 | -behaviour(elli_handler). 20 | -behaviour(elli_websocket_handler). 21 | 22 | %% 23 | %% Elli Handler Callbacks 24 | %% 25 | 26 | %% It would be nice if it was possbile to somehow pass the fact 27 | %% that this request is upgraded. 28 | %% 29 | init(Req, Args) -> 30 | _Method = case elli_request:get_header(<<"Upgrade">>, Req) of 31 | <<"websocket">> -> 32 | init_ws(elli_request:path(Req), Req, Args); 33 | _ -> 34 | ignore 35 | end. 36 | 37 | handle(Req, Args) -> 38 | Method = case elli_request:get_header(<<"Upgrade">>, Req) of 39 | <<"websocket">> -> 40 | websocket; 41 | _ -> 42 | elli_request:method(Req) 43 | end, 44 | handle(Method, elli_request:path(Req), Req, Args). 45 | 46 | 47 | %% 48 | %% Elli handler callbacks 49 | %% 50 | 51 | init_ws([<<"my">>, <<"websocket">>], _Req, _Args) -> 52 | {ok, handover}; 53 | init_ws(_, _, _) -> 54 | ignore. 55 | 56 | handle('websocket', [<<"my">>, <<"websocket">>], Req, Args) -> 57 | elli_websocket:upgrade(Req, Args), 58 | 59 | %% websocket is closed: 60 | %% See RFC-6455 (https://tools.ietf.org/html/rfc6455) for a list of 61 | %% valid WS status codes than can be used on a close frame. 62 | %% Note that the second element is the reason and is abitrary but should be meaningful 63 | %% in regards to your server and sub-protocol. 64 | {<<"1000">>, <<"Closed">>}; 65 | 66 | handle('GET', [<<"my">>, <<"websocket">>], _Req, _Args) -> 67 | {200, [], <<"Use an upgrade request">>}; 68 | 69 | handle(_, _, _, _) -> 70 | ignore. 71 | 72 | handle_event(Name, EventArgs, ElliArgs) -> 73 | io:fwrite(standard_error, "event: ~p ~p ~p~n", [Name, EventArgs, ElliArgs]), 74 | ok. 75 | 76 | %% 77 | %% Elli websocket handler callbacks 78 | %% 79 | 80 | 81 | %% @doc 82 | %% 83 | websocket_init(Req, Opts) -> 84 | io:fwrite(standard_error, "example_ws_init: ~p, ~p ~n", [Req, Opts]), 85 | State = undefined, 86 | {ok, [], State}. 87 | 88 | websocket_info(_Req, Message, State) -> 89 | io:fwrite(standard_error, "example_ws_info: ~p~n", [Message]), 90 | {ok, State}. 91 | 92 | websocket_handle(_Req, Message, State) -> 93 | io:fwrite(standard_error, "example_ws_handle: ~p~n", [Message]), 94 | %% default behaviour. 95 | {ok, State}. 96 | %% comment the line above ({ok, State}.) 97 | %% and uncomment the line below for an echo server. 98 | %% {reply, Message, State}. 99 | 100 | 101 | %% 102 | %% Elli Websocket Event Callbacks. 103 | %% 104 | 105 | %% websocket_open and websocket_close events are sent when the websocket 106 | %% opens, and when it closes. 107 | websocket_handle_event(websocket_open, [_, _Version, _Compress], _) -> ok; 108 | websocket_handle_event(websocket_close, [_, _Reason], _) -> ok; 109 | 110 | %% websocket_throw, websocket_error and websocket_exit events are sent if 111 | %% the user callback code throws an exception, has an error or 112 | %% exits. After triggering this event, a generated response is sent to 113 | %% the user. 114 | websocket_handle_event(websocket_throw, [_Request, _Exception, _Stacktrace], _) -> ok; 115 | websocket_handle_event(websocket_error, [_Request, _Exception, _Stacktrace], _) -> ok; 116 | websocket_handle_event(websocket_exit, [_Request, _Exception, _Stacktrace], _) -> ok. 117 | -------------------------------------------------------------------------------- /src/elli_proplists.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Case insensitive value retrieval from a proplist 3 | %% 4 | 5 | -module(elli_proplists). 6 | 7 | -export([get_value_ci/2, 8 | get_all_values_ci/2]). 9 | 10 | %% 11 | get_value_ci(_Key, []) -> 12 | undefined; 13 | get_value_ci(Key, [{K, Value}|Rest]) -> 14 | case elli_bstr:is_equal_ci(Key, K) of 15 | true -> 16 | Value; 17 | false -> 18 | get_value_ci(Key, Rest) 19 | end. 20 | 21 | 22 | %% 23 | get_all_values_ci(Key, Proplist) -> 24 | get_all_values_ci1(Key, Proplist, []). 25 | 26 | 27 | get_all_values_ci1(_Key, [], Acc) -> 28 | lists:reverse(Acc); 29 | get_all_values_ci1(Key, [{K, Value}|Rest], Acc) -> 30 | case elli_bstr:is_equal_ci(Key, K) of 31 | true -> 32 | get_all_values_ci1(Key, Rest, [Value|Acc]); 33 | false -> 34 | get_all_values_ci1(Key, Rest, Acc) 35 | end. 36 | -------------------------------------------------------------------------------- /src/elli_websocket.app.src: -------------------------------------------------------------------------------- 1 | {application, elli_websocket, [ 2 | {description, "Elli WebSocket Handler."}, 3 | {vsn, "0.1.1"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib, elli]}, 7 | {env, []}, 8 | 9 | {maintainers, ["elli-lib team"]}, 10 | {licenses, ["Apache 2.0"]}, 11 | {links, [ 12 | {"GitHub", "https://github.com/elli-lib/elli_websocket"} 13 | ]} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/elli_websocket.erl: -------------------------------------------------------------------------------- 1 | %% @author Maas-Maarten Zeeman 2 | %% @copyright 2012-2013, Maas-Maarten Zeeman; 2018, elli-lib team 3 | %% 4 | %% @doc Elli Websocket Handler 5 | %% @end 6 | %% 7 | %% Copyright 2012-2013, Maas-Maarten Zeeman 8 | %% Copyright 2018, elli-lib team 9 | %% 10 | %% Licensed under the Apache License, Version 2.0 (the "License"); 11 | %% you may not use this file except in compliance with the License. 12 | %% You may obtain a copy of the License at 13 | %% 14 | %% http://www.apache.org/licenses/LICENSE-2.0 15 | %% 16 | %% Unless required by applicable law or agreed to in writing, software 17 | %% distributed under the License is distributed on an "AS IS" BASIS, 18 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | %% See the License for the specific language governing permissions and 20 | %% limitations under the License. 21 | 22 | -module(elli_websocket). 23 | -author("Maas-Maarten Zeeman "). 24 | 25 | -include_lib("elli/include/elli.hrl"). 26 | 27 | %% 28 | %% Api 29 | %% 30 | 31 | -export([upgrade/2]). 32 | 33 | %% 34 | %% Public types 35 | %% 36 | 37 | -export_type([payload/0, message/0, event/0]). 38 | 39 | -type payload() :: binary() | iolist(). 40 | 41 | -type message() :: {text, payload()} | 42 | {binary, payload()} | 43 | {ping, payload()} | 44 | {pong, payload()}. 45 | 46 | -type event() :: 47 | websocket_open | websocket_close | 48 | websocket_throw | websocket_error | websocket_exit. 49 | 50 | 51 | %% Args: proplist with settings. 52 | %% handler: Websocket callback module 53 | %% handler_opts: Options to pass to the websocket_init 54 | %% callback. 55 | %% resp_compress: bool(), when set to true the traffic 56 | %% will be compressed if the client supports it. 57 | %% 58 | 59 | %% @doc Upgrade the request to a websocket, will respond with 60 | %% bad request when something is wrong. 61 | -spec upgrade(Req :: elli:req(), list()) -> ok. 62 | upgrade(Req, Args) -> 63 | RespCompress = proplists:get_value(resp_compress, Args, false), 64 | ReqAdapter = elli_ws_request_adapter:init(Req, RespCompress), 65 | {handler, Handler} = proplists:lookup(handler, Args), 66 | HandlerOpts = proplists:get_value(handler_opts, Args, []), 67 | 68 | %% Adapter is ready, hand over to ws_protocol 69 | _UpgradeResponse = elli_ws_protocol:upgrade(ReqAdapter, Args, Handler, HandlerOpts), 70 | ok. 71 | -------------------------------------------------------------------------------- /src/elli_websocket_handler.erl: -------------------------------------------------------------------------------- 1 | %% @author Maas-Maarten Zeeman 2 | %% @copyright 2013, Maas-Maarten Zeeman; 2018, elli-lib team 3 | %% 4 | %% @doc Elli WebSocket Handler Behaviour 5 | %% @end 6 | %% 7 | %% Copyright 2013 Maas-Maarten Zeeman 8 | %% Copyright 2018 elli-lib team 9 | %% 10 | %% Licensed under the Apache License, Version 2.0 (the "License"); 11 | %% you may not use this file except in compliance with the License. 12 | %% You may obtain a copy of the License at 13 | %% 14 | %% http://www.apache.org/licenses/LICENSE-2.0 15 | %% 16 | %% Unless required by applicable law or agreed to in writing, software 17 | %% distributed under the License is distributed on an "AS IS" BASIS, 18 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | %% See the License for the specific language governing permissions and 20 | %% limitations under the License. 21 | 22 | -module(elli_websocket_handler). 23 | 24 | -include_lib("elli_websocket.hrl"). 25 | 26 | 27 | -callback websocket_init(Req :: elli:req(), Args :: any()) -> 28 | {ok, Headers, State} | 29 | {ok, Headers, hibernate, State} | 30 | {ok, Headers, Timeout, State} | 31 | {ok, Headers, hibernate, Timeout, State} | 32 | {shutdown, Headers} when 33 | Headers :: elli:headers(), 34 | State :: any(), 35 | Timeout :: non_neg_integer(). 36 | 37 | 38 | -callback websocket_handle(Req, Message, State) -> 39 | {reply, ReplyMessage, ReplyState} | {ok, State} when 40 | Req :: elli:req(), 41 | Message :: elli_websocket:message(), 42 | State :: any(), 43 | ReplyState :: any(), 44 | ReplyMessage :: elli_websocket:message(). 45 | 46 | 47 | -callback websocket_info(Req, Message, State) -> {ok, ReplyState} when 48 | Req :: elli:req(), 49 | Message :: any(), 50 | State :: any(), 51 | ReplyState :: any(). 52 | 53 | 54 | -callback websocket_handle_event(Event, Args, State) -> ok when 55 | Event :: elli_websocket:event(), 56 | Args :: list(), 57 | State :: any(). 58 | -------------------------------------------------------------------------------- /src/elli_ws_http.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Some simple parsing routines. 2 | 3 | -module(elli_ws_http). 4 | 5 | -export([tokens/1]). 6 | 7 | 8 | -define(IS_TOKEN_SEP(C), (C =:= $, orelse C =:= $\s orelse C=:= $\t)). 9 | 10 | 11 | %% @doc Parse tokens 12 | -spec tokens(binary() | [binary() | [binary() | list()]]) -> [bitstring()]. 13 | tokens(L) when is_list(L) -> 14 | lists:flatten([tokens(V) || V <- L]); 15 | tokens(Header) when is_binary(Header) -> 16 | parse_before(Header, []). 17 | 18 | 19 | parse_before(<<>>, Acc) -> 20 | lists:reverse(Acc); 21 | parse_before(<< C, Rest/bits >>, Acc) when ?IS_TOKEN_SEP(C) -> 22 | parse_before(Rest, Acc); 23 | parse_before(Buffer, Acc) -> 24 | parse(Buffer, Acc, <<>>). 25 | 26 | 27 | parse(<<>>, Acc, <<>>) -> 28 | lists:reverse(Acc); 29 | parse(<<>>, Acc, Token) -> 30 | lists:reverse([Token|Acc]); 31 | parse(<>, Acc, Token) when ?IS_TOKEN_SEP(C) -> 32 | parse_before(Rest, [Token|Acc]); 33 | parse(<>, Acc, Token) -> 34 | parse(Rest, Acc, <>). 35 | -------------------------------------------------------------------------------- /src/elli_ws_protocol.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2011-2013, Loïc Hoguin 2 | %% Copyright (c) 2013, Maas-Maarten Zeeman 3 | %% Copyright (c) 2018, elli-lib team 4 | %% 5 | %% Permission to use, copy, modify, and/or distribute this software for any 6 | %% purpose with or without fee is hereby granted, provided that the above 7 | %% copyright notice and this permission notice appear in all copies. 8 | %% 9 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | %% @doc Websocket protocol implementation. 18 | %% @end 19 | %% 20 | %% Cowboy supports versions 7 through 17 of the Websocket drafts. 21 | %% It also supports RFC6455, the proposed standard for Websocket. 22 | 23 | %% 24 | %% Adapted to serve as websocket handler for elli 25 | %% 26 | 27 | %% Changes: 28 | %% * Removed ranch remove connection call. 29 | %% * Removed configurable transport => elli_tcp only 30 | %% * Uses elli_ws_request_adapter. 31 | %% * in handler_loop change socket=Socket to {_,Port} and change all receive clauses. 32 | %% * same for websocket_payload_loop. 33 | %% * Uppercased response headers. 34 | %% * Improved error reporting, all handler exceptions are reported to the handler. 35 | %% * Improved status reporting, events are fired when the websocket is open and when 36 | %% it closes. 37 | %% * Checked types with dialyzer. 38 | 39 | -module(elli_ws_protocol). 40 | 41 | %% Ignore the deprecation warning for crypto:sha/1. 42 | %% @todo Remove when we support only R16B+. 43 | -compile(nowarn_deprecated_function). 44 | 45 | %% API. 46 | -export([upgrade/4]). 47 | 48 | %% Internal. 49 | -export([handler_loop/4]). 50 | 51 | -type close_code() :: 1000..4999. 52 | -export_type([close_code/0]). 53 | 54 | -type frame() :: close | ping | pong 55 | | {text | binary | close | ping | pong, iodata()} 56 | | {close, close_code(), iodata()}. 57 | -export_type([frame/0]). 58 | 59 | -type opcode() :: 0 | 1 | 2 | 8 | 9 | 10. 60 | -type mask_key() :: 0..16#ffffffff. 61 | -type frag_state() :: undefined 62 | | {nofin, opcode(), binary()} | {fin, opcode(), binary()}. 63 | -type rsv() :: << _:3 >>. 64 | 65 | -record(state, { 66 | env :: [], %% cowboy_middleware:env(), 67 | socket = undefined :: undefined | elli_tcp:socket(), 68 | handler :: module(), 69 | key = undefined :: undefined | binary(), 70 | timeout = infinity :: timeout(), 71 | timeout_ref = undefined :: undefined | reference(), 72 | messages = undefined :: undefined | {atom(), atom(), atom()}, 73 | hibernate = false :: boolean(), 74 | frag_state = undefined :: frag_state(), 75 | utf8_state = <<>> :: binary(), 76 | deflate_frame = false :: boolean(), 77 | inflate_state :: undefined | port(), 78 | deflate_state :: undefined | port() 79 | }). 80 | 81 | %% @doc Upgrade an HTTP request to the Websocket protocol. 82 | %% 83 | %% You do not need to call this function manually. To upgrade to the Websocket 84 | %% protocol, you simply need to return {upgrade, protocol, {@module}} 85 | %% in your cowboy_http_handler:init/3 handler function. 86 | -spec upgrade(Req, Env, Handler :: module(), HandlerOpts :: any()) -> 87 | {ok, Req, Env} | {error, 400, Req} | {suspend, module(), atom(), [any()]} 88 | when Req::elli_ws_request_adapter:req(), Env::list(). 89 | upgrade(Req, Env, Handler, HandlerOpts) -> 90 | Socket = elli_ws_request_adapter:get(socket, Req), 91 | State = #state{env=Env, socket=Socket, handler=Handler}, 92 | try websocket_upgrade(State, Req) of 93 | {ok, State2, Req2} -> handler_init(State2, Req2, HandlerOpts) 94 | catch 95 | throw:Exc -> 96 | handle_event(Req, Handler, websocket_throw, [Exc, erlang:get_stacktrace()], HandlerOpts), 97 | elli_ws_request_adapter:maybe_reply(400, Req); 98 | error:Error -> 99 | handle_event(Req, Handler, websocket_error, [Error, erlang:get_stacktrace()], HandlerOpts), 100 | elli_ws_request_adapter:maybe_reply(400, Req); 101 | exit:Exit -> 102 | handle_event(Req, Handler, websocket_exit, [Exit, erlang:get_stacktrace()], HandlerOpts), 103 | elli_ws_request_adapter:maybe_reply(400, Req) 104 | end. 105 | 106 | %% -spec websocket_upgrade(#state{}, Req) 107 | %% -> {ok, #state{}, Req} when Req::elli_ws_request_adapter:req(). 108 | websocket_upgrade(State, Req) -> 109 | {ok, ConnTokens, Req2} = elli_ws_request_adapter:parse_header(<<"connection">>, Req), 110 | true = lists:member(<<"upgrade">>, ConnTokens), 111 | 112 | %% @todo Should probably send a 426 if the Upgrade header is missing. 113 | {ok, [<<"websocket">>], Req3} = elli_ws_request_adapter:parse_header(<<"upgrade">>, Req2), 114 | {Version, Req4} = elli_ws_request_adapter:header(<<"sec-websocket-version">>, Req3), 115 | IntVersion = list_to_integer(binary_to_list(Version)), 116 | true = (IntVersion =:= 7) orelse (IntVersion =:= 8) orelse (IntVersion =:= 13), 117 | {Key, Req5} = elli_ws_request_adapter:header(<<"sec-websocket-key">>, Req4), 118 | false = Key =:= undefined, 119 | websocket_extensions(State#state{key=Key}, elli_ws_request_adapter:set_meta(websocket_version, IntVersion, Req5)). 120 | 121 | %% -spec websocket_extensions(#state{}, Req) 122 | %% -> {ok, #state{}, Req} when Req::elli_ws_request_adapter:req(). 123 | websocket_extensions(State, Req) -> 124 | case elli_ws_request_adapter:parse_header(<<"sec-websocket-extensions">>, Req) of 125 | {ok, Extensions, Req2} when Extensions =/= undefined -> 126 | [Compress] = elli_ws_request_adapter:get([resp_compress], Req), 127 | case lists:keyfind(<<"x-webkit-deflate-frame">>, 1, Extensions) of 128 | {<<"x-webkit-deflate-frame">>, []} when Compress =:= true -> 129 | Inflate = zlib:open(), 130 | Deflate = zlib:open(), 131 | %% Since we are negotiating an unconstrained deflate-frame 132 | %% then we must be willing to accept frames using the 133 | %% maximum window size which is 2^15. The negative value 134 | %% indicates that zlib headers are not used. 135 | ok = zlib:inflateInit(Inflate, -15), 136 | %% Initialize the deflater with a window size of 2^15 bits and disable 137 | %% the zlib headers. 138 | ok = zlib:deflateInit(Deflate, best_compression, deflated, -15, 8, default), 139 | {ok, State#state{ 140 | deflate_frame = true, 141 | inflate_state = Inflate, 142 | deflate_state = Deflate 143 | }, elli_ws_request_adapter:set_meta(websocket_compress, true, Req2)}; 144 | _ -> 145 | {ok, State, elli_ws_request_adapter:set_meta(websocket_compress, false, Req2)} 146 | end; 147 | _ -> 148 | {ok, State, elli_ws_request_adapter:set_meta(websocket_compress, false, Req)} 149 | end. 150 | 151 | %% -spec handler_init(#state{}, Req, any()) 152 | %% -> {ok, Req, cowboy_middleware:env()} | {error, 400, Req} 153 | %% | {suspend, module(), atom(), [any()]} 154 | %% when Req::elli_ws_request_adapter:req(). 155 | handler_init(State=#state{env=Env, handler=Handler}, Req, HandlerOpts) -> 156 | try elli_ws_request_adapter:websocket_handler_init(Req, Handler, HandlerOpts) of 157 | {ok, Req2, HandlerState} -> 158 | websocket_handshake(State, Req2, HandlerState); 159 | {ok, Req2, HandlerState, hibernate} -> 160 | websocket_handshake(State#state{hibernate=true}, 161 | Req2, HandlerState); 162 | {ok, Req2, HandlerState, Timeout} -> 163 | websocket_handshake(State#state{timeout=Timeout}, 164 | Req2, HandlerState); 165 | {ok, Req2, HandlerState, Timeout, hibernate} -> 166 | websocket_handshake(State#state{timeout=Timeout, 167 | hibernate=true}, Req2, HandlerState); 168 | {shutdown, Req2} -> 169 | elli_ws_request_adapter:ensure_response(Req2, 400), 170 | {ok, Req2, [{result, closed}|Env]} 171 | catch 172 | throw:Exc -> 173 | handle_event(Req, Handler, websocket_throw, [Exc, erlang:get_stacktrace()], HandlerOpts), 174 | elli_ws_request_adapter:maybe_reply(400, Req); 175 | error:Error -> 176 | handle_event(Req, Handler, websocket_error, [Error, erlang:get_stacktrace()], HandlerOpts), 177 | elli_ws_request_adapter:maybe_reply(400, Req); 178 | exit:Exit -> 179 | handle_event(Req, Handler, websocket_exit, [Exit, erlang:get_stacktrace()], HandlerOpts), 180 | elli_ws_request_adapter:maybe_reply(400, Req) 181 | end. 182 | 183 | %% -spec websocket_handshake(#state{}, Req, any()) 184 | %% -> {ok, Req, cowboy_middleware:env()} 185 | %% | {suspend, module(), atom(), [any()]} 186 | %% when Req::elli_ws_request_adapter:req(). 187 | websocket_handshake(State=#state{key=Key, deflate_frame=DeflateFrame, handler=Handler}, Req, HandlerState) -> 188 | Challenge = base64:encode(crypto:hash(sha, 189 | << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)), 190 | Extensions = case DeflateFrame of 191 | false -> []; 192 | true -> [{<<"Sec-WebSocket-Extensions">>, <<"x-webkit-deflate-frame">>}] 193 | end, 194 | {ok, Req2} = elli_ws_request_adapter:upgrade_reply( 195 | 101, 196 | [{<<"Upgrade">>, <<"websocket">>}, 197 | {<<"Sec-WebSocket-Accept">>, Challenge}| 198 | Extensions], 199 | Req), 200 | 201 | %% Upgrade reply is sent, report that the websocket is open. 202 | handle_event(Req, Handler, websocket_open, 203 | [elli_ws_request_adapter:get(websocket_version, Req), DeflateFrame], HandlerState), 204 | 205 | %% Flush the resp_sent message before moving on. 206 | %% receive {elli_ws_request_adapter, resp_sent} -> ok after 0 -> ok end, 207 | State2 = handler_loop_timeout(State), 208 | handler_before_loop(State2#state{key=undefined, messages=elli_ws_request_adapter:messages(Req)}, 209 | Req2, HandlerState, <<>>). 210 | 211 | %% -spec handler_before_loop(#state{}, Req, any(), binary()) 212 | %% -> {ok, Req, cowboy_middleware:env()} 213 | %% | {suspend, module(), atom(), [any()]} 214 | %% when Req::elli_ws_request_adapter:req(). 215 | handler_before_loop(State=#state{socket=Socket, hibernate=true}, Req, HandlerState, SoFar) -> 216 | ok = elli_tcp:setopts(Socket, [{active, once}]), 217 | {suspend, ?MODULE, handler_loop, 218 | [State#state{hibernate=false}, Req, HandlerState, SoFar]}; 219 | handler_before_loop(State=#state{socket=Socket}, 220 | Req, HandlerState, SoFar) -> 221 | ok = elli_tcp:setopts(Socket, [{active, once}]), 222 | handler_loop(State, Req, HandlerState, SoFar). 223 | 224 | %% -spec handler_loop_timeout(#state{}) -> #state{}. 225 | handler_loop_timeout(State=#state{timeout=infinity}) -> 226 | State#state{timeout_ref=undefined}; 227 | handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) -> 228 | _ = case PrevRef of undefined -> ignore; PrevRef -> 229 | erlang:cancel_timer(PrevRef) end, 230 | TRef = erlang:start_timer(Timeout, self(), ?MODULE), 231 | State#state{timeout_ref=TRef}. 232 | 233 | %% @private 234 | %% -spec handler_loop(#state{}, Req, any(), binary()) 235 | %% -> {ok, Req, cowboy_middleware:env()} 236 | %% | {suspend, module(), atom(), [any()]} 237 | %% when Req::elli_ws_request_adapter:req(). 238 | handler_loop(State=#state{socket={_, Port}, messages={OK, Closed, Error}, 239 | timeout_ref=TRef}, Req, HandlerState, SoFar) -> 240 | receive 241 | {OK, Port, Data} -> 242 | State2 = handler_loop_timeout(State), 243 | websocket_data(State2, Req, HandlerState, 244 | << SoFar/binary, Data/binary >>); 245 | {Closed, Port} -> 246 | handler_terminate(State, Req, HandlerState, {error, closed}); 247 | {Error, Port, Reason} -> 248 | handler_terminate(State, Req, HandlerState, {error, Reason}); 249 | {timeout, TRef, ?MODULE} -> 250 | websocket_close(State, Req, HandlerState, {normal, timeout}); 251 | {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> 252 | handler_loop(State, Req, HandlerState, SoFar); 253 | Message -> 254 | handler_call(State, Req, HandlerState, 255 | SoFar, websocket_info, Message, fun handler_before_loop/4) 256 | end. 257 | 258 | %% All frames passing through this function are considered valid, 259 | %% with the only exception of text and close frames with a payload 260 | %% which may still contain errors. 261 | %% -spec websocket_data(#state{}, Req, any(), binary()) 262 | %% -> {ok, Req, cowboy_middleware:env()} 263 | %% | {suspend, module(), atom(), [any()]} 264 | %% when Req::elli_ws_request_adapter:req(). 265 | %% RSV bits MUST be 0 unless an extension is negotiated 266 | %% that defines meanings for non-zero values. 267 | websocket_data(State, Req, HandlerState, << _:1, Rsv:3, _/bits >>) 268 | when Rsv =/= 0, State#state.deflate_frame =:= false -> 269 | websocket_close(State, Req, HandlerState, {error, badframe}); 270 | %% Invalid opcode. Note that these opcodes may be used by extensions. 271 | websocket_data(State, Req, HandlerState, << _:4, Opcode:4, _/bits >>) 272 | when Opcode > 2, Opcode =/= 8, Opcode =/= 9, Opcode =/= 10 -> 273 | websocket_close(State, Req, HandlerState, {error, badframe}); 274 | %% Control frames MUST NOT be fragmented. 275 | websocket_data(State, Req, HandlerState, << 0:1, _:3, Opcode:4, _/bits >>) 276 | when Opcode >= 8 -> 277 | websocket_close(State, Req, HandlerState, {error, badframe}); 278 | %% A frame MUST NOT use the zero opcode unless fragmentation was initiated. 279 | websocket_data(State=#state{frag_state=undefined}, Req, HandlerState, 280 | << _:4, 0:4, _/bits >>) -> 281 | websocket_close(State, Req, HandlerState, {error, badframe}); 282 | %% Non-control opcode when expecting control message or next fragment. 283 | websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, 284 | << _:4, Opcode:4, _/bits >>) 285 | when Opcode =/= 0, Opcode < 8 -> 286 | websocket_close(State, Req, HandlerState, {error, badframe}); 287 | %% Close control frame length MUST be 0 or >= 2. 288 | websocket_data(State, Req, HandlerState, << _:4, 8:4, _:1, 1:7, _/bits >>) -> 289 | websocket_close(State, Req, HandlerState, {error, badframe}); 290 | %% Close control frame with incomplete close code. Need more data. 291 | websocket_data(State, Req, HandlerState, 292 | Data = << _:4, 8:4, 1:1, Len:7, _/bits >>) 293 | when Len > 1, byte_size(Data) < 8 -> 294 | handler_before_loop(State, Req, HandlerState, Data); 295 | %% 7 bits payload length. 296 | websocket_data(State, Req, HandlerState, << Fin:1, Rsv:3/bits, Opcode:4, 1:1, 297 | Len:7, MaskKey:32, Rest/bits >>) 298 | when Len < 126 -> 299 | websocket_data(State, Req, HandlerState, 300 | Opcode, Len, MaskKey, Rest, Rsv, Fin); 301 | %% 16 bits payload length. 302 | websocket_data(State, Req, HandlerState, << Fin:1, Rsv:3/bits, Opcode:4, 1:1, 303 | 126:7, Len:16, MaskKey:32, Rest/bits >>) 304 | when Len > 125, Opcode < 8 -> 305 | websocket_data(State, Req, HandlerState, 306 | Opcode, Len, MaskKey, Rest, Rsv, Fin); 307 | %% 63 bits payload length. 308 | websocket_data(State, Req, HandlerState, << Fin:1, Rsv:3/bits, Opcode:4, 1:1, 309 | 127:7, 0:1, Len:63, MaskKey:32, Rest/bits >>) 310 | when Len > 16#ffff, Opcode < 8 -> 311 | websocket_data(State, Req, HandlerState, 312 | Opcode, Len, MaskKey, Rest, Rsv, Fin); 313 | %% When payload length is over 63 bits, the most significant bit MUST be 0. 314 | websocket_data(State, Req, HandlerState, << _:8, 1:1, 127:7, 1:1, _:7, _/binary >>) -> 315 | websocket_close(State, Req, HandlerState, {error, badframe}); 316 | %% All frames sent from the client to the server are masked. 317 | websocket_data(State, Req, HandlerState, << _:8, 0:1, _/bits >>) -> 318 | websocket_close(State, Req, HandlerState, {error, badframe}); 319 | %% For the next two clauses, it can be one of the following: 320 | %% 321 | %% * The minimal number of bytes MUST be used to encode the length 322 | %% * All control frames MUST have a payload length of 125 bytes or less 323 | websocket_data(State, Req, HandlerState, << _:9, 126:7, _:48, _/bits >>) -> 324 | websocket_close(State, Req, HandlerState, {error, badframe}); 325 | websocket_data(State, Req, HandlerState, << _:9, 127:7, _:96, _/bits >>) -> 326 | websocket_close(State, Req, HandlerState, {error, badframe}); 327 | %% Need more data. 328 | websocket_data(State, Req, HandlerState, Data) -> 329 | handler_before_loop(State, Req, HandlerState, Data). 330 | 331 | %% Initialize or update fragmentation state. 332 | %% The opcode is only included in the first frame fragment. 333 | -spec websocket_data(#state{}, Req, any(), 334 | opcode(), non_neg_integer(), mask_key(), binary(), rsv(), 0 | 1) 335 | -> {ok, Req, cowboy_middleware:env()} 336 | | {suspend, module(), atom(), [any()]} 337 | when Req::elli_ws_request_adapter:req(). 338 | websocket_data(State=#state{frag_state=undefined}, Req, HandlerState, 339 | Opcode, Len, MaskKey, Data, Rsv, 0) -> 340 | websocket_payload(State#state{frag_state={nofin, Opcode, <<>>}}, 341 | Req, HandlerState, 0, Len, MaskKey, <<>>, 0, Data, Rsv); 342 | %% Subsequent frame fragments. 343 | websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, 344 | 0, Len, MaskKey, Data, Rsv, 0) -> 345 | websocket_payload(State, Req, HandlerState, 346 | 0, Len, MaskKey, <<>>, 0, Data, Rsv); 347 | %% Final frame fragment. 348 | websocket_data(State=#state{frag_state={nofin, Opcode, SoFar}}, 349 | Req, HandlerState, 0, Len, MaskKey, Data, Rsv, 1) -> 350 | websocket_payload(State#state{frag_state={fin, Opcode, SoFar}}, 351 | Req, HandlerState, 0, Len, MaskKey, <<>>, 0, Data, Rsv); 352 | %% Unfragmented frame. 353 | websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, Rsv, 1) -> 354 | websocket_payload(State, Req, HandlerState, 355 | Opcode, Len, MaskKey, <<>>, 0, Data, Rsv). 356 | 357 | %% -spec websocket_payload(#state{}, Req, any(), 358 | %% opcode(), non_neg_integer(), mask_key(), binary(), non_neg_integer(), 359 | %% binary(), rsv()) 360 | %% -> {ok, Req, cowboy_middleware:env()} 361 | %% | {suspend, module(), atom(), [any()]} 362 | %% when Req::elli_ws_request_adapter:req(). 363 | %% Close control frames with a payload MUST contain a valid close code. 364 | websocket_payload(State, Req, HandlerState, 365 | Opcode=8, Len, MaskKey, <<>>, 0, 366 | << MaskedCode:2/binary, Rest/bits >>, Rsv) -> 367 | Unmasked = << Code:16 >> = websocket_unmask(MaskedCode, MaskKey, <<>>), 368 | if Code < 1000; Code =:= 1004; Code =:= 1005; Code =:= 1006; 369 | (Code > 1011) and (Code < 3000); Code > 4999 -> 370 | websocket_close(State, Req, HandlerState, {error, badframe}); 371 | true -> 372 | websocket_payload(State, Req, HandlerState, 373 | Opcode, Len - 2, MaskKey, Unmasked, byte_size(MaskedCode), 374 | Rest, Rsv) 375 | end; 376 | %% Text frames and close control frames MUST have a payload that is valid UTF-8. 377 | websocket_payload(State=#state{utf8_state=Incomplete}, 378 | Req, HandlerState, Opcode, Len, MaskKey, Unmasked, UnmaskedLen, 379 | Data, Rsv) 380 | when (byte_size(Data) < Len) andalso ((Opcode =:= 1) orelse 381 | ((Opcode =:= 8) andalso (Unmasked =/= <<>>))) -> 382 | Unmasked2 = websocket_unmask(Data, 383 | rotate_mask_key(MaskKey, UnmaskedLen), <<>>), 384 | {Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, false, State), 385 | case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of 386 | false -> 387 | websocket_close(State2, Req, HandlerState, {error, badencoding}); 388 | Utf8State -> 389 | websocket_payload_loop(State2#state{utf8_state=Utf8State}, 390 | Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey, 391 | << Unmasked/binary, Unmasked3/binary >>, 392 | UnmaskedLen + byte_size(Data), Rsv) 393 | end; 394 | websocket_payload(State=#state{utf8_state=Incomplete}, 395 | Req, HandlerState, Opcode, Len, MaskKey, Unmasked, UnmaskedLen, 396 | Data, Rsv) 397 | when Opcode =:= 1; (Opcode =:= 8) and (Unmasked =/= <<>>) -> 398 | << End:Len/binary, Rest/bits >> = Data, 399 | Unmasked2 = websocket_unmask(End, 400 | rotate_mask_key(MaskKey, UnmaskedLen), <<>>), 401 | {Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, true, State), 402 | case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of 403 | <<>> -> 404 | websocket_dispatch(State2#state{utf8_state= <<>>}, 405 | Req, HandlerState, Rest, Opcode, 406 | << Unmasked/binary, Unmasked3/binary >>); 407 | _ -> 408 | websocket_close(State2, Req, HandlerState, {error, badencoding}) 409 | end; 410 | %% Fragmented text frames may cut payload in the middle of UTF-8 codepoints. 411 | websocket_payload(State=#state{frag_state={_, 1, _}, utf8_state=Incomplete}, 412 | Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, UnmaskedLen, 413 | Data, Rsv) 414 | when byte_size(Data) < Len -> 415 | Unmasked2 = websocket_unmask(Data, 416 | rotate_mask_key(MaskKey, UnmaskedLen), <<>>), 417 | {Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, false, State), 418 | case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of 419 | false -> 420 | websocket_close(State2, Req, HandlerState, {error, badencoding}); 421 | Utf8State -> 422 | websocket_payload_loop(State2#state{utf8_state=Utf8State}, 423 | Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey, 424 | << Unmasked/binary, Unmasked3/binary >>, 425 | UnmaskedLen + byte_size(Data), Rsv) 426 | end; 427 | websocket_payload(State=#state{frag_state={Fin, 1, _}, utf8_state=Incomplete}, 428 | Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, UnmaskedLen, 429 | Data, Rsv) -> 430 | << End:Len/binary, Rest/bits >> = Data, 431 | Unmasked2 = websocket_unmask(End, 432 | rotate_mask_key(MaskKey, UnmaskedLen), <<>>), 433 | {Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, Fin =:= fin, State), 434 | case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of 435 | <<>> -> 436 | websocket_dispatch(State2#state{utf8_state= <<>>}, 437 | Req, HandlerState, Rest, Opcode, 438 | << Unmasked/binary, Unmasked3/binary >>); 439 | Utf8State when is_binary(Utf8State), Fin =:= nofin -> 440 | websocket_dispatch(State2#state{utf8_state=Utf8State}, 441 | Req, HandlerState, Rest, Opcode, 442 | << Unmasked/binary, Unmasked3/binary >>); 443 | _ -> 444 | websocket_close(State, Req, HandlerState, {error, badencoding}) 445 | end; 446 | %% Other frames have a binary payload. 447 | websocket_payload(State, Req, HandlerState, 448 | Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Data, Rsv) 449 | when byte_size(Data) < Len -> 450 | Unmasked2 = websocket_unmask(Data, 451 | rotate_mask_key(MaskKey, UnmaskedLen), <<>>), 452 | {Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, false, State), 453 | websocket_payload_loop(State2, Req, HandlerState, 454 | Opcode, Len - byte_size(Data), MaskKey, 455 | << Unmasked/binary, Unmasked3/binary >>, UnmaskedLen + byte_size(Data), 456 | Rsv); 457 | websocket_payload(State, Req, HandlerState, 458 | Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Data, Rsv) -> 459 | << End:Len/binary, Rest/bits >> = Data, 460 | Unmasked2 = websocket_unmask(End, 461 | rotate_mask_key(MaskKey, UnmaskedLen), <<>>), 462 | {Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, true, State), 463 | websocket_dispatch(State2, Req, HandlerState, Rest, Opcode, 464 | << Unmasked/binary, Unmasked3/binary >>). 465 | 466 | %% -spec websocket_inflate_frame(binary(), rsv(), boolean(), #state{}) -> 467 | %% {binary(), #state{}}. 468 | websocket_inflate_frame(Data, << Rsv1:1, _:2 >>, _, 469 | #state{deflate_frame = DeflateFrame} = State) 470 | when DeflateFrame =:= false orelse Rsv1 =:= 0 -> 471 | {Data, State}; 472 | websocket_inflate_frame(Data, << 1:1, _:2 >>, false, State) -> 473 | Result = zlib:inflate(State#state.inflate_state, Data), 474 | {iolist_to_binary(Result), State}; 475 | websocket_inflate_frame(Data, << 1:1, _:2 >>, true, State) -> 476 | Result = zlib:inflate(State#state.inflate_state, 477 | << Data/binary, 0:8, 0:8, 255:8, 255:8 >>), 478 | {iolist_to_binary(Result), State}. 479 | 480 | %% -spec websocket_unmask(B, mask_key(), B) -> B when B::binary(). 481 | websocket_unmask(<<>>, _, Unmasked) -> 482 | Unmasked; 483 | websocket_unmask(<< O:32, Rest/bits >>, MaskKey, Acc) -> 484 | T = O bxor MaskKey, 485 | websocket_unmask(Rest, MaskKey, << Acc/binary, T:32 >>); 486 | websocket_unmask(<< O:24 >>, MaskKey, Acc) -> 487 | << MaskKey2:24, _:8 >> = << MaskKey:32 >>, 488 | T = O bxor MaskKey2, 489 | << Acc/binary, T:24 >>; 490 | websocket_unmask(<< O:16 >>, MaskKey, Acc) -> 491 | << MaskKey2:16, _:16 >> = << MaskKey:32 >>, 492 | T = O bxor MaskKey2, 493 | << Acc/binary, T:16 >>; 494 | websocket_unmask(<< O:8 >>, MaskKey, Acc) -> 495 | << MaskKey2:8, _:24 >> = << MaskKey:32 >>, 496 | T = O bxor MaskKey2, 497 | << Acc/binary, T:8 >>. 498 | 499 | %% Because we unmask on the fly we need to continue from the right mask byte. 500 | %% -spec rotate_mask_key(mask_key(), non_neg_integer()) -> mask_key(). 501 | rotate_mask_key(MaskKey, UnmaskedLen) -> 502 | Left = UnmaskedLen rem 4, 503 | Right = 4 - Left, 504 | (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)). 505 | 506 | %% Returns <<>> if the argument is valid UTF-8, false if not, 507 | %% or the incomplete part of the argument if we need more data. 508 | %% -spec is_utf8(binary()) -> false | binary(). 509 | is_utf8(Valid = <<>>) -> 510 | Valid; 511 | is_utf8(<< _/utf8, Rest/binary >>) -> 512 | is_utf8(Rest); 513 | %% 2 bytes. Codepages C0 and C1 are invalid; fail early. 514 | is_utf8(<< 2#1100000:7, _/bits >>) -> 515 | false; 516 | is_utf8(Incomplete = << 2#110:3, _:5 >>) -> 517 | Incomplete; 518 | %% 3 bytes. 519 | is_utf8(Incomplete = << 2#1110:4, _:4 >>) -> 520 | Incomplete; 521 | is_utf8(Incomplete = << 2#1110:4, _:4, 2#10:2, _:6 >>) -> 522 | Incomplete; 523 | %% 4 bytes. Codepage F4 may have invalid values greater than 0x10FFFF. 524 | is_utf8(<< 2#11110100:8, 2#10:2, High:6, _/bits >>) when High >= 2#10000 -> 525 | false; 526 | is_utf8(Incomplete = << 2#11110:5, _:3 >>) -> 527 | Incomplete; 528 | is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6 >>) -> 529 | Incomplete; 530 | is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6, 2#10:2, _:6 >>) -> 531 | Incomplete; 532 | %% Invalid. 533 | is_utf8(_) -> 534 | false. 535 | 536 | %% -spec websocket_payload_loop(#state{}, Req, any(), 537 | %% opcode(), non_neg_integer(), mask_key(), binary(), 538 | %% non_neg_integer(), rsv()) 539 | %% -> {ok, Req, cowboy_middleware:env()} 540 | %% | {suspend, module(), atom(), [any()]} 541 | %% when Req::elli_ws_request_adapter:req(). 542 | websocket_payload_loop(State=#state{socket={_, Port}=Socket, 543 | messages={OK, Closed, Error}, timeout_ref=TRef}, 544 | Req, HandlerState, Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv) -> 545 | ok = elli_tcp:setopts(Socket, [{active, once}]), 546 | receive 547 | {OK, Port, Data} -> 548 | State2 = handler_loop_timeout(State), 549 | websocket_payload(State2, Req, HandlerState, 550 | Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Data, Rsv); 551 | {Closed, Port} -> 552 | handler_terminate(State, Req, HandlerState, {error, closed}); 553 | {Error, Port, Reason} -> 554 | handler_terminate(State, Req, HandlerState, {error, Reason}); 555 | {timeout, TRef, ?MODULE} -> 556 | websocket_close(State, Req, HandlerState, {normal, timeout}); 557 | {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> 558 | websocket_payload_loop(State, Req, HandlerState, 559 | Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv); 560 | Message -> 561 | handler_call(State, Req, HandlerState, 562 | <<>>, websocket_info, Message, 563 | fun (State2, Req2, HandlerState2, _) -> 564 | websocket_payload_loop(State2, Req2, HandlerState2, 565 | Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv) 566 | end) 567 | end. 568 | 569 | %% -spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary()) 570 | %% -> {ok, Req, cowboy_middleware:env()} 571 | %% | {suspend, module(), atom(), [any()]} 572 | %% when Req::elli_ws_request_adapter:req(). 573 | %% Continuation frame. 574 | websocket_dispatch(State=#state{frag_state={nofin, Opcode, SoFar}}, 575 | Req, HandlerState, RemainingData, 0, Payload) -> 576 | websocket_data(State#state{frag_state={nofin, Opcode, 577 | << SoFar/binary, Payload/binary >>}}, Req, HandlerState, RemainingData); 578 | %% Last continuation frame. 579 | websocket_dispatch(State=#state{frag_state={fin, Opcode, SoFar}}, 580 | Req, HandlerState, RemainingData, 0, Payload) -> 581 | websocket_dispatch(State#state{frag_state=undefined}, Req, HandlerState, 582 | RemainingData, Opcode, << SoFar/binary, Payload/binary >>); 583 | %% Text frame. 584 | websocket_dispatch(State, Req, HandlerState, RemainingData, 1, Payload) -> 585 | handler_call(State, Req, HandlerState, RemainingData, 586 | websocket_handle, {text, Payload}, fun websocket_data/4); 587 | %% Binary frame. 588 | websocket_dispatch(State, Req, HandlerState, RemainingData, 2, Payload) -> 589 | handler_call(State, Req, HandlerState, RemainingData, 590 | websocket_handle, {binary, Payload}, fun websocket_data/4); 591 | %% Close control frame. 592 | websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, <<>>) -> 593 | websocket_close(State, Req, HandlerState, {remote, closed}); 594 | websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, 595 | << Code:16, Payload/bits >>) -> 596 | websocket_close(State, Req, HandlerState, {remote, Code, Payload}); 597 | %% Ping control frame. Send a pong back and forward the ping to the handler. 598 | websocket_dispatch(State=#state{socket=Socket}, 599 | Req, HandlerState, RemainingData, 9, Payload) -> 600 | Len = payload_length_to_binary(byte_size(Payload)), 601 | ok = elli_tcp:send(Socket, << 1:1, 0:3, 10:4, 0:1, Len/bits, Payload/binary >>), 602 | handler_call(State, Req, HandlerState, RemainingData, 603 | websocket_handle, {ping, Payload}, fun websocket_data/4); 604 | %% Pong control frame. 605 | websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) -> 606 | handler_call(State, Req, HandlerState, RemainingData, 607 | websocket_handle, {pong, Payload}, fun websocket_data/4). 608 | 609 | %% -spec handler_call(#state{}, Req, any(), binary(), atom(), any(), fun()) 610 | %% -> {ok, Req, cowboy_middleware:env()} 611 | %% | {suspend, module(), atom(), [any()]} 612 | %% when Req::elli_ws_request_adapter:req(). 613 | handler_call(State=#state{handler=Handler}, Req, HandlerState, 614 | RemainingData, Callback, Message, NextState) -> 615 | try elli_ws_request_adapter:websocket_handler_callback(Req, Handler, Callback, Message, HandlerState) of 616 | {ok, Req2, HandlerState2} -> 617 | NextState(State, Req2, HandlerState2, RemainingData); 618 | {ok, Req2, HandlerState2, hibernate} -> 619 | NextState(State#state{hibernate=true}, 620 | Req2, HandlerState2, RemainingData); 621 | {reply, Payload, Req2, HandlerState2} 622 | when is_list(Payload) -> 623 | case websocket_send_many(Payload, State) of 624 | {ok, State2} -> 625 | NextState(State2, Req2, HandlerState2, RemainingData); 626 | {shutdown, State2} -> 627 | handler_terminate(State2, Req2, HandlerState2, 628 | {normal, shutdown}); 629 | {{error, _} = Error, State2} -> 630 | handler_terminate(State2, Req2, HandlerState2, Error) 631 | end; 632 | {reply, Payload, Req2, HandlerState2, hibernate} 633 | when is_list(Payload) -> 634 | case websocket_send_many(Payload, State) of 635 | {ok, State2} -> 636 | NextState(State2#state{hibernate=true}, 637 | Req2, HandlerState2, RemainingData); 638 | {shutdown, State2} -> 639 | handler_terminate(State2, Req2, HandlerState2, 640 | {normal, shutdown}); 641 | {{error, _} = Error, State2} -> 642 | handler_terminate(State2, Req2, HandlerState2, Error) 643 | end; 644 | {reply, Payload, Req2, HandlerState2} -> 645 | case websocket_send(Payload, State) of 646 | {ok, State2} -> 647 | NextState(State2, Req2, HandlerState2, RemainingData); 648 | {shutdown, State2} -> 649 | handler_terminate(State2, Req2, HandlerState2, 650 | {normal, shutdown}); 651 | {{error, _} = Error, State2} -> 652 | handler_terminate(State2, Req2, HandlerState2, Error) 653 | end; 654 | {reply, Payload, Req2, HandlerState2, hibernate} -> 655 | case websocket_send(Payload, State) of 656 | {ok, State2} -> 657 | NextState(State2#state{hibernate=true}, 658 | Req2, HandlerState2, RemainingData); 659 | {shutdown, State2} -> 660 | handler_terminate(State2, Req2, HandlerState2, 661 | {normal, shutdown}); 662 | {{error, _} = Error, State2} -> 663 | handler_terminate(State2, Req2, HandlerState2, Error) 664 | end; 665 | {shutdown, Req2, HandlerState2} -> 666 | websocket_close(State, Req2, HandlerState2, {normal, shutdown}) 667 | catch 668 | throw:Exc -> 669 | handle_event(Req, Handler, websocket_throw, [Exc, erlang:get_stacktrace()], HandlerState); 670 | error:Error -> 671 | handle_event(Req, Handler, websocket_error, [Error, erlang:get_stacktrace()], HandlerState); 672 | exit:Exit -> 673 | handle_event(Req, Handler, websocket_exit, [Exit, erlang:get_stacktrace()], HandlerState) 674 | end. 675 | 676 | websocket_opcode(text) -> 1; 677 | websocket_opcode(binary) -> 2; 678 | websocket_opcode(close) -> 8; 679 | websocket_opcode(ping) -> 9; 680 | websocket_opcode(pong) -> 10. 681 | 682 | %% -spec websocket_deflate_frame(opcode(), binary(), #state{}) -> 683 | %% {binary(), rsv(), #state{}}. 684 | websocket_deflate_frame(Opcode, Payload, 685 | State=#state{deflate_frame = DeflateFrame}) 686 | when DeflateFrame =:= false orelse Opcode >= 8 -> 687 | {Payload, << 0:3 >>, State}; 688 | websocket_deflate_frame(_, Payload, State=#state{deflate_state = Deflate}) -> 689 | Deflated = iolist_to_binary(zlib:deflate(Deflate, Payload, sync)), 690 | DeflatedBodyLength = erlang:size(Deflated) - 4, 691 | Deflated1 = case Deflated of 692 | << Body:DeflatedBodyLength/binary, 0:8, 0:8, 255:8, 255:8 >> -> Body; 693 | _ -> Deflated 694 | end, 695 | {Deflated1, << 1:1, 0:2 >>, State}. 696 | 697 | %% -spec websocket_send(frame(), #state{}) 698 | %% -> {ok, #state{}} | {shutdown, #state{}} | {{error, atom()}, #state{}}. 699 | websocket_send(Type, State=#state{socket=Socket}) 700 | when Type =:= close -> 701 | Opcode = websocket_opcode(Type), 702 | case elli_tcp:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>) of 703 | ok -> {shutdown, State}; 704 | Error -> {Error, State} 705 | end; 706 | websocket_send(Type, State=#state{socket=Socket}) 707 | when Type =:= ping; Type =:= pong -> 708 | Opcode = websocket_opcode(Type), 709 | {elli_tcp:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>), State}; 710 | websocket_send({close, Payload}, State) -> 711 | websocket_send({close, 1000, Payload}, State); 712 | websocket_send({Type = close, StatusCode, Payload}, State=#state{ 713 | socket=Socket}) -> 714 | Opcode = websocket_opcode(Type), 715 | Len = 2 + iolist_size(Payload), 716 | %% Control packets must not be > 125 in length. 717 | true = Len =< 125, 718 | BinLen = payload_length_to_binary(Len), 719 | ok = elli_tcp:send(Socket, 720 | [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]), 721 | {shutdown, State}; 722 | websocket_send({Type, Payload0}, State=#state{socket=Socket}) -> 723 | Opcode = websocket_opcode(Type), 724 | {Payload, Rsv, State2} = websocket_deflate_frame(Opcode, iolist_to_binary(Payload0), State), 725 | Len = iolist_size(Payload), 726 | %% Control packets must not be > 125 in length. 727 | true = if Type =:= ping; Type =:= pong -> 728 | Len =< 125; 729 | true -> 730 | true 731 | end, 732 | BinLen = payload_length_to_binary(Len), 733 | {elli_tcp:send(Socket, 734 | [<< 1:1, Rsv/bits, Opcode:4, 0:1, BinLen/bits >>, Payload]), State2}. 735 | 736 | %% -spec websocket_send_many([frame()], #state{}) 737 | %% -> {ok, #state{}} | {shutdown, #state{}} | {{error, atom()}, #state{}}. 738 | websocket_send_many([], State) -> 739 | {ok, State}; 740 | websocket_send_many([Frame|Tail], State) -> 741 | case websocket_send(Frame, State) of 742 | {ok, State2} -> websocket_send_many(Tail, State2); 743 | {shutdown, State2} -> {shutdown, State2}; 744 | {Error, State2} -> {Error, State2} 745 | end. 746 | 747 | %% -spec websocket_close(#state{}, Req, any(), 748 | %% {atom(), atom()} | {remote, close_code(), binary()}) 749 | %% -> {ok, Req, cowboy_middleware:env()} 750 | %% when Req::elli_ws_request_adapter:req(). 751 | websocket_close(State=#state{socket=Socket}, 752 | Req, HandlerState, Reason) -> 753 | _OkOrError = case Reason of 754 | {normal, _} -> 755 | elli_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>); 756 | {error, badframe} -> 757 | elli_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1002:16 >>); 758 | {error, badencoding} -> 759 | elli_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1007:16 >>); 760 | %% TODO: check why this can't be reached. 761 | %% {error, handler} -> 762 | %% elli_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1011:16 >>); 763 | {remote, closed} -> 764 | elli_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>); 765 | {remote, Code, _} -> 766 | elli_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, Code:16 >>) 767 | end, 768 | handler_terminate(State, Req, HandlerState, Reason). 769 | 770 | %% -spec handler_terminate(#state{}, Req, any(), atom() | {atom(), atom()}) 771 | %% -> {ok, Req, cowboy_middleware:env()} 772 | %% when Req::elli_ws_request_adapter:req(). 773 | handler_terminate(#state{env=Env, handler=Handler}, Req, HandlerState, TerminateReason) -> 774 | handle_event(Req, Handler, websocket_close, [TerminateReason], HandlerState), 775 | {ok, Req, [{result, closed}|Env]}. 776 | 777 | %% -spec payload_length_to_binary(0..16#7fffffffffffffff) 778 | %% -> << _:7 >> | << _:23 >> | << _:71 >>. 779 | payload_length_to_binary(N) -> 780 | case N of 781 | N when N =< 125 -> << N:7 >>; 782 | N when N =< 16#ffff -> << 126:7, N:16 >>; 783 | N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >> 784 | end. 785 | 786 | 787 | %% 788 | %% Helper 789 | %% 790 | 791 | %% @doc Report the error to the websocket handler. 792 | handle_event(Req, Handler, Name, EventArgs, Opts) -> 793 | elli_ws_request_adapter:websocket_handler_handle_event(Req, Handler, Name, EventArgs, Opts). 794 | -------------------------------------------------------------------------------- /src/elli_ws_request_adapter.erl: -------------------------------------------------------------------------------- 1 | %% @author Maas-Maarten Zeeman 2 | %% @copyright 2013, Maas-Maarten Zeeman; 2018, elli-lib team 3 | %% 4 | %% @doc Elli WebSocket Request Adapter. 5 | %% @end 6 | %% 7 | %% Copyright 2013 Maas-Maarten Zeeman 8 | %% Copyright 2018 elli-lib-team 9 | %% 10 | %% Licensed under the Apache License, Version 2.0 (the "License"); 11 | %% you may not use this file except in compliance with the License. 12 | %% You may obtain a copy of the License at 13 | %% 14 | %% http://www.apache.org/licenses/LICENSE-2.0 15 | %% 16 | %% Unless required by applicable law or agreed to in writing, software 17 | %% distributed under the License is distributed on an "AS IS" BASIS, 18 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | %% See the License for the specific language governing permissions and 20 | %% limitations under the License. 21 | 22 | -module(elli_ws_request_adapter). 23 | 24 | -include_lib("elli/include/elli.hrl"). 25 | 26 | 27 | -export([ 28 | init/2, 29 | get/2, 30 | maybe_reply/2, 31 | ensure_response/2, 32 | upgrade_reply/3, 33 | parse_header/2, 34 | header/2, 35 | set_meta/3]). 36 | 37 | -export([ 38 | websocket_handler_init/3, 39 | websocket_handler_callback/5, 40 | 41 | websocket_handler_handle_event/5 42 | ]). 43 | 44 | %% Helper function. 45 | -export([messages/1]). 46 | -export_type([req/0]). 47 | 48 | 49 | %% Request adapter record. 50 | -record(req_adapter, { 51 | req :: elli:req(), 52 | 53 | %% If set to true we check if we can compress. 54 | resp_compress = false :: boolean(), 55 | 56 | %% Headers to add to the response. 57 | resp_headers = [] :: elli:headers(), %% TODO: should become elli:headers(). 58 | 59 | %% 60 | sent_upgrade_reply = false :: boolean(), 61 | 62 | %% Possible meta values. 63 | websocket_version=undefined :: undefined | integer(), 64 | websocket_compress=false :: boolean() 65 | }). 66 | 67 | -type req() :: #req_adapter{}. 68 | 69 | -ifdef(post20). 70 | -define(EXCEPTION(Class, Reason, Stacktrace), Class:Reason:Stacktrace). 71 | -define(GET_STACK(Stacktrace), Stacktrace). 72 | -else. 73 | -define(EXCEPTION(Class, Reason, _), Class:Reason). 74 | -define(GET_STACK(_), erlang:get_stacktrace()). 75 | -endif. 76 | 77 | %% 78 | %% 79 | %% 80 | 81 | %% @doc Initialize the request helper 82 | %% 83 | -spec init(Req :: elli:req(), RespCompress :: boolean()) -> req(). 84 | init(Req, RespCompress) -> 85 | #req_adapter{req=Req, resp_compress=RespCompress}. 86 | 87 | 88 | %% @doc Mimics cowboy_req:get/2 89 | %% 90 | -spec get(atom() | list(), req()) -> any() | list(). 91 | get(socket, ReqAdapter) -> 92 | Req = ReqAdapter#req_adapter.req, 93 | Req#req.socket; 94 | get(resp_compress, ReqAdapter) -> 95 | ReqAdapter#req_adapter.resp_compress; 96 | get(websocket_version, ReqAdapter) -> 97 | ReqAdapter#req_adapter.websocket_version; 98 | get(websocket_compress, ReqAdapter) -> 99 | ReqAdapter#req_adapter.websocket_compress; 100 | get(L, Req) when is_list(L) -> 101 | get(L, Req, []). 102 | get([], _Req, Acc) -> 103 | lists:reverse(Acc); 104 | get([H|T], Req, Acc) -> 105 | get(T, Req, [get(H, Req)|Acc]). 106 | 107 | 108 | %% @doc Mimics cowboy_req:maybe_reply/2 109 | %% 110 | -spec maybe_reply(400, req()) -> ok. 111 | maybe_reply(400, ReqAdapter) -> 112 | case ReqAdapter#req_adapter.sent_upgrade_reply of 113 | true -> 114 | ok; 115 | false -> 116 | reply(400, ReqAdapter) 117 | end. 118 | 119 | 120 | %% @doc Mimics cowboy_req:ensure_response/2 121 | %% 122 | -spec ensure_response(req(), 400) -> ok. 123 | ensure_response(ReqAdapter, 400) -> 124 | reply(400, ReqAdapter). 125 | 126 | 127 | %% 128 | %% Send an upgrade reply to the 129 | -spec upgrade_reply(101, elli:headers(), req()) -> {ok, req()}. 130 | upgrade_reply(101, Headers, #req_adapter{req=Req}=RA) -> 131 | UpgradeHeaders = [{<<"Connection">>, <<"Upgrade">>} | Headers], 132 | ok = elli_http:send_response(Req, 101, RA#req_adapter.resp_headers ++ UpgradeHeaders, <<>>), 133 | {ok, RA#req_adapter{sent_upgrade_reply=true}}. 134 | 135 | 136 | 137 | %% Note: The headers keys are already parsed by Erlang decode_packet. This 138 | %% means that all keys are capitalized. 139 | 140 | %% @doc Mimics cowboy_req:parse_header/3 {ok, ParsedHeaders, Req} 141 | %% 142 | parse_header(<<"upgrade">>, #req_adapter{req=Req}=RA) -> 143 | %% case insensitive tokens. 144 | Values = get_header_values(<<"Upgrade">>, Req), 145 | {ok, elli_ws_http:tokens(Values), RA}; 146 | parse_header(<<"connection">>, #req_adapter{req=Req}=RA) -> 147 | Values = get_header_values(<<"Connection">>, Req), 148 | {ok, elli_ws_http:tokens(Values), RA}; 149 | parse_header(<<"sec-websocket-extensions">>, #req_adapter{req=Req}=RA) -> 150 | Values = get_header_values(<<"Sec-WebSocket-Extensions">>, Req), 151 | %% We only recognize x-webkit-deflate-frame, which has no args, 152 | %% skip the rest. 153 | Exts = elli_ws_http:tokens(Values), 154 | Extensions = [{E, []} || E <- Exts, E =:= <<"x-webkit-deflate-frame">>], 155 | {ok, Extensions, RA}. 156 | 157 | %% @doc Mimics cowboy_req:header/2 158 | %% 159 | header(<<"sec-websocket-version">>, #req_adapter{req=Req}=RA) -> 160 | {get_header_value(<<"Sec-WebSocket-Version">>, Req), RA}; 161 | header(<<"sec-websocket-key">>, #req_adapter{req=Req}=RA) -> 162 | {get_header_value(<<"Sec-Websocket-Key">>, Req), RA}. 163 | 164 | 165 | %% @doc Mimics cowboy_req:set_meta/3 166 | %% 167 | set_meta(websocket_version, Version, ReqAdapter) -> 168 | ReqAdapter#req_adapter{websocket_version = Version}; 169 | set_meta(websocket_compress, Bool, ReqAdapter) -> 170 | ReqAdapter#req_adapter{websocket_compress = Bool}. 171 | 172 | 173 | %% @doc Call the websocket_init callback of the websocket handler. 174 | %% 175 | %% calls websocket_init(Req, HandlerOpts) -> 176 | %% {ok, Headers, HandlerState} 177 | %% We can upgrade, headers are added to the upgrade response. 178 | %% {ok, Headers, hibernate, HandlerState} 179 | %% We can upgrade, but this process will hibernate, headers 180 | %% are added to the upgrade response 181 | %% {ok, Headers, Timeout, HandlerState} 182 | %% We can upgrade, we will timout, headers are added to the upgrade respose. 183 | %% {ok, Headers, hibernate, Timeout, HandlerState} 184 | %% We can upgrade, set a timeout and hibernate. 185 | %% Headers are added to the response. 186 | %% {shutdown, Headers} 187 | %% We can't upgrade, a bad request response will be sent to the client. 188 | %% 189 | -spec websocket_handler_init(req(), Handler :: module(), HandlerState :: any()) -> 190 | {shutdown, req()} | 191 | {ok, req(), any()} | 192 | {ok, req(), any(), hibernate} | 193 | {ok, req(), any(), Timeout :: non_neg_integer()} | 194 | {ok, req(), any(), Timeout :: non_neg_integer(), hibernate}. 195 | websocket_handler_init(#req_adapter{req=Req}=RA, Handler, HandlerOpts) -> 196 | case Handler:websocket_init(Req, HandlerOpts) of 197 | {shutdown, Headers} -> 198 | {shutdown, RA#req_adapter{resp_headers=Headers}}; 199 | {ok, Headers, HandlerState} -> 200 | {ok, RA#req_adapter{resp_headers=Headers}, HandlerState}; 201 | {ok, Headers, hibernate, HandlerState} -> 202 | {ok, RA#req_adapter{resp_headers=Headers}, HandlerState, hibernate}; 203 | {ok, Headers, Timeout, HandlerState} -> 204 | {ok, RA#req_adapter{resp_headers=Headers}, HandlerState, Timeout}; 205 | {ok, Headers, hibernate, Timeout, HandlerState} -> 206 | {ok, RA#req_adapter{resp_headers=Headers}, HandlerState, Timeout, hibernate} 207 | end. 208 | 209 | %% @doc Calls websocket_info en websocket_handle callbacks. 210 | -spec websocket_handler_callback(Req, Handler, Callback, Message, HandlerState) -> Result when 211 | Req :: req(), 212 | Handler :: module(), 213 | Callback :: websocket_info | websocket_handle, 214 | Message :: any(), 215 | HandlerState :: any(), 216 | Result :: {ok, req(), any()} | 217 | {ok, req(), any(), hibernate} | 218 | {reply, elli_ws_protocol:frame() | [elli_ws_protocol:frame()], 219 | req(), any()} | 220 | {reply, elli_ws_protocol:frame() | [elli_ws_protocol:frame()], 221 | req(), any(), hibernate} | 222 | {shutdown, req(), any()}. 223 | websocket_handler_callback(#req_adapter{req=Req}=RA, Handler, Callback, Message, HandlerState) -> 224 | case Handler:Callback(Req, Message, HandlerState) of 225 | {ok, HandlerState1} -> 226 | {ok, RA, HandlerState1}; 227 | {ok, hibernate, HandlerState1} -> 228 | {ok, RA, HandlerState1, hibernate}; 229 | {reply, Payload, HandlerState1} -> 230 | {reply, Payload, RA, HandlerState1}; 231 | {reply, Payload, hibernate, HandlerState1} -> 232 | {reply, Payload, RA, HandlerState1, hibernate}; 233 | {shutdown, HandlerState1} -> 234 | {shutdown, RA, HandlerState1} 235 | end. 236 | 237 | %% @doc Report an event... 238 | -spec websocket_handler_handle_event(req(), Handler :: module(), atom(), list(), any()) -> ok. 239 | websocket_handler_handle_event(#req_adapter{req=Req}, Handler, Name, EventArgs, HandlerOpts) -> 240 | try 241 | Handler:websocket_handle_event(Name, [Req|EventArgs], HandlerOpts) 242 | catch 243 | ?EXCEPTION(EvClass, EvError, ST) -> 244 | error_logger:error_msg("~p:handle_event/3 crashed ~p:~p~n~p", 245 | [Handler, EvClass, EvError, ?GET_STACK(ST)]) 246 | end. 247 | 248 | %% @doc Atoms used to identify messages in {active, once | true} mode. 249 | -spec messages(RA :: req()) -> 250 | {tcp, tcp_closed, tcp_error} | {ssl, ssl_closed, ssl_error}. 251 | messages(#req_adapter{req=Req}) -> 252 | case Req#req.socket of 253 | undefined -> 254 | undefined; 255 | Socket -> 256 | socket_messages(Socket) 257 | end. 258 | 259 | -spec socket_messages(Socket :: elli_tcp:socket()) -> 260 | {tcp, tcp_closed, tcp_error} | {ssl, ssl_closed, ssl_error}. 261 | socket_messages({plain, _}) -> 262 | {tcp, tcp_closed, tcp_error}; 263 | socket_messages({ssl, _}) -> 264 | {ssl, ssl_closed, ssl_error}. 265 | 266 | %% 267 | %% Helpers 268 | %% 269 | 270 | %%%% @doc Send a bad_request reply. 271 | %% 272 | -spec reply(400, req()) -> ok. 273 | reply(400, #req_adapter{req=Req}) -> 274 | Body = <<"Bad request">>, 275 | Size = size(Body), 276 | ok = elli_http:send_response(Req, 400, [{"Connection", "close"}, 277 | {"Content-Length", Size}], Body). 278 | 279 | %% @doc Get all header values for Key 280 | -spec get_header_values(binary(), elli:req()) -> [binary()]. 281 | get_header_values(Key, #req{headers=Headers}) -> 282 | elli_proplists:get_all_values_ci(Key, Headers). 283 | 284 | %% @doc Get the first value. 285 | -spec get_header_value(binary(), elli:req()) -> undefined | binary(). 286 | get_header_value(Key, #req{headers=Headers}) -> 287 | elli_proplists:get_value_ci(Key, Headers). 288 | -------------------------------------------------------------------------------- /test/elli_bstr_tests.erl: -------------------------------------------------------------------------------- 1 | -module(elli_bstr_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | 6 | case_insensitive_equal_test() -> 7 | ?assert(elli_bstr:is_equal_ci(<<>>, <<>>)), 8 | ?assert(elli_bstr:is_equal_ci(<<"abc">>, <<"abc">>)), 9 | ?assert(elli_bstr:is_equal_ci(<<"123">>, <<"123">>)), 10 | 11 | ?assertNot(elli_bstr:is_equal_ci(<<"abcd">>, <<"abc">>)), 12 | ?assertNot(elli_bstr:is_equal_ci(<<"1234">>, <<"123">>)), 13 | 14 | ?assert(elli_bstr:is_equal_ci(<<"aBc">>, <<"abc">>)), 15 | ?assert(elli_bstr:is_equal_ci(<<"123AB">>, <<"123ab">>)), 16 | 17 | ?assertNot(elli_bstr:is_equal_ci(<<"1">>, <<"123ab">>)), 18 | ?assertNot(elli_bstr:is_equal_ci(<<"">>, <<"123ab">>)), 19 | ?assertNot(elli_bstr:is_equal_ci(<<"">>, <<" ">>)). 20 | 21 | 22 | %% Test if to_lower works. 23 | ascii_to_lower_test() -> 24 | ?assertMatch(<<>>, elli_bstr:to_lower(<<>>)), 25 | ?assertMatch(<<"abc">>, elli_bstr:to_lower(<<"abc">>)), 26 | ?assertMatch(<<"abc">>, elli_bstr:to_lower(<<"ABC">>)), 27 | ?assertMatch(<<"1234567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()">>, 28 | elli_bstr:to_lower(<<"1234567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()">>)), 29 | ?assertMatch(<<"1234567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()">>, 30 | elli_bstr:to_lower(<<"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()">>)). 31 | 32 | 33 | trim_test() -> 34 | ?assertMatch(<<"check">>, elli_bstr:trim(<<"check">>)), 35 | ?assertMatch(<<"check">>, elli_bstr:trim(<<" check">>)), 36 | ?assertMatch(<<"check">>, elli_bstr:trim(<<" check ">>)), 37 | 38 | ?assertMatch(<<"">>, elli_bstr:trim(<<" ">>)), 39 | ?assertMatch(<<>>, elli_bstr:trim(<<>>)), 40 | 41 | ?assertMatch(<<"">>, elli_bstr:trim(<<"\t\r\n">>)). 42 | -------------------------------------------------------------------------------- /test/elli_ws_http_tests.erl: -------------------------------------------------------------------------------- 1 | -module(elli_ws_http_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | 6 | tokens_test() -> 7 | ?assertMatch([<<"test">>], elli_ws_http:tokens(<<"test">>)), 8 | ?assertMatch([<<"test">>, <<"bla">>, <<"x">>], 9 | elli_ws_http:tokens(<<"test, bla,x">>)), 10 | ?assertMatch([<<"test">>, <<"een">>, <<"twee">>], 11 | elli_ws_http:tokens([<<"test">>, <<"een">>, <<"twee">>])), 12 | ?assertMatch([<<"test">>, <<"een">>, <<"twee">>], 13 | elli_ws_http:tokens([<<"test,,,">>, <<",,,een,,,">>, <<"twee">>])), 14 | ?assertMatch([], elli_ws_http:tokens(<<",,,">>)). 15 | -------------------------------------------------------------------------------- /test/ws_test.erl: -------------------------------------------------------------------------------- 1 | -module(ws_test). 2 | 3 | -export([start/0]). 4 | 5 | 6 | start() -> 7 | _ = application:start(crypto), 8 | _ = application:start(public_key), 9 | _ = application:start(ssl), 10 | 11 | WsConfig = [{handler, elli_example_websocket}], 12 | 13 | Config = [{mods, [{elli_example_websocket, WsConfig}]}], 14 | 15 | elli:start_link([{callback, elli_middleware}, 16 | {callback_args, Config}, {port, 8000}]). 17 | --------------------------------------------------------------------------------