├── .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 |
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 |
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 |
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 |
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/2 | Upgrade 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 |
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/4 | Upgrade 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 |
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 | Req = req()
Handler = module()
Callback = websocket_info | websocket_handle
Message = any()
HandlerState = any()
Result = {ok, req(), any()} | {ok, req(), any(), hibernate} | {reply, elli_ws_protocol:frame() | [elli_ws_protocol:frame()], req(), any()} | {reply, elli_ws_protocol:frame() | [elli_ws_protocol:frame()], req(), any(), hibernate} | {shutdown, req(), any()}
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 |
--------------------------------------------------------------------------------