├── rebar.config ├── .gitignore ├── rebar.debug_compilation.config ├── Gemfile ├── Guardfile ├── rebar.tests.config ├── src ├── wsock.app.src ├── wsock_http_message_data.erl ├── wsock_key.erl ├── wsock_handshake.erl ├── wsock_http.erl ├── wsock_framing.erl └── wsock_message.erl ├── espec ├── Rakefile ├── Gemfile.lock ├── CHANGELOG.md ├── include └── wsock.hrl ├── test └── spec │ ├── wsock_key_spec.erl │ ├── wsock_handshake_spec.erl │ ├── wsock_http_spec.erl │ ├── wsock_message_spec.erl │ └── wsock_framing_spec.erl └── README.md /rebar.config: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | ebin/ 3 | deps/ 4 | plt 5 | -------------------------------------------------------------------------------- /rebar.debug_compilation.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'guard-shell' 4 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :shell do 2 | watch(%r{src/.+\.erl}) {|m| `rebar compile && dialyzer --plt plt ebin` } 3 | end 4 | -------------------------------------------------------------------------------- /rebar.tests.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {espec, ".*", {git, "https://github.com/lucaspiller/espec.git", {branch, "master"}}}, 3 | {hamcrest, ".*", {git, "https://github.com/hyperthunk/hamcrest-erlang.git", {branch, master}}}, 4 | {meck, ".*", {git, "https://github.com/eproxus/meck.git", {branch, master}}} 5 | ]}. 6 | 7 | -------------------------------------------------------------------------------- /src/wsock.app.src: -------------------------------------------------------------------------------- 1 | {application, wsock, 2 | [ 3 | {description, "wsock is a library for building WebSocket clients and servers"}, 4 | {vsn, "1.1.6"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {env, []} 11 | ]}. 12 | -------------------------------------------------------------------------------- /espec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -smp enable -sname espec_runner -pa deps/*/ebin -pa ebin 3 | 4 | 5 | main([]) -> 6 | usage(); 7 | main(Args) -> 8 | espec_bin:run_spec_files_from_args(Args). 9 | 10 | -spec usage() -> term(). 11 | usage() -> 12 | io:format("Usage: espec [files or directories]\n"), 13 | halt(1). 14 | 15 | 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :clean do 2 | sh "rebar clean" 3 | end 4 | 5 | task :build => :clean do 6 | sh "rebar compile" 7 | end 8 | 9 | task :shell do 10 | sh "erl -pa ebin deps/*/ebin" 11 | end 12 | 13 | task :getdeps do 14 | sh "rebar get-deps" 15 | end 16 | 17 | task :doc do 18 | sh "rebar doc" 19 | end 20 | 21 | task :gettestdeps do 22 | sh "rebar -C rebar.tests.config get-deps" 23 | end 24 | 25 | task :features do 26 | sh "rebar -C rebar.tests.config compile run-features path=test/acceptance skip_deps=true" 27 | end 28 | 29 | task :spec do 30 | sh "rebar -C rebar.tests.config compile && ERL_LIBS='deps/' ./espec test/spec/" 31 | end 32 | 33 | task :default => :build 34 | -------------------------------------------------------------------------------- /src/wsock_http_message_data.erl: -------------------------------------------------------------------------------- 1 | -module(wsock_http_message_data). 2 | -include("wsock.hrl"). 3 | 4 | -export([new/0, new/1]). 5 | -export([update/2]). 6 | -export([headers/1]). 7 | 8 | new() -> 9 | new([]). 10 | 11 | new(Options) -> 12 | update(#http_message{}, Options). 13 | 14 | update(HttpMessage, Options) -> 15 | HttpMessage#http_message{ 16 | type = proplists:get_value(type, Options, HttpMessage#http_message.type), 17 | start_line = proplists:get_value(start_line, Options, HttpMessage#http_message.start_line), 18 | headers = proplists:get_value(headers, Options, HttpMessage#http_message.headers) 19 | }. 20 | 21 | headers(#http_message{ headers = Headers }) -> Headers. 22 | -------------------------------------------------------------------------------- /src/wsock_key.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | %% @hidden 16 | 17 | -module(wsock_key). 18 | 19 | -export([generate/0]). 20 | 21 | -spec generate() -> string(). 22 | generate() -> 23 | binary_to_list(base64:encode(crypto:rand_bytes(16))). 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | celluloid (0.15.2) 5 | timers (~> 1.1.0) 6 | coderay (1.1.0) 7 | ffi (1.9.3) 8 | formatador (0.2.4) 9 | guard (2.3.0) 10 | formatador (>= 0.2.4) 11 | listen (~> 2.1) 12 | lumberjack (~> 1.0) 13 | pry (>= 0.9.12) 14 | thor (>= 0.18.1) 15 | guard-shell (0.6.1) 16 | guard (>= 1.1.0) 17 | listen (2.4.0) 18 | celluloid (>= 0.15.2) 19 | rb-fsevent (>= 0.9.3) 20 | rb-inotify (>= 0.9) 21 | lumberjack (1.0.4) 22 | method_source (0.8.2) 23 | pry (0.9.12.6) 24 | coderay (~> 1.0) 25 | method_source (~> 0.8) 26 | slop (~> 3.4) 27 | rb-fsevent (0.9.4) 28 | rb-inotify (0.9.3) 29 | ffi (>= 0.5.0) 30 | slop (3.4.7) 31 | thor (0.18.1) 32 | timers (1.1.0) 33 | 34 | PLATFORMS 35 | ruby 36 | 37 | DEPENDENCIES 38 | guard-shell 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Change Log 2 | 3 | ### 1.1.6 4 | * Remove mod tuple from app.src file. That crashed releases as it referenced a module that does not exist. 5 | 6 | ### 1.1.5 7 | * Fix wsock_handshake:handle_response/2 spec 8 | 9 | ### 1.1.4 10 | * Move back to the header file (wsock.hrl) the definition of the #message{} record. Otherwise the record was not exported outside of the ```wsock_message``` module. 11 | 12 | ### 1.1.3 13 | * Fix versio number in app.src file 14 | 15 | ### 1.1.2 16 | * Handle fragmented HTTP messages. In previous version if you tried to decode a fragmented HTTP message with ```wsock_http:decode``` you will run into an error. Now, using [erlang:decode_packet](http://www.erlang.org/doc/man/erlang.html#decode_packet-3), ```wsock_http:decode``` can work around this issue. If you pass a fragmented http message it will return the atom ```fragmendted_http_message```. 17 | 18 | ### 1.1.1 19 | * Fix a bugs in type specs. 20 | -------------------------------------------------------------------------------- /include/wsock.hrl: -------------------------------------------------------------------------------- 1 | -record(http_message,{ 2 | type :: response | request, 3 | start_line :: list({atom(), string()}), 4 | headers :: list({string(), string()}) 5 | }). 6 | 7 | -record(handshake, { 8 | version :: integer(), 9 | type :: handle_open | handle_response | open | response, 10 | message :: #http_message{} 11 | }). 12 | 13 | -type bit() :: 0..1. 14 | 15 | -record(frame, { 16 | fin = 0:: bit(), 17 | rsv1 = 0 :: bit(), 18 | rsv2 = 0 :: bit(), 19 | rsv3 = 0 :: bit(), 20 | opcode :: byte(), 21 | mask = 0 :: bit(), 22 | payload_len :: byte(), 23 | extended_payload_len :: byte(), 24 | extended_payload_len_cont :: integer(), 25 | masking_key :: integer(), 26 | payload :: binary(), 27 | fragmented :: boolean(), 28 | raw :: binary(), % raw data for a fragmented frame 29 | next_piece :: atom(), 30 | next_piece_size :: integer() 31 | }). 32 | 33 | -type decode_options() :: masked. 34 | -type encode_options() :: mask. 35 | -type encode_types() :: text | binary | close | ping | pong. 36 | -type frame_type() :: encode_types() | continuation. 37 | -type payload() :: string() | binary() | {pos_integer(), string()}. 38 | 39 | -record(message, { 40 | frames = [] :: list(#frame{}), 41 | payload :: payload(), 42 | type :: encode_types() | fragmented 43 | }). 44 | 45 | -type message() :: #message{}. 46 | -------------------------------------------------------------------------------- /test/spec/wsock_key_spec.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | -module(wsock_key_spec). 16 | -include_lib("espec/include/espec.hrl"). 17 | -include_lib("hamcrest/include/hamcrest.hrl"). 18 | 19 | spec() -> 20 | describe("wsock_key", fun() -> 21 | it("should return a valid Sec-WebSocket-Key", fun() -> 22 | %Meck crashes if we try to mock crypto module 23 | %meck:new(crypto, [passthrough]), 24 | meck:new(base64, [unstick, passthrough]), 25 | 26 | Key = wsock_key:generate(), 27 | 28 | assert_that(length(Key), is(24)), 29 | %assert_that(meck:called(crypto, rand_bytes, 16), is(true)), 30 | assert_that(meck:called(base64, encode, '_'), is(true)), 31 | meck:unload(base64) 32 | end) 33 | end). 34 | -------------------------------------------------------------------------------- /test/spec/wsock_handshake_spec.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | -module(wsock_handshake_spec). 16 | -include_lib("espec/include/espec.hrl"). 17 | -include_lib("hamcrest/include/hamcrest.hrl"). 18 | 19 | -include("wsock.hrl"). 20 | 21 | spec() -> 22 | describe("wsock_handshake", fun() -> 23 | describe("handle_open", fun() -> 24 | it("should handle a open-handshake request from the client", fun() -> 25 | BinRequest = list_to_binary(["GET / HTTP/1.1\r\nHost : server.example.org\r\nUpgrade : websocket\r\nConnection : Upgrade\r\nSec-WebSocket-Key : AQIDBAUGBwgJCgsMDQ4PEA==\r\nSec-WebSocket-Version : 13\r\n\r\n"]), 26 | {ok, Message} = wsock_http:decode(BinRequest, request), 27 | {ok,Response} = wsock_handshake:handle_open(Message), 28 | 29 | assert_that(is_record(Response, handshake), is(true)), 30 | assert_that(Response#handshake.type, is(handle_open)) 31 | end), 32 | it("should return an error if the request isn't valid", fun() -> 33 | %Missing sec-websocket-key header 34 | BinRequest = list_to_binary(["GET / HTTP/1.1\r\nHost : server.example.org\r\nUpgrade : websocket\r\nConnection : Upgrade\r\nSec-WebSocket-Version : 13\r\n\r\n"]), 35 | {ok, Message} = wsock_http:decode(BinRequest, request), 36 | {error, invalid_handshake_opening} = wsock_handshake:handle_open(Message) 37 | end) 38 | end), 39 | describe("response", fun() -> 40 | it("should return a valid handshake response", fun() -> 41 | {ok, Response} = wsock_handshake:response("AQIDBAUGBwgJCgsMDQ4PEA=="), 42 | 43 | assert_that(is_record(Response, handshake), is(true)), 44 | assert_that(Response#handshake.type, is(response)), 45 | 46 | Message = Response#handshake.message, 47 | assert_that(Message#http_message.type, is(response)), 48 | assert_that(wsock_http:get_start_line_value(version, Message), is("1.1")), 49 | assert_that(wsock_http:get_start_line_value(status, Message), is("101")), 50 | assert_that(wsock_http:get_start_line_value(reason, Message), is("Switching protocols")), 51 | 52 | assert_that(wsock_http:get_header_value("upgrade", Message), is("Websocket")), 53 | assert_that(wsock_http:get_header_value("connection", Message), is("Upgrade")), 54 | assert_that(wsock_http:get_header_value("sec-websocket-accept", Message), is(fake_sec_websocket_accept("AQIDBAUGBwgJCgsMDQ4PEA=="))) 55 | end), 56 | it("should return an error if some of the required fields is missing") 57 | end), 58 | describe("open", fun() -> 59 | it("should return a valid handshake request", fun() -> 60 | Resource = "/", 61 | Host = "localhost", 62 | Port = 8080, 63 | 64 | {ok, HandShake} = wsock_handshake:open(Resource, Host, Port), 65 | assert_that(HandShake#handshake.version, is(13)), 66 | assert_that(HandShake#handshake.type, is(open)), 67 | 68 | HttpMessage = HandShake#handshake.message, 69 | assert_that(wsock_http:get_start_line_value(method, HttpMessage), is("GET")), 70 | assert_that(wsock_http:get_start_line_value(version, HttpMessage), is("1.1")), 71 | assert_that(wsock_http:get_start_line_value(resource, HttpMessage), is("/")), 72 | 73 | assert_that(wsock_http:get_header_value("Host", HttpMessage), is(Host ++ ":" ++ integer_to_list(Port))), 74 | assert_that(wsock_http:get_header_value("Upgrade", HttpMessage), is("websocket")), 75 | assert_that(wsock_http:get_header_value("Connection", HttpMessage), is("upgrade")), 76 | assert_that(wsock_http:get_header_value("Sec-Websocket-Key", HttpMessage), is_not(undefined)), 77 | assert_that(wsock_http:get_header_value("Sec-Websocket-Version", HttpMessage), is("13")) 78 | end) 79 | end), 80 | describe("handle_response", fun() -> 81 | it("should handle handshake response from a server", fun() -> 82 | Resource = "/", 83 | Host = "localhost", 84 | Port = 8080, 85 | 86 | {ok, OpenHandShake} = wsock_handshake:open(Resource, Host, Port), 87 | Key = wsock_http:get_header_value("sec-websocket-key", OpenHandShake#handshake.message), 88 | 89 | BinResponse = list_to_binary(["HTTP/1.1 101 Switch Protocols\r\nUpgrade: websocket\r\nConnection: upgrade\r\nSec-Websocket-Accept: ", fake_sec_websocket_accept(Key), "\r\n","Header-A: A\r\nHeader-C: 123123\r\nHeader-D: D\r\n\r\n"]), 90 | {ok, Response} = wsock_http:decode(BinResponse, response), 91 | 92 | {ok, Handshake} = wsock_handshake:handle_response(Response, OpenHandShake), 93 | assert_that(Handshake#handshake.type, is(handle_response)), 94 | assert_that(Handshake#handshake.message, is(Response)) 95 | end) 96 | end) 97 | end). 98 | 99 | fake_sec_websocket_accept(Key) -> 100 | BinaryKey = list_to_binary(Key), 101 | base64:encode_to_string(crypto:sha(<>)). 102 | -------------------------------------------------------------------------------- /src/wsock_handshake.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | %% @hidden 16 | 17 | -module(wsock_handshake). 18 | -include("wsock.hrl"). 19 | 20 | -export([open/3, handle_response/2]). 21 | -export([handle_open/1, response/1]). 22 | 23 | -define(VERSION, 13). 24 | -define(GUID, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"). 25 | 26 | -define(INVALID_CLIENT_OPEN, invalid_handshake_opening). 27 | -define(INVALID_SERVER_RESPONSE, invalid_server_response). 28 | 29 | -spec handle_open(Message::#http_message{}) -> {ok, #handshake{}} | {error, ?INVALID_CLIENT_OPEN}. 30 | handle_open(Message) -> 31 | case validate_handshake_open(Message) of 32 | true -> 33 | {ok , #handshake{ type = handle_open, message = Message}}; 34 | false -> 35 | {error, ?INVALID_CLIENT_OPEN} 36 | end. 37 | 38 | -spec handle_response(Response::#http_message{}, Handshake::#handshake{}) -> {ok, #handshake{}} | {error, ?INVALID_SERVER_RESPONSE}. 39 | handle_response(Response, Handshake) -> 40 | case validate_handshake_response(Response, Handshake) of 41 | true -> 42 | {ok, #handshake{ type = handle_response, message = Response}}; 43 | false -> 44 | {error, ?INVALID_SERVER_RESPONSE} 45 | end. 46 | 47 | -spec response(ClientWebsocketKey::string()) -> {ok, #handshake{}}. 48 | response(ClientWebsocketKey) -> 49 | BinaryKey = list_to_binary(ClientWebsocketKey), 50 | HttpMessage = #http_message{ 51 | start_line = [ 52 | {version, "1.1"}, 53 | {status, "101"}, 54 | {reason, "Switching protocols"} 55 | ], 56 | headers = [ 57 | {"upgrade", "Websocket"}, 58 | {"connection", "Upgrade"}, 59 | {"sec-websocket-accept", base64:encode_to_string(crypto:hash(sha, <>)) } 60 | ], 61 | type = response 62 | }, 63 | 64 | {ok, #handshake{ type = response, message = HttpMessage}}. 65 | 66 | -spec open(Resource ::string(), Host ::string(), Port::integer()) -> {ok, #handshake{}}. 67 | open(Resource, Host, Port) -> 68 | RequestLine = [ 69 | {method, "GET"}, 70 | {version, "1.1"}, 71 | {resource, Resource} 72 | ], 73 | 74 | Headers =[ 75 | {"Host", Host ++ ":" ++ integer_to_list(Port)}, 76 | {"Upgrade", "websocket"}, 77 | {"Connection", "upgrade"}, 78 | {"Sec-Websocket-Key", wsock_key:generate()}, 79 | {"Sec-Websocket-Version", integer_to_list(?VERSION)} 80 | ], 81 | 82 | Message = wsock_http:build(request, RequestLine, Headers), 83 | {ok, #handshake{ version = ?VERSION, type = open, message = Message}}. 84 | 85 | 86 | %======================= 87 | % INTERNAL FUNCTIONS 88 | %======================= 89 | 90 | -spec validate_startline(StartLine::list({atom(), term()})) -> true | false. 91 | validate_startline(StartLine) -> 92 | Matchers = [{method, "GET"}, {version, "1\.1"}], 93 | lists:all(fun({Key, Value}) -> 94 | match == re:run(proplists:get_value(Key, StartLine), Value, [caseless, {capture, none}]) 95 | end, Matchers). 96 | 97 | validate_headers(Headers) -> 98 | Matchers = [ 99 | {"host", ".+", required}, 100 | {"upgrade", "websocket", required}, 101 | {"connection", "upgrade", required}, 102 | {"sec-websocket-key", "[a-z0-9\+\/]{22}==", required}, 103 | {"sec-websocket-version", "13", required}, 104 | {"origin", ".+", optional}], 105 | 106 | lists:all(fun({HeaderName, HeaderValue, Type}) -> 107 | case get_value_insensitive(HeaderName, Headers) of 108 | undefined when (Type == optional) -> 109 | true; 110 | undefined -> 111 | false; 112 | Value -> 113 | match == re:run(Value, HeaderValue, [caseless, {capture, none}]) 114 | end 115 | end, Matchers). 116 | 117 | -spec validate_handshake_response(Response::#http_message{}, OpenHandshake::#handshake{}) -> true | false. 118 | validate_handshake_response(Response, OpenHandshake) -> 119 | validate_http_status(Response) 120 | andalso 121 | validate_upgrade_header(Response) 122 | andalso 123 | validate_connection_header(Response) 124 | andalso 125 | validate_sec_websocket_accept_header(Response, OpenHandshake). 126 | 127 | -spec validate_handshake_open(OpenHandshake::#http_message{}) -> true | false. 128 | validate_handshake_open(OpenHandshake) -> 129 | validate_startline(OpenHandshake#http_message.start_line) 130 | andalso 131 | validate_headers(OpenHandshake#http_message.headers). 132 | 133 | get_value_insensitive(Key, [{Name, Value} | Tail]) -> 134 | case re:run(Name, "^" ++ Key ++ "$", [caseless, {capture, first, list}]) of 135 | {match, _} -> 136 | Value; 137 | nomatch -> 138 | get_value_insensitive(Key, Tail) 139 | end; 140 | 141 | get_value_insensitive(_, []) -> 142 | undefined. 143 | 144 | 145 | 146 | 147 | -spec validate_http_status(Response::#http_message{}) -> boolean(). 148 | validate_http_status(Response) -> 149 | "101" == wsock_http:get_start_line_value(status, Response). 150 | 151 | -spec validate_upgrade_header(Response ::#http_message{}) -> boolean(). 152 | validate_upgrade_header(Response) -> 153 | "websocket" == string:to_lower(wsock_http:get_header_value("upgrade", Response)). 154 | 155 | -spec validate_connection_header(Response ::#http_message{}) -> boolean(). 156 | validate_connection_header(Response) -> 157 | "upgrade" == string:to_lower(wsock_http:get_header_value("connection", Response)). 158 | 159 | -spec validate_sec_websocket_accept_header(Response::#http_message{}, Handshake::#handshake{}) -> boolean(). 160 | validate_sec_websocket_accept_header(Response, Handshake) -> 161 | ClientKey = wsock_http:get_header_value("sec-websocket-key", Handshake#handshake.message), 162 | BinaryClientKey = list_to_binary(ClientKey), 163 | ExpectedHashedKey = base64:encode_to_string(crypto:hash(sha, <>)), 164 | HashedKey = wsock_http:get_header_value("sec-websocket-accept", Response), 165 | 166 | ExpectedHashedKey == HashedKey. 167 | -------------------------------------------------------------------------------- /src/wsock_http.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | %% @hidden 16 | 17 | -module(wsock_http). 18 | -include("wsock.hrl"). 19 | 20 | -export([build/3, get_start_line_value/2, get_header_value/2]). 21 | -export([decode/2, encode/1]). 22 | 23 | -define(CTRL, "\r\n"). 24 | 25 | -spec decode(Data::binary(), Type::request | response) -> {ok,#http_message{}} | {error, malformed_request} | fragmented_http_message. 26 | decode(Data, Type) -> 27 | case process_startline(Data, Type) of 28 | fragmented -> 29 | fragmented_http_message; 30 | {error, _} -> 31 | {error, malformed_request}; 32 | {ok, StartlineFields, Rest} -> 33 | case process_headers(Rest) of 34 | fragmented -> 35 | fragmented_http_message; 36 | {error, _} -> 37 | {error, malformed_request}; 38 | {ok, HeadersFields} -> 39 | {ok, wsock_http:build(Type, StartlineFields, HeadersFields)} 40 | end 41 | end. 42 | 43 | -spec build(Type::atom(), StartLine::list({atom(), string()}), Headers::list({string(), string()})) -> #http_message{}. 44 | build(Type, StartLine, Headers) -> 45 | #http_message{type = Type, start_line = StartLine, headers = Headers}. 46 | 47 | -spec encode(Message::#http_message{}) -> list(string()). 48 | encode(Message) -> 49 | Startline = Message#http_message.start_line, 50 | Headers = Message#http_message.headers, 51 | encode(Startline, Headers, Message#http_message.type). 52 | 53 | -spec encode(Startline::list({atom(), string()}), Headers::list({string(), string()}), Type:: request | response) -> list(string()). 54 | encode(Startline, Headers, request) -> 55 | encode_message("{{method}} {{resource}} HTTP/{{version}}", Startline, Headers); 56 | 57 | encode(Startline, Headers, response) -> 58 | encode_message("HTTP/{{version}} {{status}} {{reason}}", Startline, Headers). 59 | 60 | 61 | -spec get_start_line_value(Key::atom(), Message::#http_message{}) -> string(). 62 | get_start_line_value(Key, Message) -> 63 | proplists:get_value(Key, Message#http_message.start_line). 64 | 65 | -spec get_header_value(Key::string(), Message::#http_message{}) -> string(). 66 | get_header_value(Key, Message) -> 67 | LowerCasedKey = string:to_lower(Key), 68 | get_header_value_case_insensitive(LowerCasedKey, Message#http_message.headers). 69 | 70 | -spec get_header_value_case_insensitive(Key::string(), list()) -> undefined | string(). 71 | get_header_value_case_insensitive(_, []) -> 72 | undefined; 73 | 74 | get_header_value_case_insensitive(Key, [{Name, Value} | Tail]) -> 75 | LowerCaseName = string:to_lower(Name), 76 | case Key == LowerCaseName of 77 | true -> 78 | Value; 79 | false -> 80 | get_header_value_case_insensitive(Key, Tail) 81 | end. 82 | 83 | %============= 84 | % Helpers 85 | %============= 86 | -spec ensure_string(Data :: list()) -> list() 87 | ; (Data :: binary()) -> list() 88 | ; (Data :: atom()) -> list(). 89 | ensure_string(Data) when is_list(Data) -> Data; 90 | ensure_string(Data) when is_binary(Data) -> erlang:binary_to_list(Data); 91 | ensure_string(Data) when is_integer(Data) -> erlang:integer_to_list(Data); 92 | ensure_string(Data) -> erlang:atom_to_list(Data). 93 | 94 | -spec process_startline( 95 | StartLine::binary(), 96 | Type:: request | response 97 | ) -> 98 | fragmented | 99 | {ok, list({atom(), string()}), binary()} | 100 | {error, term()}. 101 | process_startline(StartLine, request) -> 102 | decode_http_message(http_bin, start_line, StartLine); 103 | 104 | process_startline(StartLine, response) -> 105 | decode_http_message(http_bin, status_line, StartLine). 106 | 107 | -spec decode_http_message( 108 | Type :: atom(), 109 | Chunk :: atom(), 110 | Data :: binary() 111 | ) -> 112 | fragmented | 113 | {error, invalid_http_message} | 114 | {error, unexpected_http_message} | 115 | {ok, [{method, string()} | [{resource, string()} | {version, string()}]], binary()} | 116 | {ok, [{version, string()} | [{status, string()} | {reason, string()}]], binary()} | 117 | {ok, {string(), string()}, binary()} | 118 | ok. 119 | decode_http_message(Type, Chunk, Data) -> 120 | case erlang:decode_packet(Type, Data, []) of 121 | {more, _} -> 122 | fragmented; 123 | {error, _} -> 124 | {error, invalid_http_message}; 125 | {ok, {http_error, _}} -> 126 | {error, invalid_http_message}; 127 | {ok, {http_request, Method, Resource, Version}, Rest} when Chunk == start_line -> 128 | {ok, [{method, ensure_string(Method)}, {resource, process_http_uri(Resource)}, {version, process_http_version(Version)}], Rest}; 129 | {ok, {http_response, Version, Status, Reason}, Rest} when Chunk == status_line -> 130 | {ok, [{version, process_http_version(Version)}, {status, ensure_string(Status)}, {reason, ensure_string(Reason)}], Rest}; 131 | {ok, {http_header, _, Field, _, Value}, Rest} when Chunk == header-> 132 | {ok, {ensure_string(Field), ensure_string(Value)}, Rest}; 133 | {ok, http_eoh, _} when Chunk == header -> 134 | ok; 135 | _ -> 136 | {error, unexpected_http_message} 137 | end. 138 | 139 | -spec process_http_uri( 140 | '*' 141 | ) -> string() 142 | ; 143 | ({ 144 | absoluteURI, 145 | Protocol :: http | http, 146 | Host :: string() | binary(), 147 | Port :: inet:port_number() | undefined, 148 | Path :: string() | binary() 149 | }) -> string() 150 | ; 151 | ({ 152 | scheme, 153 | Scheme :: string() | binary(), 154 | string() | binary() 155 | }) -> string() 156 | ; 157 | ({ 158 | abs_path, 159 | string() | binary() 160 | }) -> string(). 161 | process_http_uri('*') -> 162 | "*"; 163 | process_http_uri({absoluteURI, Protocol, Host, Port, Path}) -> 164 | PortString = case Port of 165 | undefined -> ""; 166 | Number -> ":" ++ ensure_string(Number) 167 | end, 168 | 169 | ensure_string(Protocol) ++ "://" ++ ensure_string(Host) ++ PortString ++ "/" ++ ensure_string(Path) ; 170 | process_http_uri({scheme, Scheme, Path}) -> 171 | ensure_string(Scheme) ++ Path; 172 | process_http_uri({abs_path, Path}) -> 173 | ensure_string(Path); 174 | process_http_uri(Uri) -> 175 | ensure_string(Uri). 176 | 177 | -spec process_http_version({Major :: pos_integer(), Minor :: pos_integer()}) -> string(). 178 | process_http_version({Major, Minor}) -> 179 | ensure_string(Major) ++ "." ++ ensure_string(Minor). 180 | 181 | -spec process_headers(Headers::list(binary())) -> {ok, list({string(), string()})} | {error, nomatch}. 182 | process_headers(Headers) -> 183 | process_headers(Headers, []). 184 | 185 | -spec process_headers( 186 | Headers::binary(), 187 | Acc::list({list(), list()}) 188 | ) -> 189 | fragmented | 190 | {ok, list({string(), string()})} | 191 | {error, invalid_http_message}. 192 | process_headers(Data, Acc) -> 193 | case decode_http_message(httph_bin, header, Data) of 194 | {ok, Header, Rest} -> 195 | process_headers(Rest, [Header | Acc]); 196 | ok -> 197 | {ok, Acc}; 198 | Other -> 199 | Other 200 | end. 201 | 202 | encode_message(StartlineExpr, StartlineFields, Headers) -> 203 | SL = build_start_line(StartlineExpr, StartlineFields), 204 | H= build_headers(Headers), 205 | 206 | lists:foldr(fun(El, Acc) -> 207 | [El++"\r\n" | Acc] 208 | end, ["\r\n"], [SL | H]). 209 | 210 | build_start_line(StartlineExpr, StartlineFields) -> 211 | lists:foldr(fun({Key, Value}, Acc) -> 212 | re:replace(Acc, "{{" ++ atom_to_list(Key) ++ "}}", Value, [{return, list}]) 213 | end, StartlineExpr, StartlineFields). 214 | 215 | -spec build_headers(list({HeaderName::string(), HeaderValue::string()})) -> list(string()). 216 | build_headers(Headers) -> 217 | lists:map(fun({Key, Value}) -> 218 | Key ++ ": " ++ Value 219 | end, Headers). 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Analytics](https://ga-beacon.appspot.com/UA-46795389-1/wsock/README)](https://github.com/igrigorik/ga-beacon) 2 | 3 | 4 | #WSOCK 5 | 6 | 7 | * [About](#about) 8 | * [Examples](#examples) 9 | * [Writing clients](#usage_clients) 10 | * [Upgrading the connection](#upgrading_client) 11 | * [Sending data](#sending_client) 12 | * [Receiving data](#receiving_client) 13 | * [Control messages](#client_control_messages) 14 | * [Writing servers](#usage_servers) 15 | * [Upgrading the connection](#upgrading_server) 16 | * [Sending data](#sending_server) 17 | * [Receiving data](#receiving_server) 18 | * [Control messages](#server_control_messages) 19 | * [Documentation](#documentation) 20 | * [Tests](#tests) 21 | * [Contributing](#contributing) 22 | * [Author](#author) 23 | * [License](#license) 24 | 25 | 26 | ## About 27 | 28 | Wsock are a set of modules that can be used to build Websockets ([RFC 6455](http://tools.ietf.org/html/rfc6455#section-5.3) compliant) clients an servers. 29 | 30 | ## Examples 31 | 32 | [wsserver](https://github.com/madtrick/wsserver) (a WebSockets server) and [wsecli](https://github.com/madtrick/wsecli) (a WebSockets client) are projects which use wsock. 33 | 34 | 35 | ## Writing clients 36 | Don't forguet to include the wsock headers file: 37 | 38 | ```erlang 39 | -include_lib("wsock/include/wsock.hrl"). 40 | ``` 41 | ### Upgrading the connection 42 | 43 | Create and send an upgrade request to the server. 44 | 45 | 46 | 1. Build a handshake request: 47 | 48 | ```erlang 49 | HandshakeRequest = wsock_handshake:open(Resource, Host, Port) 50 | ``` 51 | 52 | 2. Encode the handshake to send it to the server: 53 | 54 | ```erlang 55 | BinaryData = wsock_http:encode(HandshakeRequest#handshake.message) 56 | ``` 57 | 58 | 3. Receive and validate the handshake response: 59 | 60 | ```erlang 61 | {ok, HandshakeResponse} = wsock_http:decode(Data, response) 62 | wsock_handshake:handle_response(HandshakeResponse, HandshakeRequest) 63 | ``` 64 | 65 | If the received HTTP message is fragmented ```wsock_http:decode``` will return the atom ```fragmented_http_message```. Check the section [upgrading the connection when writing servers](#upgrading_server) for more info. 66 | 67 | ### Sending data 68 | Once the connection has been stablished you can send data through it: 69 | 70 | ```erlang 71 | Message = wsock_message:encode(Data, [mask, text]) %text data 72 | Message = wsock_message:encode(Data, [mask, binary]) %binary data 73 | ``` 74 | 75 | ### Receiving data 76 | 77 | * If there is no previous fragmented message: 78 | 79 | ```erlang 80 | ListOfMessages = wsock_message:decode(Data, []) 81 | ``` 82 | 83 | * If the previously received message was fragmented pass it as a parameter: 84 | 85 | ```erlang 86 | ListOfMessages = wsock_message:decode(Data, FragmentedMessage, []) 87 | ``` 88 | 89 | Check ```wsock.hrl``` for a description of the message record. 90 | 91 | ### Control messages 92 | 93 | * Ping/pong messages: 94 | 95 | ```erlang 96 | ListOfMessages = wsock_message:encode(Data, [mask, ping]) 97 | ListOfMessages = wsock_message:encode(Data, [mask, pong]) 98 | ``` 99 | 100 | * Close messages (with or without reason): 101 | 102 | ```erlang 103 | ListOfMessages = wsock_message:encode({StatusCode, Payload}, [mask, close]) 104 | ListOfMessages = wsock_message:encode([]], [close]) % If no payload 105 | ``` 106 | 107 | 108 | ## Writing servers 109 | 110 | 111 | Don't forget to include the wsock headers file: 112 | 113 | ```erlang 114 | -include_lib("wsock/include/wsock.hrl"). 115 | ``` 116 | 117 | ### Upgrading the connection 118 | 119 | Accept upgrade requests from your clients. 120 | 121 | 1. Decode the http-handshake request: 122 | 123 | ```erlang 124 | {ok, OpenHttpMessage} = wsock_http:decode(Data, request), 125 | {ok, OpenHandshake} = wsock_handshake:handle_open(OpenHttpMessage) 126 | ``` 127 | 128 | If the received HTTP message is fragmented ```wsock_http:decode``` will return the atom ```fragmented_http_message```. In this case, buffer the partial HTTP message until more data is received and try again. 129 | 130 | ```erlang 131 | fragmented_http_message = wsock_http:decode(Data, request), 132 | 133 | %% Buffer Data 134 | %% … some time passes and then more data is received 135 | %% Concat the new data to the buffered one and pass it to wsock_http:decode 136 | 137 | {ok, OpenHttpMessage} = wsock_http:decode(<>, request), 138 | … 139 | ``` 140 | 141 | 142 | 2. Get handshake key to generate a handshake response: 143 | 144 | ```erlang 145 | ClientWSKey = wsock_http:get_header_value("sec-websocket-key", OpenHandshake#handshake.message), 146 | {ok, HandshakeResponse} = wsock_handshake:response(ClientWSKey) 147 | ``` 148 | 149 | 3. Encode the http-handshake response: 150 | 151 | ```erlang 152 | ResponseHttpMessage = wsock_http:encode(HandshakeResponse#handshake.message) 153 | ``` 154 | 155 | Now all you have to do is send the handshake response over the wire to upgrade the HTTP connection to a WebSockets one. 156 | 157 | ### Receiving data 158 | 159 | * If there is no previous fragmented message: 160 | 161 | ```erlang 162 | ListOfMessages = wsock_message:decode(Data, [masked]) 163 | ``` 164 | 165 | * If the previously received message was fragmented pass it as a parameter: 166 | 167 | ```erlang 168 | ListOfMessages = wsock_message:decode(Data, FragmentedMessage, [masked]) 169 | ``` 170 | 171 | Check wsock.hrl for a description of the message record. 172 | 173 | ### Sending data 174 | Once the connection has been stablished you can send data through it: 175 | 176 | ```erlang 177 | ListOfMessages = wsock_message:encode(Data, [text]) % text data, servers don't mask data 178 | ListOfMessages = wsock_message:encode(Data, [binary]) % binary data 179 | ``` 180 | 181 | ### Control messages 182 | 183 | * Ping/pong messages: 184 | 185 | ```erlang 186 | ListOfMessages = wsock_message:encode(Data, [ping]) 187 | ListOfMessages = wsock_message:encode(Data, [pong]) 188 | ``` 189 | * Close messages (with or without reason): 190 | 191 | ```erlang 192 | ListOfMessages = wsock_message:encode({StatusCode, Payload}, [close]) 193 | ListOfMessages = wsock_message:encode([]], [close]) % If no payload 194 | ``` 195 | 196 | ## Documentation 197 | Documentation for the modules can be generated. Run: 198 | 199 | ```shell 200 | rake doc 201 | ``` 202 | 203 | or, in case you don't have rake installed: 204 | 205 | ```shell 206 | rebar doc 207 | ``` 208 | 209 | ## Tests 210 | Unit test where done with the library [espec](https://github.com/lucaspiller/espec) by lucaspiller. To run them: 211 | 212 | ``` 213 | rake spec 214 | ``` 215 | or, in case you don't have rake installed: 216 | 217 | ``` 218 | rebar compile && ERL_LIBS='deps/' ./espec test/spec/ 219 | ``` 220 | 221 | ## Contribute 222 | 223 | If you find or think that something isn't working properly, just open an issue. 224 | 225 | Pull requests and patches (with tests) are welcome. 226 | 227 | ## Author 228 | 229 | This stuff has been writen by Farruco sanjurjo 230 | 231 | * [@madtrick](https://twitter.com/madtrick) at twitter 232 | * Email at [madtrick@gmail.com](madtrick@gmail.com) 233 | 234 | ## License 235 | Copyright [2012] [Farruco Sanjurjo Arcay] 236 | 237 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 238 | 239 | http://www.apache.org/licenses/LICENSE-2.0 240 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 241 | 242 | -------------------------------------------------------------------------------- /src/wsock_framing.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | %% @hidden 16 | 17 | -module(wsock_framing). 18 | -include("wsock.hrl"). 19 | 20 | -export([to_binary/1, from_binary/1, from_binary/2, frame/1, frame/2]). 21 | 22 | -define(OP_CODE_CONT, 0). 23 | -define(OP_CODE_TEXT, 1). 24 | -define(OP_CODE_BIN, 2). 25 | -define(OP_CODE_CLOSE, 8). 26 | -define(OP_CODE_PING, 9). 27 | -define(OP_CODE_PONG, 10). 28 | 29 | -spec to_binary(Frame::#frame{}) -> binary(). 30 | to_binary(Frame) -> 31 | Bin1 = << 32 | (Frame#frame.fin):1, 33 | (Frame#frame.rsv1):1, (Frame#frame.rsv2):1, (Frame#frame.rsv3):1, 34 | (Frame#frame.opcode):4, 35 | (Frame#frame.mask):1, 36 | (Frame#frame.payload_len):7, 37 | (Frame#frame.extended_payload_len):(extended_payload_len_bit_width(Frame#frame.extended_payload_len, 16)), 38 | (Frame#frame.extended_payload_len_cont):(extended_payload_len_bit_width(Frame#frame.extended_payload_len_cont, 64)) 39 | >>, 40 | 41 | Bin2 = case Frame#frame.masking_key of 42 | undefined -> 43 | Bin1; 44 | Key -> 45 | <> 46 | end, 47 | 48 | <>. 49 | 50 | -spec from_binary(Data::binary()) -> list(#frame{}). 51 | from_binary(Data) -> 52 | lists:reverse(new_from_binary(Data, [])). 53 | 54 | -spec from_binary(Data::binary(), FragmentedFrame :: #frame{}) -> list(#frame{}). 55 | from_binary(Data, FragmentedFrame = #frame{ fragmented = true }) -> 56 | lists:reverse(continue_from_binary(Data, FragmentedFrame, [])). 57 | 58 | %=================== 59 | % Internal 60 | %=================== 61 | 62 | new_from_binary(Data, Acc) -> 63 | new_frame_decoding(Data, Acc). 64 | 65 | new_frame_decoding(<<>>, Acc) -> 66 | Acc; 67 | new_frame_decoding(Data, Acc) -> 68 | {Frame, Rest} = from_binary(Data, first_byte, #frame{}), 69 | new_frame_decoding(Rest, [Frame | Acc]). 70 | 71 | continue_from_binary(Data, FragmentedFrame, Acc) -> 72 | ComposedData = <<(FragmentedFrame#frame.raw)/binary, Data/binary>>, 73 | {Frame, Rest} = from_binary(ComposedData, next_piece_from_binary(ComposedData, FragmentedFrame#frame.next_piece_size, FragmentedFrame#frame.next_piece), FragmentedFrame), 74 | new_frame_decoding(Rest, [Frame | Acc]). 75 | 76 | from_binary(Data, {not_enough_bytes, ExpectedNextPiece, ExpectedSize}, Frame) -> 77 | {Frame#frame{fragmented = true, raw = Data, next_piece = ExpectedNextPiece, next_piece_size = ExpectedSize}, <<>>}; 78 | from_binary(<>, first_byte, Frame) -> 79 | NewFrame = Frame#frame{ fin = Fin, rsv1 = Rsv1, rsv2 = Rsv2, rsv3 = Rsv3, opcode = Opcode }, 80 | from_binary(Rest, next_piece_from_binary(Rest, 1, second_byte), NewFrame); 81 | from_binary(<>, second_byte, Frame) -> 82 | NewFrame = Frame#frame{ mask = Mask, payload_len = 127 }, 83 | from_binary(Rest, next_piece_from_binary(Rest, 8, extended_payload_length_cont), NewFrame); 84 | from_binary(<>, second_byte, Frame) -> 85 | NewFrame = Frame#frame{ mask = Mask, payload_len = 126 }, 86 | from_binary(Rest, next_piece_from_binary(Rest, 2, extended_payload_length), NewFrame); 87 | from_binary(<>, second_byte, Frame) -> 88 | NewFrame = Frame#frame{ mask = Mask, payload_len = PayloadLen }, 89 | from_binary(Rest, payload_or_masking_key(Rest, NewFrame), NewFrame); 90 | from_binary(<>, extended_payload_length, Frame)-> 91 | NewFrame = Frame#frame{ extended_payload_len = ExtendedPayloadLength }, 92 | from_binary(Rest, payload_or_masking_key(Rest, NewFrame), NewFrame); 93 | from_binary(<>, extended_payload_length_cont, Frame)-> 94 | NewFrame = Frame#frame{ extended_payload_len_cont = ExtendedPayloadLengthCont }, 95 | from_binary(Rest, payload_or_masking_key(Rest, NewFrame), NewFrame); 96 | from_binary(Data, masking_key, Frame = #frame{ mask = 0 }) -> 97 | from_binary(Data, next_piece_from_binary(Data, real_payload_length(Frame), payload), Frame); 98 | from_binary(<>, masking_key, Frame)-> 99 | NewFrame = Frame#frame{ masking_key = MaskKey }, 100 | from_binary(Rest, next_piece_from_binary(Rest, real_payload_length(Frame), payload), NewFrame); 101 | 102 | from_binary(Data, payload, Frame = #frame{ mask = 1, masking_key = MaskingKey })-> 103 | {Payload, Rest} = extract_payload(Data, Frame), 104 | NewFrame = Frame#frame{ payload = mask(Payload, MaskingKey, <<>>)}, 105 | {finish_frame(NewFrame), Rest}; 106 | 107 | from_binary(Data, payload, Frame)-> 108 | {Payload, Rest} = extract_payload(Data, Frame), 109 | NewFrame = Frame#frame{ payload = Payload}, 110 | {finish_frame(NewFrame), Rest}. 111 | 112 | extract_payload(Data, Frame) -> 113 | RL = real_payload_length(Frame), 114 | <> = Data, 115 | {Payload, Rest}. 116 | 117 | finish_frame(Frame) -> 118 | Frame#frame{ fragmented = false, raw = <<>>, next_piece = undefined, next_piece_size = undefined}. 119 | 120 | real_payload_length(Frame = #frame{ payload_len = 126 }) -> 121 | Frame#frame.extended_payload_len; 122 | real_payload_length(Frame = #frame{ payload_len = 127 }) -> 123 | Frame#frame.extended_payload_len_cont; 124 | real_payload_length(Frame) -> 125 | Frame#frame.payload_len. 126 | 127 | extended_payload_len_bit_width(PayloadLen, Max) -> 128 | case PayloadLen of 129 | 0 -> 0; 130 | _ -> Max 131 | end. 132 | 133 | payload_or_masking_key(Data, #frame{ mask = 1 }) -> 134 | next_piece_from_binary(Data, 4, masking_key); 135 | payload_or_masking_key(Data, Frame) -> 136 | next_piece_from_binary(Data, real_payload_length(Frame), payload). 137 | 138 | next_piece_from_binary(Data, RequiredSize, PieceDescription) -> 139 | case assert_required_bytes(Data, RequiredSize) of 140 | true -> PieceDescription; 141 | false -> {not_enough_bytes, PieceDescription, RequiredSize} 142 | end. 143 | 144 | assert_required_bytes(Data, RequiredSize) -> 145 | byte_size(Data) >= RequiredSize. 146 | 147 | -spec frame(Data::binary() | string()) -> #frame{}. 148 | frame(Data) when is_binary(Data) -> 149 | frame(Data, [{opcode, binary}]); 150 | 151 | frame(Data) when is_list(Data)-> 152 | frame(list_to_binary(Data), [{opcode, text}]). 153 | 154 | -spec frame(Data::string() | binary(), Options::list()) -> #frame{}. 155 | frame(Data, Options) when is_list(Data) -> 156 | frame(list_to_binary(Data), Options); 157 | 158 | %don't like having this function clause just for close frames 159 | frame({CloseCode, Reason}, Options) -> 160 | BinReason = list_to_binary(Reason), 161 | Data = <>, 162 | frame(Data, Options); 163 | 164 | 165 | frame(Data, Options) -> 166 | Frame = #frame{ payload = Data}, 167 | Frame2 = length(Frame, Data), 168 | apply_options(Frame2, Options). 169 | 170 | -spec apply_options(Frame::#frame{}, Options::list()) -> #frame{}. 171 | apply_options(Frame, [mask | Tail]) -> 172 | <> = crypto:rand_bytes(4), 173 | T = Frame#frame{ 174 | mask = 1, 175 | masking_key = MaskKey, 176 | payload = mask(Frame#frame.payload, MaskKey, <<>>) 177 | }, 178 | apply_options(T, Tail); 179 | 180 | apply_options(Frame, [fin | Tail]) -> 181 | T = Frame#frame{fin = 1}, 182 | apply_options(T, Tail); 183 | 184 | apply_options(Frame, [{opcode, continuation} | Tail]) -> 185 | T = Frame#frame{opcode = ?OP_CODE_CONT}, 186 | apply_options(T, Tail); 187 | 188 | apply_options(Frame, [{opcode, text} | Tail]) -> 189 | T = Frame#frame{opcode = ?OP_CODE_TEXT}, 190 | apply_options(T, Tail); 191 | 192 | apply_options(Frame, [{opcode, binary} | Tail]) -> 193 | T = Frame#frame{opcode = ?OP_CODE_BIN}, 194 | apply_options(T, Tail); 195 | 196 | apply_options(Frame, [{opcode, close} | Tail]) -> 197 | T = Frame#frame{opcode = ?OP_CODE_CLOSE}, 198 | apply_options(T, Tail); 199 | 200 | apply_options(Frame, [{opcode, ping} | Tail]) -> 201 | T = Frame#frame{opcode = ?OP_CODE_PING}, 202 | apply_options(T, Tail); 203 | 204 | apply_options(Frame, [{opcode, pong} | Tail]) -> 205 | T = Frame#frame{opcode = ?OP_CODE_PONG}, 206 | apply_options(T, Tail); 207 | 208 | apply_options(Frame, []) -> 209 | Frame. 210 | 211 | -spec length(Frame::#frame{}, Data :: binary()) -> #frame{}. 212 | length(Frame, Data) -> 213 | Len = byte_size(Data), 214 | if 215 | Len =< 125 -> 216 | Frame#frame{ 217 | payload_len = Len, 218 | extended_payload_len = 0, 219 | extended_payload_len_cont = 0 220 | }; 221 | (Len > 125) and (Len =< 65536) -> 222 | Frame#frame{ 223 | payload_len = 126, 224 | extended_payload_len = Len, 225 | extended_payload_len_cont = 0 226 | }; 227 | Len > 65536 -> 228 | Frame#frame{ 229 | payload_len = 127, 230 | extended_payload_len = 0, 231 | extended_payload_len_cont = Len 232 | } 233 | end. 234 | 235 | 236 | % 237 | % Masking code got at Cowboy source code 238 | % 239 | -spec mask(Data::binary(), MaskKey::integer(), Acc::binary()) -> binary(). 240 | mask(<>, MaskKey, Acc) -> 241 | T = Data bxor MaskKey, 242 | mask(Rest, MaskKey, <>); 243 | 244 | mask(<>, MaskKey, Acc) -> 245 | <> = <>, 246 | T = Data bxor MaskKey2, 247 | <>; 248 | 249 | mask(<>, MaskKey, Acc) -> 250 | <> = <>, 251 | T = Data bxor MaskKey2, 252 | <>; 253 | 254 | mask(<>, MaskKey, Acc) -> 255 | <> = <>, 256 | T = Data bxor MaskKey2, 257 | <>; 258 | 259 | mask(<<>>, _, Acc) -> 260 | Acc. 261 | -------------------------------------------------------------------------------- /test/spec/wsock_http_spec.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | -module(wsock_http_spec). 16 | -include_lib("espec/include/espec.hrl"). 17 | -include_lib("hamcrest/include/hamcrest.hrl"). 18 | -include("wsock.hrl"). 19 | 20 | spec() -> 21 | describe("build", fun() -> 22 | it("should build proper HTTP messages", fun() -> 23 | RequestLine = [ 24 | {method, "GET"}, 25 | {version, "1.1"}, 26 | {resource, "/"} 27 | ], 28 | 29 | Headers = [ 30 | {"Header-A", "A"}, 31 | {"Header-B", "B"} 32 | ], 33 | 34 | Message = wsock_http:build(request, RequestLine, Headers), 35 | 36 | assert_that(Message#http_message.type, is(request)), 37 | 38 | assert_that(proplists:get_value(method, Message#http_message.start_line), is("GET")), 39 | assert_that(proplists:get_value(version, Message#http_message.start_line), is("1.1")), 40 | assert_that(proplists:get_value(resource, Message#http_message.start_line), is("/")), 41 | assert_that(proplists:get_value("Header-A", Message#http_message.headers), is("A")), 42 | assert_that(proplists:get_value("Header-B", Message#http_message.headers), is("B")) 43 | end) 44 | end), 45 | describe("get_start_line_value", fun() -> 46 | it("should return http_message start_line values if present", fun() -> 47 | Message = #http_message{ 48 | type = request, 49 | start_line = [ 50 | {method, "GET"}, 51 | {version, "1.1"}, 52 | {resource, "/"} 53 | ], 54 | headers = [ 55 | {"header-a", "A"}, 56 | {"header-b", "b"} 57 | ] 58 | }, 59 | 60 | assert_that(wsock_http:get_start_line_value(version, Message), is("1.1")), 61 | assert_that(wsock_http:get_start_line_value(method, Message), is("GET")), 62 | assert_that(wsock_http:get_start_line_value(resource, Message), is("/")) 63 | end) 64 | end), 65 | describe("get_header_value", fun() -> 66 | it("should return http_message header values if present", fun() -> 67 | Message = #http_message{ 68 | type = request, 69 | start_line = [ 70 | {method, "GET"}, 71 | {version, "1.1"}, 72 | {resource, "/"} 73 | ], 74 | headers = [ 75 | {"Header-a", "A"}, 76 | {"header-B", "b"} 77 | ] 78 | }, 79 | 80 | assert_that(wsock_http:get_header_value("header-a", Message), is("A")), 81 | assert_that(wsock_http:get_header_value("header-b", Message), is("b")) 82 | end) 83 | end), 84 | describe("encode", fun() -> 85 | describe("requests", fun() -> 86 | it("should return a string representating a http request", fun() -> 87 | RequestLine = [ 88 | {method, "GET"}, 89 | {version, "1.1"}, 90 | {resource, "/"} 91 | ], 92 | 93 | Headers = [ 94 | {"Header-A", "A"}, 95 | {"Header-B", "B"} 96 | ], 97 | 98 | %Message = wsock_http:build(request, RequestLine, Headers), 99 | Message = #http_message{ type = request, start_line = RequestLine, headers = Headers }, 100 | Request = wsock_http:encode(Message), 101 | 102 | assert_that(Request, is([ 103 | "GET / HTTP/1.1\r\n", 104 | "Header-A: A\r\n", 105 | "Header-B: B\r\n", 106 | "\r\n" 107 | ])) 108 | end), 109 | it("should return an error if some field is missing") 110 | end), 111 | describe("responses", fun() -> 112 | it("should return a string representating a http response", fun() -> 113 | StartLine = [ 114 | {version, "1.1"}, 115 | {status, "101"}, 116 | {reason, "Switching protocols"} 117 | ], 118 | 119 | Headers = [ 120 | {"Header-A", "A"}, 121 | {"Header-B", "B"} 122 | ], 123 | 124 | Message = #http_message{ type = response, start_line = StartLine, headers = Headers}, 125 | Response = wsock_http:encode(Message), 126 | 127 | assert_that(Response, is([ 128 | "HTTP/1.1 101 Switching protocols\r\n", 129 | "Header-A: A\r\n", 130 | "Header-B: B\r\n", 131 | "\r\n"]) 132 | ) 133 | end), 134 | it("should return an error if some field is missing") 135 | end) 136 | end), 137 | describe("decode", fun()-> 138 | describe("requests", fun() -> 139 | it("should return a http_message record of type request", fun() -> 140 | Data = <<"GET / HTTP/1.1\r\nHost : www.example.org\r\nUpgrade : websocket\r\nSec-WebSocket-Key : ----\r\nHeader-D: D\r\n\r\n">>, 141 | 142 | {ok, Message} = wsock_http:decode(Data, request), 143 | 144 | assert_that(Message#http_message.type, is(request)), 145 | 146 | assert_that(wsock_http:get_start_line_value(method, Message), is("GET")), 147 | assert_that(wsock_http:get_start_line_value(resource, Message), is("/")), 148 | assert_that(wsock_http:get_start_line_value(version, Message), is("1.1")), 149 | 150 | assert_that(wsock_http:get_header_value("host", Message), is("www.example.org")), 151 | assert_that(wsock_http:get_header_value("upgrade", Message), is("websocket")), 152 | assert_that(wsock_http:get_header_value("sec-websocket-key", Message), is("----")), 153 | assert_that(wsock_http:get_header_value("header-d", Message), is("D")), 154 | assert_that(wsock_http:get_header_value("non-existant-header", Message), is(undefined)) 155 | 156 | end), 157 | it("should return an error if the message startline is malformed", fun() -> 158 | %Missing HTTP method 159 | Data = <<" / HTTP/1.1\r\nHost : www.example.org\r\nUpgrade : websocket\r\nSec-WebSocket-Key : ----\r\nHeader-D: D\r\n\r\n">>, 160 | 161 | Response = wsock_http:decode(Data, request), 162 | 163 | assert_that(Response, is({error, malformed_request})) 164 | end ), 165 | it("should return an error if some header is malformed", fun() -> 166 | %missing ":" in Upgrade header 167 | Data = <<"GET / HTTP/1.1\r\nHost : www.example.org\r\nUpgrade websocket\r\nHeader-D: D\r\n\r\n">>, 168 | 169 | Response = wsock_http:decode(Data, request), 170 | 171 | assert_that(Response, is({error, malformed_request})) 172 | end), 173 | describe("should handle fragmented http requests", fun() -> 174 | it("should handle a request with only a chunk of the startline", fun() -> 175 | Data = <<"GET / ">>, 176 | 177 | Response = wsock_http:decode(Data, request), 178 | assert_that(Response, is(fragmented_http_message)) 179 | end), 180 | it("should handle a request with fragmented headers", fun() -> 181 | Data = <<"GET / HTTP/1.1\r\nHost">>, 182 | 183 | Response = wsock_http:decode(Data, request), 184 | assert_that(Response, is(fragmented_http_message)) 185 | end) 186 | end) 187 | end), 188 | describe("responses", fun() -> 189 | it("should return a http_message record of type response", fun() -> 190 | Data = <<"HTTP/1.1 205 Reset Content\r\nHeader-A: A\r\nHeader-C: dGhlIHNhbXBsZSBub25jZQ==\r\nHeader-D: D\r\n\r\n">>, 191 | 192 | {ok, Message} = wsock_http:decode(Data, response), 193 | 194 | assert_that(Message#http_message.type, is(response)), 195 | 196 | assert_that(wsock_http:get_start_line_value(version, Message), is("1.1")), 197 | assert_that(wsock_http:get_start_line_value(status, Message), is("205")), 198 | assert_that(wsock_http:get_start_line_value(reason, Message), is("Reset Content")), 199 | 200 | assert_that(wsock_http:get_header_value("header-a", Message), is("A")), 201 | assert_that(wsock_http:get_header_value("header-c", Message), is("dGhlIHNhbXBsZSBub25jZQ==")), 202 | assert_that(wsock_http:get_header_value("header-d", Message), is("D")) 203 | end), 204 | it("should return an error if the message startline is malformed", fun() -> 205 | Data = <<"HTTP/1.1 Reset Content\r\nHeader-A: A\r\nHeader-C: dGhlIHNhbXBsZSBub25jZQ==\r\nHeader-D: D\r\n\r\n">>, 206 | 207 | Response = wsock_http:decode(Data, response), 208 | 209 | assert_that(Response, is({error, malformed_request})) 210 | end), 211 | it("should return an error if some header is malformed", fun() -> 212 | Data = <<"HTTP/1.1 205 Reset Content\r\nHeader-A: A\r\nHeader-C dGhlIHNhbXBsZSBub25jZQ==\r\nHeader-D: D\r\n\r\n">>, 213 | 214 | Response = wsock_http:decode(Data, response), 215 | 216 | assert_that(Response, is({error, malformed_request})) 217 | end), 218 | describe("should handle fragmented http responses", fun() -> 219 | it("should handle a response with only a chunk of the status line", fun() -> 220 | Data = <<"HTTP/1.1 205">>, 221 | 222 | Response = wsock_http:decode(Data, response), 223 | assert_that(Response, is(fragmented_http_message)) 224 | end), 225 | it("should handle a response with fragmented headers", fun() -> 226 | Data = <<"HTTP/1.1 200 OK\r\nContent-type">>, 227 | 228 | Response = wsock_http:decode(Data, response), 229 | assert_that(Response, is(fragmented_http_message)) 230 | end) 231 | end) 232 | end) 233 | end). 234 | -------------------------------------------------------------------------------- /src/wsock_message.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | -module(wsock_message). 16 | -include("wsock.hrl"). 17 | 18 | -export([encode/2, decode/2, decode/3]). 19 | %-export([encode/3]). 20 | 21 | 22 | %%======================================== 23 | %% Constants 24 | %%======================================== 25 | -define(FRAGMENT_SIZE, 4096). 26 | 27 | %%======================================== 28 | %% Types 29 | %%======================================== 30 | -type message_type() :: begin_message | continue_message. 31 | 32 | % @type decode_options() = masked. 33 | % @type encode_options() = mask. 34 | % @type encode_types() = text | binary | close | ping | pong. 35 | % @type frame_type() = encode_types() | continuation. 36 | % @type payload() = string() | binary() | {pos_integer(), string()}. 37 | % @type message() = #message{ 38 | % frames = list(#frame{}), 39 | % payload = payload(), 40 | % type = encode_types() | fragmented 41 | % }. 42 | 43 | %%======================================== 44 | %% Interface functions 45 | %%======================================== 46 | 47 | % @doc Encode Data into WebSockets frames and returns an iolist with those 48 | % frames. 49 | % 50 | % Options can be: 51 | % 52 | %
53 | %
`text'
54 | %
To encode data as text messages
55 | % 56 | %
`binary'
57 | %
To encode data as binary messages
58 | % 59 | %
`close'
60 | %
To encode data as close messages. When including a payload for this type of messages it 61 | % has to be a tuple with an integer if the first position (closing status) and a string on the 62 | % second (reason)
63 | % 64 | %
`ping'
65 | %
To encode data as ping messages
66 | % 67 | %
`pong'
68 | %
To encode data as pong messages
69 | % 70 | %
`mask'
71 | %
To mask the data (i.e. when it is send from clients to servers)
72 | % 73 | %
74 | % 75 | % One of the `text', `binary', `close', `ping' or `pong' options is required. 76 | % 77 | % If no type (see {@link encode_types()}) is given the error `missing_datatypes' is returned. 78 | -spec encode( 79 | Data :: payload(), 80 | Options :: [encode_types() | encode_options()] 81 | ) -> 82 | [binary()] | 83 | {error, missing_datatype}. 84 | encode(Data, Options) when is_list(Data) -> 85 | encode(list_to_binary(Data), Options); 86 | encode(Data, Options) -> 87 | case extract_type(Options) of 88 | error -> 89 | {error, missing_datatype}; 90 | {Type, BaseOptions} -> 91 | lists:reverse(encode(Data, Type, BaseOptions, [])) 92 | end. 93 | 94 | 95 | % @doc Decode received frames and return a list of messages 96 | % 97 | % Options can be: 98 | % 99 | %
100 | %
`masked'
101 | %
Data contains masked data. By default, if this option is not present, 102 | % the library will consider the data as not masked. 103 | %
104 | %
105 | % 106 | % If a received message is fragmented (WebSocket or TCP fragmentation), this 107 | % message must be given as parameter to {@link decode/3} when new data is ready to be 108 | % decoded. 109 | % 110 | % A fragmented message has the property `Message#message.type' set with the atom `fragmented'. 111 | -spec decode( 112 | Data :: binary(), 113 | Options :: list(decode_options()) 114 | ) -> 115 | list(message()) | 116 | {error, 117 | fragmented_control_message | 118 | frames_masked | 119 | frames_unmasked 120 | }. 121 | decode(Data, Options) -> 122 | Masked = proplists:get_value(masked, Options, false), 123 | decode(Data, begin_message, #message{}, Masked). 124 | 125 | % @see decode/2 126 | % @doc Decodes received frames and tries to complete a previous fragmented message. 127 | -spec decode( 128 | Data :: binary(), 129 | Message :: message(), 130 | Options :: list(decode_options()) 131 | ) -> 132 | [] | 133 | list(message()) | 134 | {error, 135 | fragmented_control_message | 136 | frames_masked | 137 | frames_unmasked 138 | }. 139 | decode(Data, Message, Options) -> 140 | Masked = proplists:get_value(masked, Options, false), 141 | decode(Data, continue_message, Message, Masked). 142 | 143 | %%======================================== 144 | %% Internal 145 | %%======================================== 146 | -spec extract_type( 147 | Options :: list(encode_options() | encode_types()) 148 | ) -> 149 | error | 150 | {encode_types(), []} | 151 | {encode_types(), encode_options()}. 152 | extract_type(Options) -> 153 | Types = [text, binary, close, ping, pong], 154 | Type = lists:filter(fun(E) -> 155 | true == proplists:get_value(E, Options) 156 | end, Types), 157 | 158 | case Type of 159 | [] -> error; 160 | [T] -> 161 | OptionsWithoutType = proplists:delete(T, Options), 162 | {T, OptionsWithoutType} 163 | end. 164 | 165 | -spec encode( 166 | Data :: binary(), 167 | Type :: frame_type(), 168 | BaseOptions :: list(encode_options()), 169 | Acc :: list(binary()) 170 | ) -> list(binary()). 171 | encode(Data, Type, BaseOptions, _Acc) when Type =:= ping ; Type =:= pong ; Type =:= close-> 172 | [frame(Data, [ fin, {opcode, Type} | BaseOptions])]; 173 | encode(<>, Type, BaseOptions, Acc) -> 174 | [frame(Data, [fin, {opcode, Type} | BaseOptions]) | Acc]; 175 | encode(<>, Type, BaseOptions, []) -> 176 | encode(Rest, continuation, BaseOptions, [frame(Data, [{opcode, Type} | BaseOptions]) | []]); 177 | encode(<>, Type, BaseOptions, Acc) -> 178 | encode(Rest, Type, BaseOptions, [frame(Data, [{opcode, Type} | BaseOptions]) | Acc]); 179 | encode(<<>>, _Type, _Options, Acc) -> 180 | Acc; 181 | encode(<>, Type, BaseOptions, Acc) -> 182 | [frame(Data, [fin, {opcode, Type} | BaseOptions]) | Acc]. 183 | 184 | -spec frame( 185 | Data :: binary(), 186 | Options :: list() 187 | ) -> binary(). 188 | frame(Data, Options) -> 189 | Frame = wsock_framing:frame(Data, Options), 190 | wsock_framing:to_binary(Frame). 191 | 192 | -spec decode( 193 | Data :: binary(), 194 | Type :: message_type(), 195 | Message :: message(), 196 | Masked :: boolean() 197 | ) -> 198 | list(message()) | 199 | {error, 200 | frames_unmasked | 201 | frames_masked | 202 | fragmented_control_message 203 | }. 204 | decode(Data, begin_message, _Message, Masked) -> 205 | do_decode(Data, begin_message, [], Masked); 206 | decode(Data, continue_message, Message, Masked) -> 207 | do_decode(Data, continue_message, [Message | []], Masked). 208 | 209 | -spec do_decode( 210 | Data :: binary(), 211 | Type :: message_type(), 212 | Acc :: list(), 213 | Masked :: boolean() 214 | ) -> 215 | list(message()) | 216 | {error, 217 | frames_unmasked | 218 | frames_masked | 219 | fragmented_control_message 220 | }. 221 | do_decode(Data, continue_message, [FragmentedMessage | Acc] = Messages, Masked) -> 222 | [LastFrame | TailFrames] = FragmentedMessage#message.frames, 223 | 224 | case LastFrame#frame.fragmented of 225 | true -> 226 | Frames = wsock_framing:from_binary(Data, LastFrame), 227 | do_decode_frames(Masked, continue_message, Frames, [FragmentedMessage#message{ frames = TailFrames } | Acc]); 228 | false -> 229 | Frames = wsock_framing:from_binary(Data), 230 | do_decode_frames(Masked, continue_message, Frames, Messages) 231 | end; 232 | do_decode(Data, Type, Acc, Masked) -> 233 | Frames = wsock_framing:from_binary(Data), 234 | do_decode_frames(Masked, Type, Frames, Acc). 235 | 236 | -spec do_decode_frames( 237 | Masked :: boolean(), 238 | Type :: message_type(), 239 | Frames :: list(#frame{}), 240 | Acc :: list() 241 | ) -> 242 | list(message()) | 243 | {error, 244 | frames_unmasked | 245 | frames_masked | 246 | fragmented_control_message 247 | }. 248 | do_decode_frames(_Masked = true, Type, Frames, Acc) -> 249 | do_decode_masked_frames(ensure_all_frames_mask_value(Frames, 1), Type, Frames, Acc); 250 | do_decode_frames(_Masked = false, Type, Frames, Acc) -> 251 | do_decode_unmasked_frames(ensure_all_frames_mask_value(Frames, 0), Type, Frames, Acc). 252 | 253 | -spec do_decode_masked_frames( 254 | AllFramesMasked :: boolean(), 255 | Type :: message_type(), 256 | Frames :: list(#frame{}), 257 | Acc :: list() 258 | ) -> 259 | list(message()) | 260 | {error, 261 | frames_unmasked | 262 | fragmented_control_message 263 | }. 264 | do_decode_masked_frames(_AllFramesMasked = true, Type, Frames, Acc) -> 265 | transform_frames_into_messages(Type, Frames, Acc); 266 | do_decode_masked_frames(_AllFramesMasked = false, _, _, _) -> 267 | {error, frames_unmasked}. 268 | 269 | -spec do_decode_unmasked_frames( 270 | AllFramesUnmasked :: boolean(), 271 | Type :: message_type(), 272 | Frames :: list(#frame{}), 273 | Acc :: list() 274 | ) -> 275 | list(message()) | 276 | {error, 277 | frames_masked | 278 | fragmented_control_message 279 | }. 280 | do_decode_unmasked_frames(_AllFramesUnmasked = true, Type, Frames, Acc) -> 281 | transform_frames_into_messages(Type, Frames, Acc); 282 | do_decode_unmasked_frames(_AllFramesUnmasked = false, _, _, _) -> 283 | {error, frames_masked}. 284 | 285 | -spec ensure_all_frames_mask_value( 286 | Frames :: list(#frame{}), 287 | Value :: integer() 288 | ) -> boolean(). 289 | ensure_all_frames_mask_value(Frames, Value) -> 290 | lists:all( 291 | fun(#frame{ fragmented = true }) -> 292 | true; 293 | (F) -> 294 | F#frame.mask == Value 295 | end, Frames). 296 | 297 | -spec transform_frames_into_messages( 298 | Type :: message_type(), 299 | Frames :: list(#frame{}), 300 | Acc :: list(message()) 301 | ) -> 302 | list(message()) | 303 | {error, fragmented_control_message}. 304 | transform_frames_into_messages(Type, Frames, Acc) -> 305 | case process_frames(Type, Frames, Acc) of 306 | {error, Reason} -> 307 | {error, Reason}; 308 | Messages -> 309 | lists:reverse(Messages) 310 | end. 311 | 312 | -spec process_frames( 313 | Type :: message_type(), 314 | Frames :: list(#frame{}), 315 | Messages :: list(message()) 316 | ) -> 317 | list(message()) | 318 | {error, fragmented_control_message}. 319 | process_frames(_, [], Acc) -> 320 | Acc; 321 | process_frames(begin_message, Frames, Acc) -> 322 | wtf(Frames, begin_message, #message{}, Acc); 323 | process_frames(continue_message, Frames, [FramgmentedMessage | Acc]) -> 324 | wtf(Frames, continue_message, FramgmentedMessage, Acc). 325 | 326 | wtf([Frame | Frames], Type, XMessage, Acc) -> 327 | case process_frame(Frame, Type, XMessage) of 328 | {error, Reason} -> 329 | {error, Reason}; 330 | {fragmented, Message} -> 331 | process_frames(continue_message, Frames, [Message#message{type = fragmented} | Acc]); 332 | {completed, Message} -> 333 | process_frames(begin_message, Frames, [Message | Acc]) 334 | end. 335 | 336 | -spec process_frame( 337 | Frame :: #frame{}, 338 | MessageType :: message_type(), 339 | Message :: message() 340 | ) -> 341 | {fragmented | completed, message()} | 342 | {error, fragmented_control_message}. 343 | process_frame(Frame, MessageType, Message) -> 344 | process_frame(contextualize_frame(Frame), MessageType, Frame, Message). 345 | 346 | -spec process_frame( 347 | FrameType :: atom(), 348 | MessageType :: message_type(), 349 | Frame :: #frame{}, 350 | Message :: message() 351 | ) -> 352 | {frame | completed, message()} | 353 | {error, fragmented_control_message}. 354 | process_frame(control_fragment, _ ,_, _) -> 355 | {error, fragmented_control_message}; 356 | process_frame(open_close, _, Frame, Message) -> 357 | frame_to_complete_message(Frame, Message); 358 | process_frame(open_continue, _, Frame, Message) -> 359 | frame_for_fragmented_message(Frame, Message); 360 | process_frame(continue, continue_message, Frame, Message) -> 361 | frame_for_fragmented_message(Frame, Message); 362 | process_frame(continue_close, continue_message, Frame, Message) -> 363 | frame_to_complete_message(Frame, Message); 364 | process_frame(fragmented_frame, _, Frame, Message) -> 365 | frame_for_fragmented_message(Frame, Message). 366 | 367 | frame_to_complete_message(Frame, Message) -> 368 | UpdatedMessage = append_frame_to_message(Frame, Message), 369 | BuiltMessage = build_message(UpdatedMessage, lists:reverse(UpdatedMessage#message.frames)), 370 | {completed, BuiltMessage}. 371 | frame_for_fragmented_message(Frame, Message) -> 372 | {fragmented, append_frame_to_message(Frame, Message)}. 373 | 374 | append_frame_to_message(Frame, Message) -> 375 | Frames = Message#message.frames, 376 | Message#message{frames = [Frame | Frames]}. 377 | 378 | -spec contextualize_frame( 379 | Frame :: #frame{}) 380 | -> 381 | continue_close | 382 | open_continue | 383 | continue | 384 | open_close | 385 | control_fragment | 386 | fragmented_frame. 387 | contextualize_frame(#frame{ fragmented = true }) -> 388 | fragmented_frame; 389 | contextualize_frame(Frame) -> 390 | case {Frame#frame.fin, Frame#frame.opcode} of 391 | {1, 0} -> continue_close; 392 | {0, 0} -> continue; 393 | {0, Opcode} when Opcode == 8; Opcode == 9; Opcode == 10 -> control_fragment; 394 | {1, _} -> open_close; 395 | {0, _} -> open_continue 396 | end. 397 | 398 | -spec build_message( 399 | Message :: message(), 400 | Frames :: list(#frame{}) 401 | ) -> message(). 402 | build_message(Message, Frames) -> 403 | [HeadFrame | _] = Frames, 404 | 405 | case HeadFrame#frame.opcode of 406 | 1 -> 407 | Payload = build_payload_from_frames(text, Frames), 408 | Message#message{type = text, payload = Payload}; 409 | 2 -> 410 | Payload = build_payload_from_frames(binary, Frames), 411 | Message#message{type = binary, payload = Payload}; 412 | 8 -> 413 | Payload = build_payload_from_frames(close, Frames), 414 | Message#message{type = close, payload = Payload}; 415 | 9 -> 416 | Payload = build_payload_from_frames(text, Frames), 417 | Message#message{type = ping, payload = Payload}; 418 | 10 -> 419 | Payload = build_payload_from_frames(text, Frames), 420 | Message#message{type = pong, payload = Payload} 421 | end. 422 | 423 | -spec build_payload_from_frames( 424 | Type :: close | binary | text, 425 | Frames :: list(#frame{}) 426 | ) -> payload(). 427 | build_payload_from_frames(close, [Frame]) -> 428 | case Frame#frame.payload of 429 | <<>> -> {undefined, undefined}; 430 | <> -> {Status, binary_to_list(Reason)} 431 | end; 432 | build_payload_from_frames(binary, Frames) -> 433 | concatenate_payload_from_frames(Frames); 434 | build_payload_from_frames(text, Frames) -> 435 | Payload = concatenate_payload_from_frames(Frames), 436 | binary_to_list(Payload). 437 | 438 | -spec concatenate_payload_from_frames( 439 | Frames :: list(#frame{}) 440 | ) -> binary(). 441 | concatenate_payload_from_frames(Frames) -> 442 | concatenate_payload_from_frames(Frames, <<>>). 443 | 444 | -spec concatenate_payload_from_frames( 445 | Frames :: list(#frame{}), 446 | Acc :: binary() 447 | ) -> binary(). 448 | concatenate_payload_from_frames([], Acc) -> 449 | Acc; 450 | concatenate_payload_from_frames([Frame | Rest], Acc) -> 451 | concatenate_payload_from_frames(Rest, <>). 452 | -------------------------------------------------------------------------------- /test/spec/wsock_message_spec.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | -module(wsock_message_spec). 16 | -include_lib("espec/include/espec.hrl"). 17 | -include_lib("hamcrest/include/hamcrest.hrl"). 18 | -include("wsock.hrl"). 19 | 20 | -define(FRAGMENT_SIZE, 4096). 21 | 22 | spec() -> 23 | describe("encode", fun() -> 24 | before_each(fun()-> 25 | meck:new(wsock_framing, [passthrough]) 26 | end), 27 | 28 | after_each(fun()-> 29 | meck:unload(wsock_framing) 30 | end), 31 | 32 | it("should return an error if no datatype option is given", fun() -> 33 | Return = wsock_message:encode("motosicleta man", []), 34 | assert_that(Return, is({error, missing_datatype})) 35 | end), 36 | it("should mask data if 'mask' option is present", fun() -> 37 | wsock_message:encode("asdasda", [text ,mask]), 38 | [_Data, Options] = meck_arguments(wsock_framing, frame), 39 | assert_that(proplists:get_value(mask, Options), is(true)) 40 | end), 41 | it("should not mask data if 'mask' option is not present", fun() -> 42 | wsock_message:encode("frotisfrotis", [text]), 43 | [_Data, Options] = meck_arguments(wsock_framing, frame), 44 | assert_that(proplists:get_value(mask, Options), is(undefined)) 45 | end), 46 | it("should set opcode to 'text' if type is text", fun() -> 47 | wsock_message:encode("asadsd", [text]), 48 | [_Data, Options] = meck_arguments(wsock_framing, frame), 49 | assert_that(proplists:get_value(opcode, Options), is(text)) 50 | end), 51 | it("should set opcode to 'binary' if type is binary", fun() -> 52 | wsock_message:encode(<<"asdasd">>, [binary]), 53 | [_Data, Options] = meck_arguments(wsock_framing, frame), 54 | assert_that(proplists:get_value(opcode, Options), is(binary)) 55 | end), 56 | describe("when payload size is <= fragment size", fun()-> 57 | it("should return a list with only one binary fragment", fun()-> 58 | Data = "Foo bar", 59 | [BinFrame | []] = wsock_message:encode(Data, [text]), 60 | assert_that(byte_size(list_to_binary(Data)),is(less_than(?FRAGMENT_SIZE))), 61 | assert_that(is_binary(BinFrame), is(true)), 62 | assert_that(meck:called(wsock_framing, to_binary, '_'), is(true)), 63 | assert_that(meck:called(wsock_framing, frame, '_'), is(true)) 64 | end), 65 | it("should set opcode to 'type'", fun() -> 66 | Data = "Foo bar", 67 | [Frame] = wsock_message:encode(Data, [text]), 68 | 69 | <<_:4, Opcode:4, _/binary>> = Frame, 70 | 71 | assert_that(Opcode, is(1)) 72 | end), 73 | it("should set fin", fun()-> 74 | Data = "Foo bar", 75 | [Frame] = wsock_message:encode(Data, [text]), 76 | 77 | <> = Frame, 78 | 79 | assert_that(Fin, is(1)) 80 | end) 81 | end), 82 | describe("when payload size is > fragment size", fun() -> 83 | it("should return a list of binary fragments", fun()-> 84 | Data = crypto:rand_bytes(5000), 85 | Frames = wsock_message:encode(Data, [binary]), 86 | assert_that(meck:called(wsock_framing, to_binary, '_'), is(true)), 87 | assert_that(meck:called(wsock_framing, frame, '_'), is(true)), 88 | assert_that(length(Frames), is(2)) 89 | end), 90 | it("should set a payload of 4096 bytes or less on each fragment", fun() -> 91 | Data = crypto:rand_bytes(?FRAGMENT_SIZE*3), 92 | Frames = wsock_message:encode(Data, [binary]), 93 | 94 | [Frame1, Frame2, Frame3] = Frames, 95 | 96 | <<_:32, Payload1/binary>> = Frame1, 97 | <<_:32, Payload2/binary>> = Frame2, 98 | <<_:32, Payload3/binary>> = Frame3, 99 | 100 | assert_that(byte_size(Payload1), is(?FRAGMENT_SIZE)), 101 | assert_that(byte_size(Payload2), is(?FRAGMENT_SIZE)), 102 | assert_that(byte_size(Payload3), is(?FRAGMENT_SIZE)) 103 | end), 104 | it("should set opcode to 'type' on the first fragment", fun()-> 105 | Data = crypto:rand_bytes(5000), 106 | Frames = wsock_message:encode(Data, [binary]), 107 | 108 | [FirstFragment | _ ] = Frames, 109 | 110 | <<_:4, Opcode:4, _/binary>> = FirstFragment, 111 | 112 | assert_that(Opcode, is(2)) 113 | end), 114 | it("should unset fin on all fragments but last", fun() -> 115 | Data = crypto:rand_bytes(12288), %4096 * 3 116 | Frames = wsock_message:encode(Data, [binary]), 117 | 118 | [Frame1, Frame2, Frame3] = Frames, 119 | 120 | <> = Frame1, 121 | <> = Frame2, 122 | <> = Frame3, 123 | 124 | assert_that(Fin1, is(0)), 125 | assert_that(Fin2, is(0)), 126 | assert_that(Fin3, is(1)) 127 | end), 128 | it("should set opcode to 'continuation' on all fragments but first", fun() -> 129 | Data = crypto:rand_bytes(12288), %4096 * 3 130 | Frames = wsock_message:encode(Data, [binary]), 131 | 132 | [Frame1, Frame2, Frame3] = Frames, 133 | 134 | <<_:4, Opcode1:4, _/binary>> = Frame1, 135 | <<_:4, Opcode2:4, _/binary>> = Frame2, 136 | <<_:4, Opcode3:4, _/binary>> = Frame3, 137 | 138 | assert_that(Opcode1, is(2)), 139 | assert_that(Opcode2, is(0)), 140 | assert_that(Opcode3, is(0)) 141 | end) 142 | end), 143 | describe("control messages", fun() -> 144 | describe("close", fun() -> 145 | it("should return a list of one frame", fun() -> 146 | [_Frame] = wsock_message:encode([], [close]) 147 | end), 148 | it("should return a close frame", fun() -> 149 | [Frame] = wsock_message:encode([], [close]), 150 | 151 | <> = Frame, 152 | 153 | assert_that(Fin, is(1)), 154 | assert_that(Rsv, is(0)), 155 | assert_that(Opcode, is(8)) 156 | end), 157 | it("should attach application payload", fun() -> 158 | [Frame] = wsock_message:encode({1004, "Chapando el garito"}, [mask, close]), 159 | 160 | <<_Fin:1, _Rsv:3, _Opcode:4, 1:1, _PayloadLen:7, _Mask:32, _Payload/binary>> = Frame 161 | end) 162 | end), 163 | describe("ping", fun() -> 164 | it("should return a list of one frame", fun() -> 165 | [_Frame] = wsock_message:encode([], [ping]) 166 | end), 167 | it("should return a ping frame", fun() -> 168 | [Frame] = wsock_message:encode([], [ping]), 169 | 170 | <> = Frame, 171 | 172 | assert_that(Fin, is(1)), 173 | assert_that(Rsv, is(0)), 174 | assert_that(Opcode, is(9)) 175 | end), 176 | it("should attach application payload", fun() -> 177 | [Frame] = wsock_message:encode("1234", [mask, ping]), 178 | 179 | <<_Fin:1, _Rsv:3, _Opcode:4, 1:1, 4:7, _Mask:32, _Payload:4/binary>> = Frame 180 | end) 181 | end), 182 | describe("pong", fun() -> 183 | it("should return a list of one frame", fun() -> 184 | [_Frame] = wsock_message:encode([], [pong]) 185 | end), 186 | it("should return a ping frame", fun() -> 187 | [Frame] = wsock_message:encode([], [pong]), 188 | 189 | <> = Frame, 190 | 191 | assert_that(Fin, is(1)), 192 | assert_that(Rsv, is(0)), 193 | assert_that(Opcode, is(10)) 194 | end), 195 | it("should attach application payload", fun() -> 196 | [Frame] = wsock_message:encode("1234", [mask, pong]), 197 | 198 | <<_Fin:1, _Rsv:3, _Opcode:4, 1:1, 4:7, _Mask:32, _Payload:4/binary>> = Frame 199 | end) 200 | end) 201 | end) 202 | end), 203 | describe("decode", fun()-> 204 | it("should return an error if unexpected masking", fun() -> 205 | Payload = crypto:rand_bytes(20), 206 | Frame = get_binary_frame(0, 0, 0, 0, 2, 1, 20, 0, Payload), 207 | 208 | Response = wsock_message:decode(Frame, []), 209 | 210 | assert_that(Response, is({error, frames_masked})) 211 | end), 212 | it("should return an error if expected masking", fun() -> 213 | Payload = crypto:rand_bytes(20), 214 | Frame = get_binary_frame(0, 0, 0, 0, 2, 0, 20, 0, Payload), 215 | 216 | Response = wsock_message:decode(Frame, [masked]), 217 | 218 | assert_that(Response, is({error, frames_unmasked})) 219 | end), 220 | it("should decode masked messages", fun() -> 221 | Payload = crypto:rand_bytes(20), 222 | Fragment = get_binary_frame(0, 0, 0, 0, 2, 1, 20, 0, Payload), 223 | 224 | [_Message] = wsock_message:decode(Fragment, [masked]) 225 | end), 226 | it("should decode unmasked messages", fun() -> 227 | Payload = crypto:rand_bytes(20), 228 | Frame = get_binary_frame(0, 0, 0, 0, 2, 0, 20, 0, Payload), 229 | 230 | [_Message] = wsock_message:decode(Frame, []) 231 | end), 232 | describe("fragmented messages", fun() -> 233 | %describe("when they are control messages", ) 234 | it("should complain when control messages are fragmented", fun() -> 235 | Data = crypto:rand_bytes(10), 236 | Frame1 = get_binary_frame(0, 0, 0, 0, 8, 0, 10, 0, Data), 237 | Frame2 = get_binary_frame(1, 0, 0, 0, 0, 0, 10, 0, Data), 238 | 239 | Message = <>, 240 | 241 | Return = wsock_message:decode(Message, []), 242 | 243 | assert_that(Return, is({error, fragmented_control_message})) 244 | end), 245 | it("should return a fragmented message with undefined payload when message is not complete", fun() -> 246 | Payload = crypto:rand_bytes(20), 247 | << 248 | Payload1:10/binary, 249 | Payload2:5/binary, 250 | _Payload3/binary 251 | >> = Payload, 252 | 253 | FakeFragment1 = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, Payload1), 254 | FakeFragment2 = get_binary_frame(0, 0, 0, 0, 0, 0, 5, 0, Payload2), 255 | 256 | Data = <>, 257 | 258 | [Message] = wsock_message:decode(Data, []), 259 | 260 | assert_that(Message#message.type, is(fragmented)), 261 | assert_that(length(Message#message.frames), is(2)) 262 | end), 263 | it("should decode data containing a complete fragmented binary message", fun() -> 264 | Payload = crypto:rand_bytes(40), 265 | << 266 | Payload1:10/binary, 267 | Payload2:10/binary, 268 | Payload3:10/binary, 269 | Payload4:10/binary 270 | >> = Payload, 271 | 272 | FakeFragment1 = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, Payload1), 273 | FakeFragment2 = get_binary_frame(0, 0, 0, 0, 0, 0, 10, 0, Payload2), 274 | FakeFragment3 = get_binary_frame(0, 0, 0, 0, 0, 0, 10, 0, Payload3), 275 | FakeFragment4 = get_binary_frame(1, 0, 0, 0, 0, 0, 10, 0, Payload4), 276 | 277 | Data = << FakeFragment1/binary, FakeFragment2/binary, FakeFragment3/binary, FakeFragment4/binary>>, 278 | 279 | [Message] = wsock_message:decode(Data, []), 280 | 281 | assert_that(Message#message.type, is(binary)), 282 | assert_that(Message#message.payload, is(Payload)) 283 | end), 284 | it("should decode data containing a complete fragmented text message", fun() -> 285 | Text = "asasdasdasdasdasdasdasdasdasdasdasdasdasdasdasd", 286 | Payload = list_to_binary(Text), 287 | << 288 | Payload1:5/binary, 289 | Payload2:2/binary, 290 | Payload3/binary 291 | >> = Payload, 292 | 293 | FakeFragment1 = get_binary_frame(0, 0, 0, 0, 1, 0, byte_size(Payload1), 0, Payload1), 294 | FakeFragment2 = get_binary_frame(0, 0, 0, 0, 0, 0, byte_size(Payload2), 0, Payload2), 295 | FakeFragment3 = get_binary_frame(1, 0, 0, 0, 0, 0, byte_size(Payload3), 0, Payload3), 296 | 297 | Data = << FakeFragment1/binary, FakeFragment2/binary, FakeFragment3/binary>>, 298 | 299 | [Message] = wsock_message:decode(Data, []), 300 | 301 | assert_that(Message#message.type, is(text)), 302 | assert_that(Message#message.payload, is(Text)) 303 | end), 304 | it("should complete a fragmented message", fun() -> 305 | Payload = crypto:rand_bytes(20), 306 | << 307 | Payload1:10/binary, 308 | Payload2:5/binary, 309 | Payload3/binary 310 | >> = Payload, 311 | 312 | FakeFragment1 = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, Payload1), 313 | FakeFragment2 = get_binary_frame(0, 0, 0, 0, 0, 0, 5, 0, Payload2), 314 | FakeFragment3 = get_binary_frame(1, 0, 0, 0, 0, 0, 5, 0, Payload3), 315 | 316 | 317 | Data1 = <>, 318 | Data2 = <>, 319 | 320 | [Message1] = wsock_message:decode(Data1, []), 321 | [Message2] = wsock_message:decode(Data2, Message1, []), 322 | 323 | assert_that(Message1#message.type, is(fragmented)), 324 | assert_that(Message2#message.type, is(binary)), 325 | assert_that(Message2#message.payload, is(Payload)) 326 | end), 327 | it("should decode data with complete fragmented messages and part of fragmented one", fun() -> 328 | BinPayload1 = crypto:rand_bytes(30), 329 | << 330 | Payload1:10/binary, 331 | Payload2:10/binary, 332 | Payload3/binary 333 | >> = BinPayload1, 334 | 335 | FakeFragment1 = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, Payload1), 336 | FakeFragment2 = get_binary_frame(0, 0, 0, 0, 0, 0, 10, 0, Payload2), 337 | FakeFragment3 = get_binary_frame(1, 0, 0, 0, 0, 0, 10, 0, Payload3), 338 | 339 | BinPayload2 = crypto:rand_bytes(10), 340 | FakeFragment4 = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, BinPayload2), 341 | 342 | Data = << FakeFragment1/binary, FakeFragment2/binary, FakeFragment3/binary, FakeFragment4/binary>>, 343 | 344 | [Message1, Message2] = wsock_message:decode(Data, []), 345 | 346 | assert_that(Message1#message.type, is(binary)), 347 | assert_that(Message1#message.payload, is(BinPayload1)), 348 | assert_that(length(Message1#message.frames), is(3)), 349 | assert_that(Message2#message.type, is(fragmented)), 350 | assert_that(length(Message2#message.frames), is(1)) 351 | end), 352 | describe("fragmented frames", fun() -> 353 | it("should return a fragmented message", fun() -> 354 | FakeFrame = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, crypto:rand_bytes(10)), 355 | 356 | <> = FakeFrame, 357 | [Message] = wsock_message:decode(Data, []), 358 | 359 | assert_that(Message#message.type, is(fragmented)), 360 | assert_that(length(Message#message.frames), is(1)) 361 | end), 362 | it("should return a fragmented message that is made up of more that one frame", fun() -> 363 | Payload = crypto:rand_bytes(10), 364 | FakeFrame = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, Payload), 365 | 366 | <> = FakeFrame, 367 | [FragmentedMessage] = wsock_message:decode(FirstFragment, []), 368 | [Message] = wsock_message:decode(SecondFragment, FragmentedMessage, []), 369 | 370 | assert_that(Message#message.type, is(fragmented)), 371 | assert_that(length(Message#message.frames), is(1)) 372 | end), 373 | it("should complete a fragmented message that is made up of one frame", fun() -> 374 | Payload = crypto:rand_bytes(10), 375 | FakeFrame = get_binary_frame(1, 0, 0, 0, 2, 0, 10, 0, Payload), 376 | 377 | <> = FakeFrame, 378 | [FragmentedMessage] = wsock_message:decode(FirstFragment, []), 379 | [Message] = wsock_message:decode(SecondFragment, FragmentedMessage, []), 380 | 381 | assert_that(Message#message.type, is(binary)), 382 | assert_that(length(Message#message.frames), is(1)), 383 | assert_that(Message#message.payload, is(Payload)) 384 | end), 385 | it("should complete a fragmented message that is made up of more than one frame", fun() -> 386 | Data = crypto:rand_bytes(20), 387 | <> = Data, 388 | FakeFrame1 = get_binary_frame(0, 0, 0, 0, 2, 0, 10, 0, DataFrame1), 389 | FakeFrame2 = get_binary_frame(1, 0, 0, 0, 0, 0, 10, 0, DataFrame2), 390 | 391 | <> = FakeFrame2, 392 | 393 | 394 | InputData = <>, 395 | [FragmentedMessage] = wsock_message:decode(InputData, []), 396 | [Message] = wsock_message:decode(FakeFrameFragment2, FragmentedMessage, []), 397 | 398 | assert_that(Message#message.type, is(binary)), 399 | assert_that(length(Message#message.frames), is(2)), 400 | assert_that(Message#message.payload, is(Data)) 401 | end) 402 | end) 403 | 404 | end), 405 | describe("unfragmented messages", fun()-> 406 | it("should decode data containing various text messages", fun()-> 407 | Text1 = "Churras churras", 408 | Payload1 = list_to_binary(Text1), 409 | PayloadLength1 = byte_size(Payload1), 410 | 411 | Text2 = "Pitas pitas", 412 | Payload2 = list_to_binary(Text2), 413 | PayloadLength2 = byte_size(Payload2), 414 | 415 | Text3 = "Pero que jallo eh", 416 | Payload3 = list_to_binary(Text3), 417 | PayloadLength3 = byte_size(Payload3), 418 | 419 | FakeMessage1 = get_binary_frame(1, 0, 0, 0, 1, 0, PayloadLength1, 0, Payload1), 420 | FakeMessage2 = get_binary_frame(1, 0, 0, 0, 1, 0, PayloadLength2, 0, Payload2), 421 | FakeMessage3 = get_binary_frame(1, 0, 0, 0, 1, 0, PayloadLength3, 0, Payload3), 422 | 423 | Data = << FakeMessage1/binary, FakeMessage2/binary, FakeMessage3/binary>>, 424 | 425 | [Message1, Message2, Message3] = wsock_message:decode(Data, []), 426 | 427 | assert_that(Message1#message.type, is(text)), 428 | assert_that(Message1#message.payload, is(Text1)), 429 | assert_that(Message2#message.type, is(text)), 430 | assert_that(Message2#message.payload, is(Text2)), 431 | assert_that(Message3#message.type, is(text)), 432 | assert_that(Message3#message.payload, is(Text3)) 433 | end), 434 | it("should decode data containing text and binary messages", fun()-> 435 | Text1 = "Churras churras", 436 | Payload1 = list_to_binary(Text1), 437 | PayloadLength1 = byte_size(Payload1), 438 | 439 | Payload2 = crypto:rand_bytes(20), 440 | PayloadLength2 = 20, 441 | 442 | Text3 = "Pero que jallo eh", 443 | Payload3 = list_to_binary(Text3), 444 | PayloadLength3 = byte_size(Payload3), 445 | 446 | FakeMessage1 = get_binary_frame(1, 0, 0, 0, 1, 0, PayloadLength1, 0, Payload1), 447 | FakeMessage2 = get_binary_frame(1, 0, 0, 0, 2, 0, PayloadLength2, 0, Payload2), 448 | FakeMessage3 = get_binary_frame(1, 0, 0, 0, 1, 0, PayloadLength3, 0, Payload3), 449 | 450 | Data = << FakeMessage1/binary, FakeMessage2/binary, FakeMessage3/binary>>, 451 | 452 | [Message1, Message2, Message3] = wsock_message:decode(Data, []), 453 | 454 | assert_that(Message1#message.type, is(text)), 455 | assert_that(Message1#message.payload, is(Text1)), 456 | assert_that(Message2#message.type, is(binary)), 457 | assert_that(Message2#message.payload, is(Payload2)), 458 | assert_that(Message3#message.type, is(text)), 459 | assert_that(Message3#message.payload, is(Text3)) 460 | end), 461 | it("should decode data containing all message types"), 462 | it("should decode data containing a binary message", fun() -> 463 | Payload = crypto:rand_bytes(45), 464 | %") 465 | FakeMessage = get_binary_frame(1, 0, 0, 0, 2, 0, 45, 0, Payload), 466 | [Message] = wsock_message:decode(FakeMessage, []), 467 | 468 | assert_that( Message#message.payload, is(Payload)) 469 | end), 470 | it("should decode data containing a text message", fun() -> 471 | Payload = "Iepa yei!", 472 | PayloadLength = length(Payload), 473 | PayloadData = list_to_binary(Payload), 474 | 475 | FakeMessage = get_binary_frame(1, 0, 0, 0, 1, 0, PayloadLength, 0, PayloadData), 476 | [Message] = wsock_message:decode(FakeMessage, []), 477 | 478 | assert_that( Message#message.payload, is(Payload)) 479 | end), 480 | describe("control frames", fun() -> 481 | describe("ping", fun() -> 482 | it("should return a message with type ping", fun() -> 483 | FakeMessage = get_binary_frame(1, 0, 0, 0, 9, 0, 0, 0, <<>>), 484 | 485 | [Message] = wsock_message:decode(FakeMessage, []), 486 | 487 | assert_that(Message#message.type, is(ping)) 488 | end) 489 | end), 490 | describe("pong", fun() -> 491 | it("should return a message with type pong", fun() -> 492 | FakeMessage = get_binary_frame(1, 0, 0, 0, 10, 0, 0, 0, <<>>), 493 | 494 | [Message] = wsock_message:decode(FakeMessage, []), 495 | 496 | assert_that(Message#message.type, is(pong)) 497 | end) 498 | end), 499 | describe("close", fun() -> 500 | it("should return a message with type close", fun() -> 501 | FakeMessage = get_binary_frame(1, 0, 0, 0, 8, 0, 0, 0, <<>>), 502 | 503 | [Message] = wsock_message:decode(FakeMessage, []), 504 | 505 | assert_that(Message#message.type, is(close)) 506 | end), 507 | describe("with payload", fun() -> 508 | it("should return the payload a a tuple {Status, Reason}", fun()-> 509 | Status = 1004, 510 | Reason = list_to_binary("A tomar por saco"), 511 | Payload = <>, 512 | PayloadLen = byte_size(Payload), 513 | FakeMessage = get_binary_frame(1, 0, 0, 0, 8, 0, PayloadLen, 0, Payload), 514 | 515 | [Message] = wsock_message:decode(FakeMessage, []), 516 | {St, Re} = Message#message.payload, 517 | 518 | assert_that(St, is(Status)), 519 | assert_that(Re, is("A tomar por saco")) 520 | end) 521 | end), 522 | describe("without payload", fun() -> 523 | it("should return the payload as a tuple {undefined, undefined}", fun() -> 524 | FakeMessage = get_binary_frame(1, 0, 0, 0, 8, 0, 0, 0, <<>>), 525 | 526 | [Message] = wsock_message:decode(FakeMessage, []), 527 | {Status, Reason} = Message#message.payload, 528 | 529 | assert_that(Status, is(undefined)), 530 | assert_that(Reason, is(undefined)) 531 | end) 532 | end) 533 | end) 534 | end) 535 | end) 536 | end). 537 | 538 | get_binary_frame(Fin, Rsv1, Rsv2, Rsv3, Opcode, Mask, Length, ExtendedPayloadLength, Payload) -> 539 | Head = <>, 540 | TempBin = case Length of 541 | 126 -> 542 | <>; 543 | 127 -> 544 | <>; 545 | _ -> 546 | <> 547 | end, 548 | 549 | case Mask of 550 | 0 -> 551 | <>; 552 | 1 -> 553 | <> = crypto:rand_bytes(4), 554 | MaskedPayload = mask(Payload, Mk, <<>>), 555 | <> 556 | end. 557 | 558 | mask(<>, MaskKey, Acc) -> 559 | T = Data bxor MaskKey, 560 | mask(Rest, MaskKey, <>); 561 | 562 | mask(<< Data:24>>, MaskKey, Acc) -> 563 | <> = <>, 564 | T = Data bxor MaskKey2, 565 | <>; 566 | 567 | mask(<< Data:16>>, MaskKey, Acc) -> 568 | <> = <>, 569 | T = Data bxor MaskKey2, 570 | <>; 571 | 572 | mask(<< Data:8>>, MaskKey, Acc) -> 573 | <> = <>, 574 | T = Data bxor MaskKey2, 575 | <>; 576 | 577 | mask(<<>>, _, Acc) -> 578 | Acc. 579 | meck_arguments(Module, Function) -> 580 | History = meck:history(Module), 581 | 582 | [Args] = [ X || {_, {_, F, X}, _} <- History, F == Function], 583 | Args. 584 | -------------------------------------------------------------------------------- /test/spec/wsock_framing_spec.erl: -------------------------------------------------------------------------------- 1 | %Copyright [2012] [Farruco Sanjurjo Arcay] 2 | 3 | % Licensed under the Apache License, Version 2.0 (the "License"); 4 | % you may not use this file except in compliance with the License. 5 | % You may obtain a copy of the License at 6 | 7 | % http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | % Unless required by applicable law or agreed to in writing, software 10 | % distributed under the License is distributed on an "AS IS" BASIS, 11 | % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | % See the License for the specific language governing permissions and 13 | % limitations under the License. 14 | 15 | -module(wsock_framing_spec). 16 | -include_lib("espec/include/espec.hrl"). 17 | -include_lib("hamcrest/include/hamcrest.hrl"). 18 | -include("wsock.hrl"). 19 | %-compile([export_all]). 20 | 21 | -define(OP_CODE_CONT, 0). 22 | -define(OP_CODE_TEXT, 1). 23 | -define(OP_CODE_BIN, 2). 24 | -define(OP_CODE_CLOSE, 8). 25 | -define(OP_CODE_PING, 9). 26 | -define(OP_CODE_PONG, 10). 27 | 28 | spec() -> 29 | describe("to_binary", fun() -> 30 | describe("not payload fields", fun() -> 31 | describe("opcode", fun() -> 32 | before_all(fun() -> 33 | spec_set(validator, fun(OpcodeType, OpcodeBinType) -> 34 | Frame = wsock_framing:frame("aas", [{opcode, OpcodeType}]), 35 | <<_:4, Opcode:4, _/binary>> = wsock_framing:to_binary(Frame), 36 | assert_that(Opcode, is(OpcodeBinType)) 37 | end) 38 | end), 39 | it("should set 'opcode' to close if close frame", fun() -> 40 | (spec_get(validator))(close, ?OP_CODE_CLOSE) 41 | end), 42 | it("should set 'opcode' to pong if pong frame", fun() -> 43 | (spec_get(validator))(pong, ?OP_CODE_PONG) 44 | end), 45 | it("should set 'opcode' to ping if ping frame", fun() -> 46 | (spec_get(validator))(ping, ?OP_CODE_PING) 47 | end), 48 | it("should set 'opcode' to continuation if continuation frame", fun() -> 49 | (spec_get(validator))(continuation, ?OP_CODE_CONT) 50 | end), 51 | it("should set 'opcode' to binary if binary frame", fun() -> 52 | (spec_get(validator))(binary, ?OP_CODE_BIN) 53 | end), 54 | it("should set 'opcode' to text if text frame", fun() -> 55 | (spec_get(validator))(text, ?OP_CODE_TEXT) 56 | end) 57 | end), 58 | describe("mask", fun() -> 59 | before_all(fun() -> 60 | spec_set(validator, fun(Options, ExpectedMask) -> 61 | Frame = wsock_framing:frame("asas", Options), 62 | BinFrame = wsock_framing:to_binary(Frame), 63 | 64 | <<_:8, Mask:1, _:7, _/binary>> = BinFrame, 65 | 66 | assert_that(Mask, is(ExpectedMask)) 67 | end) 68 | end), 69 | it("should set 'mask' to 0 if unmasked data", fun() -> 70 | (spec_get(validator))([{opcode, text}], 0) 71 | end), 72 | it("should set 'mask' to 1 if masked data", fun() -> 73 | (spec_get(validator))([{opcode, text}, mask], 1) 74 | end) 75 | end), 76 | describe("mask key", fun() -> 77 | it("should include masking key when 'mask' option is set", fun() -> 78 | Frame = wsock_framing:frame("pesto", [mask, {opcode, text}]), 79 | BinFrame = wsock_framing:to_binary(Frame), 80 | 81 | <<_:16, MaskKey:32, _/binary>> = BinFrame, 82 | 83 | assert_that(MaskKey, is(Frame#frame.masking_key)) 84 | end), 85 | it("should not include masking key when 'mask' option is not set", fun() -> 86 | Frame = wsock_framing:frame("conasa", [{opcode, text}]), 87 | BinFrame = wsock_framing:to_binary(Frame), 88 | 89 | <<_:16, Payload/binary>> = BinFrame, 90 | 91 | assert_that(Payload, is(Frame#frame.payload)) 92 | end) 93 | end), 94 | describe("fin", fun() -> 95 | before_all(fun() -> 96 | spec_set(validator, fun(Options, Expected) -> 97 | Frame = wsock_framing:frame("asas", Options), 98 | BinFrame = wsock_framing:to_binary(Frame), 99 | 100 | <> = BinFrame, 101 | 102 | assert_that(Fin, is(Expected)) 103 | end) 104 | end), 105 | it("should unset 'fin' bit if 'fin' option is not set", fun() -> 106 | (spec_get(validator))([{opcode, text}], 0) 107 | end), 108 | it("should set 'fin' bit if 'fin' option is set", fun() -> 109 | (spec_get(validator))([mask, {opcode, text}], 0) 110 | end) 111 | end), 112 | describe("rsv", fun() -> 113 | it("should set all 3 rsv bits to 0", fun() -> 114 | Frame = wsock_framing:frame("asasda", [{opcode, text}]), 115 | BinFrame = wsock_framing:to_binary(Frame), 116 | 117 | <<_:1, Rsv:3, _/bits>> = BinFrame, 118 | 119 | assert_that(Rsv, is(0)) 120 | end) 121 | end), 122 | describe("payload length", fun()-> 123 | it("should set payload length of data with <= 125 bytes", fun() -> 124 | Frame = wsock_framing:frame("asdasd", [{opcode, text}]), 125 | BinFrame = wsock_framing:to_binary(Frame), 126 | 127 | <<_:9, PayloadLen:7, _/binary>> = BinFrame, 128 | 129 | assert_that(PayloadLen, is(Frame#frame.payload_len)) 130 | end), 131 | it("should set extended payload length of data with > 125 and <= 65536 bytes", fun() -> 132 | Frame = wsock_framing:frame(crypto:rand_bytes(300), [{opcode, binary}]), 133 | BinFrame = wsock_framing:to_binary(Frame), 134 | 135 | <<_:9, PayloadLen:7, ExtendedPLen:16, _/binary>> = BinFrame, 136 | 137 | assert_that(PayloadLen, is(Frame#frame.payload_len)), 138 | assert_that(ExtendedPLen, is(Frame#frame.extended_payload_len)) 139 | end), 140 | it("should set extended payload length cont. of data with > 65536 bytes ", fun() -> 141 | Frame = wsock_framing:frame(crypto:rand_bytes(70000), [{opcode, binary}]), 142 | BinFrame = wsock_framing:to_binary(Frame), 143 | 144 | <<_:9, PayloadLen:7, ExtendedPLen:64, _/binary>> = BinFrame, 145 | 146 | assert_that(PayloadLen, is(Frame#frame.payload_len)), 147 | assert_that(ExtendedPLen, is(Frame#frame.extended_payload_len_cont)) 148 | end) 149 | end) 150 | end), 151 | describe("payload", fun()-> 152 | before_all(fun() -> 153 | spec_set(validator, fun(Size, Options, Callback) -> 154 | Frame = wsock_framing:frame(crypto:rand_bytes(Size), Options), 155 | BinFrame = wsock_framing:to_binary(Frame), 156 | Payload = Callback(BinFrame), 157 | assert_that(Payload, is(Frame#frame.payload)) 158 | end) 159 | end), 160 | describe("length <= 125 bytes", fun() -> 161 | it("should set unmasked data", fun() -> 162 | (spec_get(validator))(100, [{opcode, text}], fun(<<_:16, Payload/binary>>) -> 163 | Payload 164 | end) 165 | end), 166 | it("should set masked data", fun()-> 167 | (spec_get(validator))(100, [mask, {opcode, text}], fun(<<_:48, Payload/binary>>) -> 168 | Payload 169 | end) 170 | end) 171 | end), 172 | describe("length 125 > and <= 65536 bytes", fun() -> 173 | it("should set unmasked data", fun() -> 174 | (spec_get(validator))(300, [{opcode, text}], fun(<<_:32, Payload/binary>>) -> 175 | Payload 176 | end) 177 | end), 178 | it("should set masked data", fun()-> 179 | (spec_get(validator))(300, [mask, {opcode, text}], fun(<<_:64, Payload/binary>>) -> 180 | Payload 181 | end) 182 | end) 183 | end), 184 | describe("length > 65536 bytes", fun() -> 185 | it("should set unmasked data", fun() -> 186 | (spec_get(validator))(70000, [{opcode, text}], fun(<<_:80, Payload/binary>>) -> 187 | Payload 188 | end) 189 | end), 190 | it("should set masked data", fun()-> 191 | (spec_get(validator))(70000, [mask, {opcode, text}], fun(<<_:112, Payload/binary>>) -> 192 | Payload 193 | end) 194 | end) 195 | end) 196 | end) 197 | end), 198 | describe("from_binary", fun() -> 199 | before_all(fun() -> 200 | spec_set(frame_builder, fun(Fin, Rsv, Opcode, Mask, Data) -> 201 | ByteSize = byte_size(Data), 202 | DataLen = case ByteSize of 203 | X when X =< 125 -> X; 204 | X when X =< 65536 -> 126; 205 | X when X > 65536 -> 127 206 | end, 207 | BinFrame = get_binary_frame(Fin, Rsv, Rsv, Rsv, Opcode, Mask, DataLen, ByteSize, Data), 208 | [Frame] = wsock_framing:from_binary(BinFrame), 209 | {BinFrame, Frame} 210 | end) 211 | end), 212 | describe("non payload fields", fun() -> 213 | describe("fin", fun() -> 214 | it("should set fin property to fin bit", fun() -> 215 | Data = crypto:rand_bytes(20), 216 | DataLen = byte_size(Data), 217 | 218 | 219 | BinFrame = get_binary_frame(1, 0, 0, 0, 1, 0, DataLen, 0, Data), 220 | [Frame] = wsock_framing:from_binary(BinFrame), 221 | assert_that(Frame#frame.fin, is(1)) 222 | end) 223 | end), 224 | describe("rsv", fun() -> 225 | before_all(fun() -> 226 | spec_set(generator, fun(Rsv1, Rsv2, Rsv3)-> 227 | Data = crypto:rand_bytes(20), 228 | DataLen = byte_size(Data), 229 | 230 | BinFrame = get_binary_frame(1, Rsv1, Rsv2, Rsv3, 1, 0, DataLen, 0, Data), 231 | [Frame] = wsock_framing:from_binary(BinFrame), 232 | Frame 233 | end) 234 | end), 235 | it("should set rsv1 to rsv1 bit", fun() -> 236 | Frame = (spec_get(generator))(0, 0, 0), 237 | assert_that(Frame#frame.rsv1, is(0)) 238 | end), 239 | it("should set rsv2 to rsv2 bit", fun() -> 240 | Frame = (spec_get(generator))(0, 0, 0), 241 | assert_that(Frame#frame.rsv2, is(0)) 242 | end), 243 | it("should set rsv3 to rsv3 bit", fun() -> 244 | Frame = (spec_get(generator))(0, 0, 0), 245 | assert_that(Frame#frame.rsv3, is(0)) 246 | end) 247 | end), 248 | describe("opcode", fun() -> 249 | before_all(fun() -> 250 | spec_set(validator, fun(OpCode) -> 251 | Data = crypto:rand_bytes(20), 252 | DataLen = byte_size(Data), 253 | BinFrame = get_binary_frame(1, 0, 0, 0, OpCode, 0, DataLen, 0, Data), 254 | [Frame] = wsock_framing:from_binary(BinFrame), 255 | 256 | assert_that(Frame#frame.opcode, is(OpCode)) 257 | end) 258 | end), 259 | it("should set opcode to continuation if continuation frame", fun() -> 260 | (spec_get(validator))(?OP_CODE_CONT) 261 | end), 262 | it("should set opcode to text if text frame", fun() -> 263 | (spec_get(validator))(?OP_CODE_TEXT) 264 | end), 265 | it("should set opcode to binary if binary frame", fun() -> 266 | (spec_get(validator))(?OP_CODE_BIN) 267 | end), 268 | it("should set opcode to ping if ping frame", fun() -> 269 | (spec_get(validator))(?OP_CODE_PING) 270 | end), 271 | it("should set opcode to pong if pong frame", fun() -> 272 | (spec_get(validator))(?OP_CODE_PONG) 273 | end), 274 | it("should set opcode to close if close frame", fun() -> 275 | (spec_get(validator))(?OP_CODE_CLOSE) 276 | end) 277 | end), 278 | describe("mask", fun() -> 279 | before_all(fun() -> 280 | spec_set(validator, fun(Mask) -> 281 | {_BinFrame, Frame} = (spec_get(frame_builder))(0, 0, 1, Mask, crypto:rand_bytes(20)), 282 | assert_that(Frame#frame.mask, is(Mask)) 283 | end) 284 | end), 285 | it("should set mask if masked data", fun() -> 286 | (spec_get(validator))(1) 287 | end), 288 | it("should not set mask if unmasked data", fun() -> 289 | (spec_get(validator))(0) 290 | end) 291 | end), 292 | describe("payoad lenght", fun()-> 293 | before_all(fun() -> 294 | spec_set(frame, fun(Size) -> 295 | {_BinFrame, Frame} = (spec_get(frame_builder))(0, 0, 2, 0, crypto:rand_bytes(Size)), 296 | Frame 297 | end) 298 | end), 299 | it("set payload length of data with <= 125 bytes", fun() -> 300 | Frame = (spec_get(frame))(100), 301 | 302 | assert_that(Frame#frame.payload_len, is(100)) 303 | end), 304 | it("set payload length of data with > 125 <= 65536 bytes", fun() -> 305 | Frame = (spec_get(frame))(200), 306 | 307 | assert_that(Frame#frame.payload_len, is(126)), 308 | assert_that(Frame#frame.extended_payload_len, is(200)) 309 | end), 310 | it("set payload length of data with > 65536 bytes", fun() -> 311 | Frame = (spec_get(frame))(70000), 312 | 313 | assert_that(Frame#frame.payload_len, is(127)), 314 | assert_that(Frame#frame.extended_payload_len_cont, is(70000)) 315 | end) 316 | end), 317 | describe("masking key", fun() -> 318 | before_all(fun() -> 319 | spec_set(frame, fun(Mask) -> 320 | (spec_get(frame_builder))(0, 0, 2, Mask, crypto:rand_bytes(20)) 321 | end) 322 | end), 323 | it("should be undefined if data is unmasked", fun() -> 324 | {_, Frame} = (spec_get(frame))(0), 325 | 326 | assert_that(Frame#frame.masking_key, is(undefined)) 327 | end), 328 | it("should be set if data is masked", fun() -> 329 | {BinFrame, Frame} = (spec_get(frame))(1), 330 | <<_:2/binary, MK:32/integer, _/binary>> = BinFrame, 331 | 332 | assert_that(Frame#frame.masking_key, is_not(undefined)), 333 | assert_that(Frame#frame.masking_key, is(MK)) 334 | end) 335 | end) 336 | end), 337 | describe("payload", fun() -> 338 | before_all(fun() -> 339 | spec_set(validator, fun(Mask, Size) -> 340 | Data = crypto:rand_bytes(Size), 341 | {_BinFrame, Frame} = (spec_get(frame_builder))(0, 0, 2, Mask, Data), 342 | 343 | assert_that(Frame#frame.payload, is(Data)) 344 | end) 345 | end), 346 | describe("when payload length is 0", fun() -> 347 | it("should set fragmented to true", fun() -> 348 | {_, Frame} = (spec_get(frame_builder))(0, 0, 0, 0, <<>>), 349 | 350 | assert_that(Frame#frame.fragmented, is(false)) 351 | end) 352 | end), 353 | describe("when payload length <= 125", fun()-> 354 | it("should set unmasked data", fun()-> 355 | (spec_get(validator))(0 ,100) 356 | end), 357 | it("should unmask masked data", fun() -> 358 | (spec_get(validator))(1 ,100) 359 | end) 360 | end), 361 | describe("when payload length > 125 and <= 65536 bytes", fun()-> 362 | it("should set unmasked data", fun()-> 363 | (spec_get(validator))(0 , 300) 364 | end), 365 | it("should unmask masked data", fun() -> 366 | (spec_get(validator))(1 , 300) 367 | end) 368 | end), 369 | describe("when payload length > 65536 bytes", fun()-> 370 | it("should set unmasked data", fun()-> 371 | (spec_get(validator))(0 , 70000) 372 | end), 373 | it("should unmask masked data", fun() -> 374 | (spec_get(validator))(1 , 70000) 375 | end) 376 | end) 377 | end), 378 | describe("when binary is composed from various frames", fun() -> 379 | it("should return a list of frame records", fun() -> 380 | Text1 = "Jankle jankle", 381 | Payload1 = list_to_binary(Text1), 382 | PayloadLen1 = byte_size(Payload1), 383 | 384 | Text2 = "Pasa pra casa", 385 | Payload2 = list_to_binary(Text2), 386 | PayloadLen2 = byte_size(Payload2), 387 | 388 | BinFrame1 = get_binary_frame(0, 0, 0, 0, 1, 0, PayloadLen1, 0, Payload1), 389 | BinFrame2 = get_binary_frame(1, 0, 0, 0, 0, 0, PayloadLen2, 0, Payload2), 390 | 391 | BinFrames = <>, 392 | 393 | [Frame1, Frame2] = wsock_framing:from_binary(BinFrames), 394 | 395 | assert_that(Frame1#frame.fin, is(0)), 396 | assert_that(Frame1#frame.rsv1, is(0)), 397 | assert_that(Frame1#frame.rsv2, is(0)), 398 | assert_that(Frame1#frame.rsv3, is(0)), 399 | assert_that(Frame1#frame.opcode, is(1)), 400 | assert_that(Frame1#frame.mask, is(0)), 401 | assert_that(Frame1#frame.payload_len, is(PayloadLen1)), 402 | assert_that(Frame1#frame.payload, is(Payload1)), 403 | 404 | assert_that(Frame2#frame.fin, is(1)), 405 | assert_that(Frame2#frame.rsv1, is(0)), 406 | assert_that(Frame2#frame.rsv2, is(0)), 407 | assert_that(Frame2#frame.rsv3, is(0)), 408 | assert_that(Frame2#frame.opcode, is(0)), 409 | assert_that(Frame2#frame.mask, is(0)), 410 | assert_that(Frame2#frame.payload_len, is(PayloadLen2)), 411 | assert_that(Frame2#frame.payload, is(Payload2)) 412 | end) 413 | end), 414 | describe("when input data is fragmented", fun() -> 415 | describe("when there's only 8 bits of data", fun() -> 416 | it("should return a fragmented frame", fun() -> 417 | Data = crypto:rand_bytes(20), 418 | DataLen = byte_size(Data), 419 | BinFrame = get_binary_frame(1, 0, 0, 0, 1, 0, DataLen, 0, Data), 420 | <> = BinFrame, 421 | 422 | [Frame] = wsock_framing:from_binary(Fragment), 423 | 424 | assert_that(Frame#frame.fragmented, is(true)), 425 | assert_that(Frame#frame.fin, is(1)), 426 | assert_that(Frame#frame.rsv1, is(0)), 427 | assert_that(Frame#frame.rsv2, is(0)), 428 | assert_that(Frame#frame.rsv3, is(0)), 429 | assert_that(Frame#frame.opcode, is(1)), 430 | assert_that(Frame#frame.raw, is(<<>>)) 431 | end) 432 | end), 433 | describe("when there's only 16 bits of data", fun() -> 434 | it("shoudl return a fragmented frame", fun() -> 435 | Data = crypto:rand_bytes(20), 436 | DataLen = byte_size(Data), 437 | BinFrame = get_binary_frame(1, 0, 0, 0, 1, 0, DataLen, 0, Data), 438 | <> = BinFrame, 439 | 440 | [Frame] = wsock_framing:from_binary(Fragment), 441 | 442 | assert_that(Frame#frame.fragmented, is(true)), 443 | assert_that(Frame#frame.mask, is(0)), 444 | assert_that(Frame#frame.payload_len, is(DataLen)), 445 | assert_that(Frame#frame.raw, is(<<>>)) 446 | end) 447 | end), 448 | describe("when there's only 24 bits of data", fun() -> 449 | describe("when payload length is extended", fun() -> 450 | it("should not set the extended payload length", fun() -> 451 | Data = crypto:rand_bytes(140), 452 | DataLen = byte_size(Data), 453 | BinFrame = get_binary_frame(1, 0, 0, 0, 1, 0, 126, DataLen, Data), 454 | <> = BinFrame, 455 | <<_:2/binary, LastFragment/binary>> = Fragment, 456 | 457 | [Frame] = wsock_framing:from_binary(Fragment), 458 | 459 | assert_that(Frame#frame.fragmented, is(true)), 460 | assert_that(Frame#frame.payload_len, is(126)), 461 | assert_that(Frame#frame.extended_payload_len, is(undefined)), 462 | assert_that(Frame#frame.raw, is(LastFragment)) 463 | end) 464 | end) 465 | end), 466 | describe("when new data is received", fun() -> 467 | it("should complete the fragmented frame", fun() -> 468 | Data = crypto:rand_bytes(140), 469 | DataLen = byte_size(Data), 470 | BinFrame = get_binary_frame(1, 0, 0, 0, 1, 0, 126, DataLen, Data), 471 | <> = BinFrame, 472 | 473 | [FragmentedFrame] = wsock_framing:from_binary(FirstFragment), 474 | [Frame] = wsock_framing:from_binary(SecondFragment, FragmentedFrame), 475 | 476 | assert_that(Frame#frame.fragmented, is(false)), 477 | assert_that(Frame#frame.payload, is(Data)), 478 | assert_that(Frame#frame.raw, is(<<>>)) 479 | end) 480 | end) 481 | end) 482 | end), 483 | describe("frame", fun() -> 484 | describe("when no options are passed", fun() -> 485 | it("should unset fin", fun() -> 486 | Frame = wsock_framing:frame("Foo bar"), 487 | assert_that(Frame#frame.fin, is(0)) 488 | end), 489 | it("should set opcode to text on text data", fun()-> 490 | Frame = wsock_framing:frame("Foo bar"), 491 | assert_that(Frame#frame.opcode, is(1)) 492 | end), 493 | it("should set opcode to binary on binary data", fun()-> 494 | Frame = wsock_framing:frame(<<"Foo bar">>), 495 | assert_that(Frame#frame.opcode, is(2)) 496 | end), 497 | it("should leave data unmasked", fun() -> 498 | Data = "Fofito", 499 | Frame = wsock_framing:frame(Data), 500 | assert_that(Frame#frame.mask, is(0)), 501 | assert_that(Frame#frame.payload, is(list_to_binary(Data))), 502 | assert_that(Frame#frame.masking_key, is(undefined)) 503 | end) 504 | end), 505 | describe("not payload fields", fun()-> 506 | describe("fin", fun() -> 507 | it("should set fin if fin option is present", fun()-> 508 | Frame = wsock_framing:frame("Foo bar", [fin]), 509 | assert_that(Frame#frame.fin, is(1)) 510 | end), 511 | it("should unset fin if fin option is not present", fun() -> 512 | Frame = wsock_framing:frame("asdasda", []), 513 | assert_that(Frame#frame.fin, is(0)) 514 | end) 515 | end), 516 | describe("rsv", fun() -> 517 | it("should set all 3 'rsv' to 0", fun()-> 518 | Data = "Foo bar", 519 | Frame = wsock_framing:frame(Data), 520 | assert_that(Frame#frame.rsv1, is(0)), 521 | assert_that(Frame#frame.rsv2, is(0)), 522 | assert_that(Frame#frame.rsv3, is(0)) 523 | end) 524 | end), 525 | describe("opcode", fun() -> 526 | before_all(fun() -> 527 | spec_set(validator, fun(Options, Expected) -> 528 | Frame = wsock_framing:frame("Foo bar", Options), 529 | assert_that(Frame#frame.opcode, is(Expected)) 530 | end) 531 | end), 532 | it("should set opcode to text if opcode option is text", fun()-> 533 | (spec_get(validator))([{opcode, text}], 1) 534 | end), 535 | it("should set opcode to binary if opcode option is binary", fun()-> 536 | (spec_get(validator))([{opcode, binary}], 2) 537 | end), 538 | it("should set opcode to ping if opcode option is ping", fun()-> 539 | (spec_get(validator))([{opcode, ping}], 9) 540 | end), 541 | it("should set opcode to pong if opcode option is pong", fun() -> 542 | (spec_get(validator))([{opcode, pong}], 10) 543 | end), 544 | it("should set opcode to close if opcode option is close", fun() -> 545 | (spec_get(validator))([{opcode, close}], 8) 546 | end), 547 | it("should set opcode to continuation if opcode option is continuation", fun()-> 548 | (spec_get(validator))([{opcode, continuation}], 0) 549 | end) 550 | end), 551 | describe("mask", fun() -> 552 | it("should be unset if mask option is not present", fun() -> 553 | Frame = wsock_framing:frame("asdasd", []), 554 | assert_that(Frame#frame.mask, is(0)) 555 | end), 556 | it("should set mask if mask option is present", fun() -> 557 | Frame = wsock_framing:frame("assd", [mask]), 558 | assert_that(Frame#frame.mask, is(1)) 559 | end) 560 | end), 561 | describe("masking key", fun() -> 562 | it("should be unset if mask option is not present", fun() -> 563 | Frame = wsock_framing:frame("asda", []), 564 | assert_that(Frame#frame.masking_key, is(undefined)) 565 | end), 566 | it("should be set if mask option is present", fun() -> 567 | Frame = wsock_framing:frame("asads", [mask]), 568 | assert_that(Frame#frame.masking_key, is_not(undefined)) 569 | end) 570 | end), 571 | describe("payload length", fun() -> 572 | before_all(fun() -> 573 | spec_set(validator, fun(Size, PL, EPL, EPLC) -> 574 | Frame = wsock_framing:frame(crypto:rand_bytes(Size), []), 575 | assert_that(Frame#frame.payload_len, is(PL)), 576 | assert_that(Frame#frame.extended_payload_len, is(EPL)), 577 | assert_that(Frame#frame.extended_payload_len_cont, is(EPLC)) 578 | end) 579 | end), 580 | it("should set payload length of data <= 125 bytes", fun() -> 581 | (spec_get(validator))(100, 100, 0, 0) 582 | end), 583 | it("should set payload length of data > 125 and <= 65536 bytes", fun() -> 584 | (spec_get(validator))(1000, 126, 1000, 0) 585 | end), 586 | it("should set payload length of data > 65536 bytes", fun() -> 587 | (spec_get(validator))(70000, 127, 0, 70000) 588 | end) 589 | end) 590 | end), 591 | describe("payload", fun()-> 592 | it("should set unmasked payload", fun() -> 593 | Data = crypto:rand_bytes(100), 594 | Frame = wsock_framing:frame(Data, []), 595 | 596 | assert_that(Frame#frame.payload, is(Data)) 597 | end), 598 | it("should set masked payload", fun() -> 599 | Data = crypto:rand_bytes(100), 600 | Frame = wsock_framing:frame(Data, [mask]), 601 | 602 | MaskedData = mask(Data, Frame#frame.masking_key, <<>>), 603 | 604 | assert_that(Frame#frame.payload, is(MaskedData)) 605 | end) 606 | end), 607 | describe("control frames", fun()-> 608 | describe("close", fun() -> 609 | it("should frame closes without payload", fun() -> 610 | Frame = wsock_framing:frame([], [fin, {opcode, close}]), 611 | 612 | assert_that(Frame#frame.fin, is(1)), 613 | assert_that(Frame#frame.rsv1, is(0)), 614 | assert_that(Frame#frame.rsv2, is(0)), 615 | assert_that(Frame#frame.rsv3, is(0)), 616 | assert_that(Frame#frame.opcode, is(8)), 617 | assert_that(Frame#frame.mask, is(0)) 618 | end), 619 | it("should frames closes with payload", fun() -> 620 | Frame = wsock_framing:frame({1000, "Closing this shit"}, [mask, fin, {opcode, close}]), 621 | %mask function also unmask the data 622 | <> = mask( 623 | Frame#frame.payload, 624 | Frame#frame.masking_key, 625 | <<>>), 626 | 627 | assert_that(Frame#frame.fin, is(1)), 628 | assert_that(Frame#frame.rsv1, is(0)), 629 | assert_that(Frame#frame.rsv2, is(0)), 630 | assert_that(Frame#frame.rsv3, is(0)), 631 | assert_that(Frame#frame.opcode, is(8)), 632 | assert_that(Code, is(1000)), 633 | assert_that(Frame#frame.mask, is(1)), 634 | assert_that(binary_to_list(Reason), is("Closing this shit")) 635 | end) 636 | end), 637 | it("should not allow payload size over 125 bytes") 638 | end) 639 | end). 640 | 641 | get_binary_frame(Fin, Rsv1, Rsv2, Rsv3, Opcode, Mask, Length, ExtendedPayloadLength, Payload) -> 642 | Head = <>, 643 | TempBin = case Length of 644 | 126 -> 645 | <>; 646 | 127 -> 647 | <>; 648 | _ -> 649 | <> 650 | end, 651 | 652 | case Mask of 653 | 0 -> 654 | <>; 655 | 1 -> 656 | <> = crypto:rand_bytes(4), 657 | MaskedPayload = mask(Payload, Mk, <<>>), 658 | <> 659 | end. 660 | 661 | %get_random_string(Length) -> 662 | % AllowedChars = "qwertyQWERTY1234567890", 663 | % lists:foldl(fun(_, Acc) -> 664 | % [lists:nth(random:uniform(length(AllowedChars)), 665 | % AllowedChars)] 666 | % ++ Acc 667 | % end, [], lists:seq(1, Length)). 668 | 669 | %mask(Bin, MaskKey, Acc) -> 670 | mask(<>, MaskKey, Acc) -> 671 | T = Data bxor MaskKey, 672 | mask(Rest, MaskKey, <>); 673 | 674 | mask(<< Data:24>>, MaskKey, Acc) -> 675 | <> = <>, 676 | T = Data bxor MaskKey2, 677 | <>; 678 | 679 | mask(<< Data:16>>, MaskKey, Acc) -> 680 | <> = <>, 681 | T = Data bxor MaskKey2, 682 | <>; 683 | 684 | mask(<< Data:8>>, MaskKey, Acc) -> 685 | <> = <>, 686 | T = Data bxor MaskKey2, 687 | <>; 688 | 689 | mask(<<>>, _, Acc) -> 690 | Acc. 691 | --------------------------------------------------------------------------------