├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── ct ├── echo_server.erl ├── wc_SUITE.erl ├── websocket_client.coverspec └── ws_client.erl ├── erlang.mk ├── examples ├── sample_ws_handler.erl └── ws_ping_example.erl ├── rebar.config └── src ├── websocket_client.app.src ├── websocket_client.erl ├── websocket_client_handler.erl └── websocket_req.erl /.gitignore: -------------------------------------------------------------------------------- 1 | ebin/* 2 | deps/* 3 | *.beam 4 | .eunit/* 5 | erl_crash.dump 6 | log/* 7 | .ct_results 8 | test-deps -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - R16B03 4 | - R16B02 5 | - R16B01 6 | script: make test 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (C) 2012-2013 Jeremy Ong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | PROJECT = websocket_client 3 | 4 | include erlang.mk 5 | 6 | test-deps: 7 | git clone https://github.com/extend/cowboy.git test-deps/cowboy 8 | pushd test-deps/cowboy; git checkout 0.9.0; make; popd 9 | 10 | test: test-deps all 11 | mkdir -p .ct_results 12 | ct_run -pa test-deps/cowboy/ebin test-deps/cowboy/deps/*/ebin ebin \ 13 | -dir ct \ 14 | -logdir ./.ct_results \ 15 | -cover ct/websocket_client.coverspec 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlang Websocket Client 2 | 3 | [![Build Status](https://travis-ci.org/jeremyong/websocket_client.svg?branch=master)](https://travis-ci.org/jeremyong/websocket_client) 4 | 5 | ## Existing features 6 | 7 | 1. Client to Server Masking 8 | 2. gen_server like callback behaviour 9 | 3. Handshake validation 10 | 4. tcp and ssl support 11 | 5. Handling of text, binary, ping, pong, and close frames 12 | 6. Handling of continuation frames 13 | 14 | ## Usage 15 | 16 | For basic usage, please see the source files in the `examples/` 17 | directory. Writing a handler is easy: 18 | 19 | ```erlang 20 | -module(sample_ws_handler). 21 | 22 | -behaviour(websocket_client_handler). 23 | 24 | -export([ 25 | start_link/0, 26 | init/2, 27 | websocket_handle/3, 28 | websocket_info/3, 29 | websocket_terminate/3 30 | ]). 31 | 32 | start_link() -> 33 | crypto:start(), 34 | ssl:start(), 35 | websocket_client:start_link("wss://echo.websocket.org", ?MODULE, []). 36 | 37 | init([], _ConnState) -> 38 | websocket_client:cast(self(), {text, <<"message 1">>}), 39 | {ok, 2}. 40 | 41 | websocket_handle({pong, _}, _ConnState, State) -> 42 | {ok, State}; 43 | websocket_handle({text, Msg}, _ConnState, 5) -> 44 | io:format("Received msg ~p~n", [Msg]), 45 | {close, <<>>, 10}; 46 | websocket_handle({text, Msg}, _ConnState, State) -> 47 | io:format("Received msg ~p~n", [Msg]), 48 | timer:sleep(1000), 49 | BinInt = list_to_binary(integer_to_list(State)), 50 | {reply, {text, <<"hello, this is message #", BinInt/binary >>}, State + 1}. 51 | 52 | websocket_info(start, _ConnState, State) -> 53 | {reply, {text, <<"erlang message received">>}, State}. 54 | 55 | websocket_terminate({close, Code, Payload}, _ConnState, State) -> 56 | io:format("Websocket closed in state ~p wih code ~p and payload ~p~n", 57 | [State, Code, Payload]), 58 | ok. 59 | ``` 60 | 61 | The above code will send messages to the echo server that count up 62 | from 1. It will also print all replies from the server: 63 | 64 | ``` 65 | Received msg <<"this is message 1">> 66 | Received msg <<"hello, this is message #3">> 67 | Received msg <<"hello, this is message #4">> 68 | Received msg <<"hello, this is message #5">> 69 | Received msg <<"hello, this is message #6">> 70 | ... 71 | ``` 72 | 73 | Erlang is typically used to write server applications. Now that 74 | applications like `cowboy` supporting websocket applications are more 75 | commonplace, it is important to have a compliant websocket client for 76 | benchmarking and debugging purposes. 77 | 78 | This client implements a cowboy like `websocket_client_handler` to 79 | interact with a websocket server. Currently, it can connect via tcp or 80 | ssl via the `ws` and `wss` protocols. It can also send and receive 81 | contiguous text or binary websocket frames. 82 | 83 | ## TODO 84 | 85 | The client as is is still missing a lot of functionality. We still 86 | need to: 87 | 88 | 1. Close the connection in a number of error cases (malformed headers, 89 | etc). 90 | 2. Add tests!! (hint hint) 91 | 3. Stop using `verify_none` by default 92 | 93 | This is being released without the above functionality in case it is 94 | useful as is for benchmarking or debugging as mentioned above (this is 95 | the case for me). The hope is that the community (and me, as time 96 | permits) will contribute key pieces to the codebase so that the major 97 | pieces are taken care of. 98 | -------------------------------------------------------------------------------- /ct/echo_server.erl: -------------------------------------------------------------------------------- 1 | -module(echo_server). 2 | 3 | -behaviour(cowboy_websocket_handler). 4 | 5 | -export([ 6 | start/0 7 | ]). 8 | 9 | -export([ 10 | init/3, 11 | websocket_init/3, 12 | websocket_handle/3, 13 | websocket_info/3, 14 | websocket_terminate/3 15 | ]). 16 | 17 | -record(state, {}). 18 | 19 | start() -> 20 | io:format("Starting echo server.~n"), 21 | Dispatch = cowboy_router:compile([{'_', [ 22 | {"/hello", ?MODULE, []}, 23 | {'_', ?MODULE, []} 24 | ]}]), 25 | {ok, _} = cowboy:start_http(echo_listener, 2, [ 26 | {port, 8080}, 27 | {max_connections, 100} 28 | ], 29 | [{env, [{dispatch, Dispatch}]}]), 30 | ok. 31 | 32 | init(_, _Req, _Opts) -> 33 | {upgrade, protocol, cowboy_websocket}. 34 | 35 | websocket_init(_Transport, Req, _Opts) -> 36 | case cowboy_req:qs_val(<<"code">>, Req) of 37 | {undefined, Req2} -> 38 | case cowboy_req:qs_val(<<"q">>, Req2) of 39 | {undefined, Req3} -> 40 | {ok, Req3, #state{}}; 41 | {Text, Req3} -> 42 | self() ! {send, Text}, 43 | {ok, Req3, #state{}} 44 | end; 45 | {Code, Req2} -> 46 | IntegerCode = list_to_integer(binary_to_list(Code)), 47 | io:format("Shuting down on init using '~p' status code~n", [IntegerCode]), 48 | {ok, Req3} = cowboy_req:reply(IntegerCode, Req2), 49 | {shutdown, Req3} 50 | end. 51 | 52 | websocket_handle(Frame, Req, State) -> 53 | io:format("Received frame~n"), 54 | {reply, Frame, Req, State}. 55 | 56 | websocket_info({send, Text}, Req, State) -> 57 | io:format("Sent frame~n"), 58 | {reply, {text, Text}, Req, State}; 59 | 60 | websocket_info(_Msg, Req, State) -> 61 | {ok, Req, State}. 62 | 63 | websocket_terminate(Reason, _Req, _State) -> 64 | io:format("Server terminating with reason ~p~n", [Reason]), 65 | ok. 66 | -------------------------------------------------------------------------------- /ct/wc_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(wc_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | -define(print(Value), io:format("~n~p~n", [Value])). 5 | 6 | -export([ 7 | all/0, 8 | init_per_suite/1, 9 | end_per_suite/1 10 | ]). 11 | -export([ 12 | test_text_frames/1, 13 | test_binary_frames/1, 14 | test_control_frames/1, 15 | test_quick_response/1, 16 | test_bad_request/1 17 | ]). 18 | 19 | all() -> 20 | [ 21 | test_text_frames, 22 | test_binary_frames, 23 | test_control_frames, 24 | test_quick_response, 25 | test_bad_request 26 | ]. 27 | 28 | init_per_suite(Config) -> 29 | ok = application:start(sasl), 30 | ok = application:start(asn1), 31 | ok = crypto:start(), 32 | ok = application:start(public_key), 33 | ok = application:start(ssl), 34 | ok = application:start(cowlib), 35 | ok = application:start(ranch), 36 | ok = application:start(cowboy), 37 | ok = echo_server:start(), 38 | Config. 39 | 40 | end_per_suite(Config) -> 41 | Config. 42 | 43 | test_text_frames(_) -> 44 | {ok, Pid} = ws_client:start_link(), 45 | %% Short message 46 | Short = short_msg(), 47 | ws_client:send_text(Pid, Short), 48 | {text, Short} = ws_client:recv(Pid), 49 | %% Payload length greater than 125 (actual 150). 50 | Medium = medium_msg(), 51 | ws_client:send_text(Pid, Medium), 52 | {text, Medium} = ws_client:recv(Pid), 53 | 54 | %% Now check that websocket_client:send is working 55 | Pid ! {send_text, Medium}, 56 | {text, Medium} = ws_client:recv(Pid), 57 | 58 | %% Payload length greater than 65535 59 | Long = long_msg(), 60 | ws_client:send_text(Pid, Long), 61 | {text, Long} = ws_client:recv(Pid), 62 | ws_client:stop(Pid), 63 | ok. 64 | 65 | test_binary_frames(_) -> 66 | {ok, Pid} = ws_client:start_link(), 67 | %% Short message 68 | Short = short_msg(), 69 | ws_client:send_binary(Pid, Short), 70 | {binary, Short} = ws_client:recv(Pid), 71 | %% Payload length greater than 125 (actual 150). 72 | Medium = medium_msg(), 73 | ws_client:send_binary(Pid, Medium), 74 | {binary, Medium} = ws_client:recv(Pid), 75 | %% Payload length greater than 65535 76 | Long = long_msg(), 77 | ws_client:send_binary(Pid, Long), 78 | {binary, Long} = ws_client:recv(Pid), 79 | ws_client:stop(Pid), 80 | ok. 81 | 82 | test_control_frames(_) -> 83 | {ok, Pid} = ws_client:start_link(), 84 | %% Send ping with short message 85 | Short = short_msg(), 86 | ws_client:send_ping(Pid, Short), 87 | {pong, Short} = ws_client:recv(Pid), 88 | %% Server will echo the ping as well 89 | {ping, Short} = ws_client:recv(Pid), 90 | {pong, Short} = ws_client:recv(Pid), 91 | %% Send ping without message 92 | ws_client:send_ping(Pid, <<>>), 93 | {pong, <<>>} = ws_client:recv(Pid), 94 | ws_client:stop(Pid), 95 | ok. 96 | 97 | test_quick_response(_) -> 98 | %% Connect to the server and... 99 | {ok, Pid} = ws_client:start_link("ws://localhost:8080/hello/?q=world!"), 100 | %% ...make sure we receive the first frame. 101 | {text, <<"world!">>} = ws_client:recv(Pid, 100), 102 | ws_client:stop(Pid), 103 | %% Also, make sure the HTTP response is parsed correctly. 104 | {ok, Pid2} = ws_client:start_link("ws://localhost:8080/hello/?q=Hello%0D%0A%0D%0AWorld%0D%0A%0D%0A!"), 105 | {text, <<"Hello\r\n\r\nWorld\r\n\r\n!">>} = ws_client:recv(Pid2, 100), 106 | ws_client:stop(Pid2), 107 | ok. 108 | 109 | test_bad_request(_) -> 110 | %% Connect to the server and wait for a error 111 | {error, {400, <<"Bad Request">>}} = ws_client:start_link("ws://localhost:8080/hello/?code=400"), 112 | {error, {403, <<"Forbidden">>}} = ws_client:start_link("ws://localhost:8080/hello/?code=403"), 113 | ok. 114 | 115 | short_msg() -> 116 | <<"hello">>. 117 | medium_msg() -> 118 | <<"ttttttttttttttttttttttttt" 119 | "ttttttttttttttttttttttttt" 120 | "ttttttttttttttttttttttttt" 121 | "ttttttttttttttttttttttttt" 122 | "ttttttttttttttttttttttttt" 123 | "ttttttttttttttttttttttttt">>. 124 | long_msg() -> 125 | Medium = medium_msg(), 126 | %% 600 bytes 127 | L = << Medium/binary, Medium/binary, Medium/binary, Medium/binary >>, 128 | %% 2400 bytes 129 | L1 = << L/binary, L/binary, L/binary, L/binary >>, 130 | %% 9600 bytes 131 | L2 = << L1/binary, L1/binary, L1/binary, L1/binary >>, 132 | %% 38400 bytes 133 | L3 = << L2/binary, L2/binary, L2/binary, L2/binary >>, 134 | %% 76800 bytes 135 | << L3/binary, L3/binary >>. 136 | -------------------------------------------------------------------------------- /ct/websocket_client.coverspec: -------------------------------------------------------------------------------- 1 | {incl_mods, [websocket_client, websocket_req]}. -------------------------------------------------------------------------------- /ct/ws_client.erl: -------------------------------------------------------------------------------- 1 | -module(ws_client). 2 | 3 | -behaviour(websocket_client_handler). 4 | 5 | -export([ 6 | start_link/0, 7 | start_link/1, 8 | send_text/2, 9 | send_binary/2, 10 | send_ping/2, 11 | recv/2, 12 | recv/1, 13 | stop/1 14 | ]). 15 | 16 | -export([ 17 | init/2, 18 | websocket_handle/3, 19 | websocket_info/3, 20 | websocket_terminate/3 21 | ]). 22 | 23 | -record(state, { 24 | buffer = [] :: list(), 25 | waiting = undefined :: undefined | pid() 26 | }). 27 | 28 | start_link() -> 29 | start_link("ws://localhost:8080"). 30 | 31 | start_link(Url) -> 32 | websocket_client:start_link(Url, ?MODULE, []). 33 | 34 | stop(Pid) -> 35 | Pid ! stop. 36 | 37 | send_text(Pid, Msg) -> 38 | websocket_client:cast(Pid, {text, Msg}). 39 | 40 | send_binary(Pid, Msg) -> 41 | websocket_client:cast(Pid, {binary, Msg}). 42 | 43 | send_ping(Pid, Msg) -> 44 | websocket_client:cast(Pid, {ping, Msg}). 45 | 46 | recv(Pid) -> 47 | recv(Pid, 5000). 48 | 49 | recv(Pid, Timeout) -> 50 | Pid ! {recv, self()}, 51 | receive 52 | M -> M 53 | after 54 | Timeout -> error 55 | end. 56 | 57 | init(_, _WSReq) -> 58 | {ok, #state{}}. 59 | 60 | websocket_handle(Frame, _, State = #state{waiting = undefined, buffer = Buffer}) -> 61 | io:format("Client received frame~n"), 62 | {ok, State#state{buffer = [Frame|Buffer]}}; 63 | websocket_handle(Frame, _, State = #state{waiting = From}) -> 64 | io:format("Client received frame~n"), 65 | From ! Frame, 66 | {ok, State#state{waiting = undefined}}. 67 | 68 | websocket_info({send_text, Text}, WSReq, State) -> 69 | websocket_client:send({text, Text}, WSReq), 70 | {ok, State}; 71 | websocket_info({recv, From}, _, State = #state{buffer = []}) -> 72 | {ok, State#state{waiting = From}}; 73 | websocket_info({recv, From}, _, State = #state{buffer = [Top|Rest]}) -> 74 | From ! Top, 75 | {ok, State#state{buffer = Rest}}; 76 | websocket_info(stop, _, State) -> 77 | {close, <<>>, State}. 78 | 79 | websocket_terminate(Close, _, State) -> 80 | io:format("Websocket closed with frame ~p and state ~p", [Close, State]), 81 | ok. 82 | -------------------------------------------------------------------------------- /erlang.mk: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2014, Loïc Hoguin 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | # Project. 16 | 17 | PROJECT ?= $(notdir $(CURDIR)) 18 | 19 | # Packages database file. 20 | 21 | PKG_FILE ?= $(CURDIR)/.erlang.mk.packages.v1 22 | export PKG_FILE 23 | 24 | PKG_FILE_URL ?= https://raw.github.com/extend/erlang.mk/master/packages.v1.tsv 25 | 26 | define get_pkg_file 27 | wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE) 28 | endef 29 | 30 | # Verbosity and tweaks. 31 | 32 | V ?= 0 33 | 34 | appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; 35 | appsrc_verbose = $(appsrc_verbose_$(V)) 36 | 37 | erlc_verbose_0 = @echo " ERLC " $(filter %.erl %.core,$(?F)); 38 | erlc_verbose = $(erlc_verbose_$(V)) 39 | 40 | xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); 41 | xyrl_verbose = $(xyrl_verbose_$(V)) 42 | 43 | dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); 44 | dtl_verbose = $(dtl_verbose_$(V)) 45 | 46 | gen_verbose_0 = @echo " GEN " $@; 47 | gen_verbose = $(gen_verbose_$(V)) 48 | 49 | .PHONY: rel clean-rel all clean-all app clean deps clean-deps \ 50 | docs clean-docs build-tests tests build-plt dialyze 51 | 52 | # Release. 53 | 54 | RELX_CONFIG ?= $(CURDIR)/relx.config 55 | 56 | ifneq ($(wildcard $(RELX_CONFIG)),) 57 | 58 | RELX ?= $(CURDIR)/relx 59 | export RELX 60 | 61 | RELX_URL ?= https://github.com/erlware/relx/releases/download/v0.6.0/relx 62 | RELX_OPTS ?= 63 | 64 | define get_relx 65 | wget -O $(RELX) $(RELX_URL) || rm $(RELX) 66 | chmod +x $(RELX) 67 | endef 68 | 69 | rel: clean-rel all $(RELX) 70 | @$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) 71 | 72 | $(RELX): 73 | @$(call get_relx) 74 | 75 | clean-rel: 76 | @rm -rf _rel 77 | 78 | endif 79 | 80 | # Deps directory. 81 | 82 | DEPS_DIR ?= $(CURDIR)/deps 83 | export DEPS_DIR 84 | 85 | REBAR_DEPS_DIR = $(DEPS_DIR) 86 | export REBAR_DEPS_DIR 87 | 88 | ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS)) 89 | ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) 90 | 91 | # Application. 92 | 93 | ifeq ($(filter $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),) 94 | ifeq ($(ERL_LIBS),) 95 | ERL_LIBS = $(DEPS_DIR) 96 | else 97 | ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR) 98 | endif 99 | endif 100 | export ERL_LIBS 101 | 102 | ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ 103 | +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec 104 | COMPILE_FIRST ?= 105 | COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) 106 | 107 | all: deps app 108 | 109 | clean-all: clean clean-deps clean-docs 110 | $(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs 111 | 112 | app: ebin/$(PROJECT).app 113 | $(eval MODULES := $(shell find ebin -type f -name \*.beam \ 114 | | sed 's/ebin\///;s/\.beam/,/' | sed '$$s/.$$//')) 115 | $(appsrc_verbose) cat src/$(PROJECT).app.src \ 116 | | sed 's/{modules,[[:space:]]*\[\]}/{modules, \[$(MODULES)\]}/' \ 117 | > ebin/$(PROJECT).app 118 | 119 | define compile_erl 120 | $(erlc_verbose) erlc -v $(ERLC_OPTS) -o ebin/ \ 121 | -pa ebin/ -I include/ $(COMPILE_FIRST_PATHS) $(1) 122 | endef 123 | 124 | define compile_xyrl 125 | $(xyrl_verbose) erlc -v -o ebin/ $(1) 126 | $(xyrl_verbose) erlc $(ERLC_OPTS) -o ebin/ ebin/*.erl 127 | @rm ebin/*.erl 128 | endef 129 | 130 | define compile_dtl 131 | $(dtl_verbose) erl -noshell -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/ -eval ' \ 132 | Compile = fun(F) -> \ 133 | Module = list_to_atom( \ 134 | string:to_lower(filename:basename(F, ".dtl")) ++ "_dtl"), \ 135 | erlydtl:compile(F, Module, [{out_dir, "ebin/"}]) \ 136 | end, \ 137 | _ = [Compile(F) || F <- string:tokens("$(1)", " ")], \ 138 | init:stop()' 139 | endef 140 | 141 | ebin/$(PROJECT).app: $(shell find src -type f -name \*.erl) \ 142 | $(shell find src -type f -name \*.core) \ 143 | $(shell find src -type f -name \*.xrl) \ 144 | $(shell find src -type f -name \*.yrl) \ 145 | $(shell find templates -type f -name \*.dtl 2>/dev/null) 146 | @mkdir -p ebin/ 147 | $(if $(strip $(filter %.erl %.core,$?)), \ 148 | $(call compile_erl,$(filter %.erl %.core,$?))) 149 | $(if $(strip $(filter %.xrl %.yrl,$?)), \ 150 | $(call compile_xyrl,$(filter %.xrl %.yrl,$?))) 151 | $(if $(strip $(filter %.dtl,$?)), \ 152 | $(call compile_dtl,$(filter %.dtl,$?))) 153 | 154 | clean: 155 | $(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump 156 | 157 | # Dependencies. 158 | 159 | define get_dep 160 | @mkdir -p $(DEPS_DIR) 161 | ifeq (,$(findstring pkg://,$(word 1,$(dep_$(1))))) 162 | git clone -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1) 163 | else 164 | @if [ ! -f $(PKG_FILE) ]; then $(call get_pkg_file); fi 165 | git clone -n -- `awk 'BEGIN { FS = "\t" }; \ 166 | $$$$1 == "$(subst pkg://,,$(word 1,$(dep_$(1))))" { print $$$$2 }' \ 167 | $(PKG_FILE)` $(DEPS_DIR)/$(1) 168 | endif 169 | cd $(DEPS_DIR)/$(1) ; git checkout -q $(word 2,$(dep_$(1))) 170 | endef 171 | 172 | define dep_target 173 | $(DEPS_DIR)/$(1): 174 | $(call get_dep,$(1)) 175 | endef 176 | 177 | $(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep)))) 178 | 179 | deps: $(ALL_DEPS_DIRS) 180 | @for dep in $(ALL_DEPS_DIRS) ; do \ 181 | if [ -f $$dep/Makefile ] ; then \ 182 | $(MAKE) -C $$dep ; \ 183 | else \ 184 | echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep ; \ 185 | fi ; \ 186 | done 187 | 188 | clean-deps: 189 | @for dep in $(ALL_DEPS_DIRS) ; do \ 190 | if [ -f $$dep/Makefile ] ; then \ 191 | $(MAKE) -C $$dep clean ; \ 192 | else \ 193 | echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep clean ; \ 194 | fi ; \ 195 | done 196 | 197 | # Documentation. 198 | 199 | EDOC_OPTS ?= 200 | 201 | docs: clean-docs 202 | $(gen_verbose) erl -noshell \ 203 | -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), init:stop().' 204 | 205 | clean-docs: 206 | $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info 207 | 208 | # Tests. 209 | 210 | $(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) 211 | 212 | TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard 213 | TEST_ERLC_OPTS += -DTEST=1 -DEXTRA=1 +'{parse_transform, eunit_autoexport}' 214 | 215 | build-test-deps: $(ALL_TEST_DEPS_DIRS) 216 | @for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done 217 | 218 | build-tests: build-test-deps 219 | $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -o test/ \ 220 | $(wildcard test/*.erl test/*/*.erl) -pa ebin/ 221 | 222 | CT_OPTS ?= 223 | CT_RUN = ct_run \ 224 | -no_auto_compile \ 225 | -noshell \ 226 | -pa $(realpath ebin) $(DEPS_DIR)/*/ebin \ 227 | -dir test \ 228 | -logdir logs \ 229 | $(CT_OPTS) 230 | 231 | CT_SUITES ?= 232 | 233 | define test_target 234 | test_$(1): ERLC_OPTS = $(TEST_ERLC_OPTS) 235 | test_$(1): clean deps app build-tests 236 | @if [ -d "test" ] ; \ 237 | then \ 238 | mkdir -p logs/ ; \ 239 | $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) ; \ 240 | fi 241 | $(gen_verbose) rm -f test/*.beam 242 | endef 243 | 244 | $(foreach test,$(CT_SUITES),$(eval $(call test_target,$(test)))) 245 | 246 | tests: ERLC_OPTS = $(TEST_ERLC_OPTS) 247 | tests: clean deps app build-tests 248 | @if [ -d "test" ] ; \ 249 | then \ 250 | mkdir -p logs/ ; \ 251 | $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) ; \ 252 | fi 253 | $(gen_verbose) rm -f test/*.beam 254 | 255 | # Dialyzer. 256 | 257 | DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt 258 | export DIALYZER_PLT 259 | 260 | PLT_APPS ?= 261 | DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \ 262 | -Wunmatched_returns # -Wunderspecs 263 | 264 | build-plt: deps app 265 | @dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(ALL_DEPS_DIRS) 266 | 267 | dialyze: 268 | @dialyzer --src src --no_native $(DIALYZER_OPTS) 269 | 270 | # Packages. 271 | 272 | $(PKG_FILE): 273 | @$(call get_pkg_file) 274 | 275 | pkg-list: $(PKG_FILE) 276 | @cat $(PKG_FILE) | awk 'BEGIN { FS = "\t" }; { print \ 277 | "Name:\t\t" $$1 "\n" \ 278 | "Repository:\t" $$2 "\n" \ 279 | "Website:\t" $$3 "\n" \ 280 | "Description:\t" $$4 "\n" }' 281 | 282 | ifdef q 283 | pkg-search: $(PKG_FILE) 284 | @cat $(PKG_FILE) | grep -i ${q} | awk 'BEGIN { FS = "\t" }; { print \ 285 | "Name:\t\t" $$1 "\n" \ 286 | "Repository:\t" $$2 "\n" \ 287 | "Website:\t" $$3 "\n" \ 288 | "Description:\t" $$4 "\n" }' 289 | else 290 | pkg-search: 291 | @echo "Usage: make pkg-search q=STRING" 292 | endif 293 | -------------------------------------------------------------------------------- /examples/sample_ws_handler.erl: -------------------------------------------------------------------------------- 1 | -module(sample_ws_handler). 2 | 3 | -behaviour(websocket_client_handler). 4 | 5 | -export([ 6 | start_link/0, 7 | init/2, 8 | websocket_handle/3, 9 | websocket_info/3, 10 | websocket_terminate/3 11 | ]). 12 | 13 | start_link() -> 14 | crypto:start(), 15 | ssl:start(), 16 | websocket_client:start_link("wss://echo.websocket.org", ?MODULE, []). 17 | 18 | init([], _ConnState) -> 19 | websocket_client:cast(self(), {text, <<"message 1">>}), 20 | {ok, 2}. 21 | 22 | websocket_handle({pong, _}, _ConnState, State) -> 23 | {ok, State}; 24 | websocket_handle({text, Msg}, _ConnState, 5) -> 25 | io:format("Received msg ~p~n", [Msg]), 26 | {close, <<>>, "done"}; 27 | websocket_handle({text, Msg}, _ConnState, State) -> 28 | io:format("Received msg ~p~n", [Msg]), 29 | timer:sleep(1000), 30 | BinInt = list_to_binary(integer_to_list(State)), 31 | {reply, {text, <<"hello, this is message #", BinInt/binary >>}, State + 1}. 32 | 33 | websocket_info(start, _ConnState, State) -> 34 | {reply, {text, <<"erlang message received">>}, State}. 35 | 36 | websocket_terminate(Reason, _ConnState, State) -> 37 | io:format("Websocket closed in state ~p wih reason ~p~n", 38 | [State, Reason]), 39 | ok. 40 | -------------------------------------------------------------------------------- /examples/ws_ping_example.erl: -------------------------------------------------------------------------------- 1 | -module(ws_ping_example). 2 | 3 | -behaviour(websocket_client_handler). 4 | 5 | -export([ 6 | start_link/0, 7 | init/2, 8 | websocket_handle/3, 9 | websocket_info/3, 10 | websocket_terminate/3 11 | ]). 12 | 13 | start_link() -> 14 | crypto:start(), 15 | ssl:start(), 16 | websocket_client:start_link("wss://echo.websocket.org", ?MODULE, []). 17 | 18 | init([], _ConnState) -> 19 | websocket_client:cast(self(), {text, <<"message 1">>}), 20 | %% Execute a ping every 1000 milliseconds 21 | {ok, 2, 1000}. 22 | 23 | websocket_handle({pong, _Msg}, _ConnState, State) -> 24 | io:format("Received pong ~n"), 25 | 26 | %% This is how to access info about the connection/request 27 | Proto = websocket_req:protocol(_ConnState), 28 | io:format("On protocol: ~p~n", [Proto]), 29 | 30 | {ok, State}; 31 | websocket_handle({text, Msg}, _ConnState, 5) -> 32 | io:format("Received msg ~p~n", [Msg]), 33 | {close, <<>>, 10}; 34 | websocket_handle({text, Msg}, _ConnState, State) -> 35 | io:format("Received msg ~p~n", [Msg]), 36 | timer:sleep(1000), 37 | BinInt = list_to_binary(integer_to_list(State)), 38 | Reply = {text, <<"hello, this is message #", BinInt/binary >>}, 39 | io:format("Replying: ~p~n", [Reply]), 40 | {reply, Reply, State + 1}. 41 | 42 | websocket_info(start, _ConnState, State) -> 43 | {reply, {text, <<"erlang message received">>}, State}. 44 | 45 | websocket_terminate(Reason, _ConnState, State) -> 46 | io:format("Websocket closed in state ~p wih reason ~p~n", 47 | [State, Reason]), 48 | ok. 49 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [{src_dirs, ["src"]}, 2 | debug_info]}. 3 | {sub_dirs, []}. 4 | -------------------------------------------------------------------------------- /src/websocket_client.app.src: -------------------------------------------------------------------------------- 1 | {application, websocket_client, 2 | [ 3 | {description, "Erlang websocket client"}, 4 | {vsn, "0.6.1"}, 5 | {registered, []}, 6 | {applications, [ 7 | ssl, 8 | crypto, 9 | inets 10 | ]}, 11 | {env, []}, 12 | {modules, []} 13 | ] 14 | }. 15 | -------------------------------------------------------------------------------- /src/websocket_client.erl: -------------------------------------------------------------------------------- 1 | %% @author Jeremy Ong 2 | %% @doc Erlang websocket client 3 | -module(websocket_client). 4 | 5 | -export([start_link/3, 6 | start_link/4, 7 | cast/2, 8 | send/2 9 | ]). 10 | 11 | -export([ws_client_init/7]). 12 | 13 | -type opt() :: {async_start, boolean()} 14 | | {extra_headers, [{string() | binary(), string() | binary()}]} 15 | . 16 | 17 | -type opts() :: [opt()]. 18 | 19 | %% @doc Start the websocket client 20 | -spec start_link(URL :: string() | binary(), Handler :: module(), HandlerArgs :: list()) -> 21 | {ok, pid()} | {error, term()}. 22 | start_link(URL, Handler, HandlerArgs) -> 23 | start_link(URL, Handler, HandlerArgs, []). 24 | 25 | start_link(URL, Handler, HandlerArgs, AsyncStart) when is_boolean(AsyncStart) -> 26 | start_link(URL, Handler, HandlerArgs, [{async_start, AsyncStart}]); 27 | start_link(URL, Handler, HandlerArgs, Opts) when is_binary(URL) -> 28 | start_link(erlang:binary_to_list(URL), Handler, HandlerArgs, Opts); 29 | start_link(URL, Handler, HandlerArgs, Opts) when is_list(Opts) -> 30 | case http_uri:parse(URL, [{scheme_defaults, [{ws,80},{wss,443}]}]) of 31 | {ok, {Protocol, _, Host, Port, Path, Query}} -> 32 | proc_lib:start_link(?MODULE, ws_client_init, 33 | [Handler, Protocol, Host, Port, Path ++ Query, HandlerArgs, Opts]); 34 | {error, _} = Error -> 35 | Error 36 | end. 37 | 38 | %% Send a frame asynchronously 39 | -spec cast(Client :: pid(), Frame :: websocket_req:frame()) -> 40 | ok. 41 | cast(Client, Frame) -> 42 | Client ! {cast, Frame}, 43 | ok. 44 | 45 | %% @doc Create socket, execute handshake, and enter loop 46 | -spec ws_client_init(Handler :: module(), Protocol :: websocket_req:protocol(), 47 | Host :: string(), Port :: inet:port_number(), Path :: string(), 48 | Args :: list(), Opts :: opts()) -> 49 | no_return(). 50 | ws_client_init(Handler, Protocol, Host, Port, Path, Args, Opts) -> 51 | Transport = case Protocol of 52 | wss -> 53 | ssl; 54 | ws -> 55 | gen_tcp 56 | end, 57 | SockReply = case Transport of 58 | ssl -> 59 | ssl:connect(Host, Port, 60 | [{mode, binary}, 61 | {verify, verify_none}, 62 | {active, false}, 63 | {packet, 0} 64 | ], 6000); 65 | gen_tcp -> 66 | gen_tcp:connect(Host, Port, 67 | [binary, 68 | {active, false}, 69 | {packet, 0} 70 | ], 6000) 71 | end, 72 | {ok, Socket} = case SockReply of 73 | {ok, Sock} -> {ok, Sock}; 74 | {error, _} = ConnectError -> 75 | proc_lib:init_ack(ConnectError), 76 | exit(normal) 77 | end, 78 | WSReq = websocket_req:new( 79 | Protocol, 80 | Host, 81 | Port, 82 | Path, 83 | Socket, 84 | Transport, 85 | Handler, 86 | generate_ws_key() 87 | ), 88 | ExtraHeaders = proplists:get_value(extra_headers, Opts, []), 89 | case websocket_handshake(WSReq, ExtraHeaders) of 90 | {error, _} = HandshakeError -> 91 | proc_lib:init_ack(HandshakeError), 92 | exit(normal); 93 | {ok, Buffer} -> 94 | AsyncStart = proplists:get_value(async_start, Opts, true), 95 | AsyncStart andalso proc_lib:init_ack({ok, self()}), 96 | {ok, HandlerState, KeepAlive} = case Handler:init(Args, WSReq) of 97 | {ok, HS} -> 98 | {ok, HS, infinity}; 99 | {ok, HS, KA} -> 100 | {ok, HS, KA} 101 | end, 102 | AsyncStart orelse proc_lib:init_ack({ok, self()}), 103 | case Socket of 104 | {sslsocket, _, _} -> 105 | ssl:setopts(Socket, [{active, true}]); 106 | _ -> 107 | inet:setopts(Socket, [{active, true}]) 108 | end, 109 | %% Since we could have already received some data already, we simulate a Socket message. 110 | case Buffer of 111 | <<>> -> ok; 112 | _ -> self() ! {Transport, Socket, Buffer} 113 | end, 114 | KATimer = case KeepAlive of 115 | infinity -> 116 | undefined; 117 | _ -> 118 | erlang:send_after(KeepAlive, self(), keepalive) 119 | end, 120 | websocket_loop(websocket_req:set([{keepalive,KeepAlive},{keepalive_timer,KATimer}], WSReq), HandlerState, <<>>) 121 | end. 122 | 123 | %% @doc Send http upgrade request and validate handshake response challenge 124 | -spec websocket_handshake(WSReq :: websocket_req:req(), [{string(), string()}]) -> {ok, binary()} | {error, term()}. 125 | websocket_handshake(WSReq, ExtraHeaders) -> 126 | [Path, Host, Key, Transport, Socket] = 127 | websocket_req:get([path, host, key, transport, socket], WSReq), 128 | Handshake = ["GET ", Path, " HTTP/1.1\r\n" 129 | "Host: ", Host, "\r\n" 130 | "Connection: Upgrade\r\n" 131 | "Sec-WebSocket-Version: 13\r\n" 132 | "Sec-WebSocket-Key: ", Key, "\r\n" 133 | "Upgrade: websocket\r\n", 134 | [ [Header, ": ", Value, "\r\n"] || {Header, Value} <- ExtraHeaders], 135 | "\r\n"], 136 | Transport:send(Socket, Handshake), 137 | {ok, HandshakeResponse} = receive_handshake(<<>>, Transport, Socket), 138 | validate_handshake(HandshakeResponse, Key). 139 | 140 | %% @doc Blocks and waits until handshake response data is received 141 | -spec receive_handshake(Buffer :: binary(), 142 | Transport :: module(), 143 | Socket :: term()) -> 144 | {ok, binary()}. 145 | receive_handshake(Buffer, Transport, Socket) -> 146 | case re:run(Buffer, "\\r\\n\\r\\n") of 147 | {match, _} -> 148 | {ok, Buffer}; 149 | _ -> 150 | {ok, Data} = Transport:recv(Socket, 0, 6000), 151 | receive_handshake(<< Buffer/binary, Data/binary >>, 152 | Transport, Socket) 153 | end. 154 | 155 | %% @doc Send frame to server 156 | -spec send(websocket_req:frame(), websocket_req:req()) -> ok | {error, term()}. 157 | send(Frame, WSReq) -> 158 | Socket = websocket_req:socket(WSReq), 159 | Transport = websocket_req:transport(WSReq), 160 | Transport:send(Socket, encode_frame(Frame)). 161 | 162 | %% @doc Main loop 163 | -spec websocket_loop(WSReq :: websocket_req:req(), HandlerState :: any(), 164 | Buffer :: binary()) -> 165 | ok. 166 | websocket_loop(WSReq, HandlerState, Buffer) -> 167 | receive 168 | Message -> handle_websocket_message(WSReq, HandlerState, Buffer, Message) 169 | end. 170 | 171 | handle_websocket_message(WSReq, HandlerState, Buffer, Message) -> 172 | [Handler, Remaining, Socket] = 173 | websocket_req:get([handler, remaining, socket], WSReq), 174 | case Message of 175 | keepalive -> 176 | cancel_keepalive_timer(WSReq), 177 | ok = send({ping, <<>>}, WSReq), 178 | KATimer = erlang:send_after(websocket_req:keepalive(WSReq), self(), keepalive), 179 | websocket_loop(websocket_req:keepalive_timer(KATimer, WSReq), HandlerState, Buffer); 180 | {cast, Frame} -> 181 | ok = send(Frame, WSReq), 182 | websocket_loop(WSReq, HandlerState, Buffer); 183 | {_Closed, Socket} -> 184 | websocket_close(WSReq, HandlerState, remote); 185 | {_TransportType, Socket, Data} -> 186 | case Remaining of 187 | undefined -> 188 | retrieve_frame(WSReq, HandlerState, 189 | << Buffer/binary, Data/binary >>); 190 | _ -> 191 | retrieve_frame(WSReq, HandlerState, 192 | websocket_req:opcode(WSReq), Remaining, Data, Buffer) 193 | end; 194 | Msg -> 195 | try Handler:websocket_info(Msg, WSReq, HandlerState) of 196 | HandlerResponse -> 197 | handle_response(WSReq, HandlerResponse, Buffer) 198 | catch 199 | _:Reason -> 200 | websocket_close(WSReq, HandlerState, {handler, Reason}) 201 | end 202 | end. 203 | 204 | -spec cancel_keepalive_timer(websocket_req:req()) -> ok. 205 | cancel_keepalive_timer(WSReq) -> 206 | case websocket_req:keepalive_timer(WSReq) of 207 | undefined -> 208 | ok; 209 | OldTimer -> 210 | erlang:cancel_timer(OldTimer), 211 | ok 212 | end. 213 | 214 | -spec websocket_close(WSReq :: websocket_req:req(), 215 | HandlerState :: any(), 216 | Reason :: tuple()) -> ok. 217 | websocket_close(WSReq, HandlerState, Reason) -> 218 | Handler = websocket_req:handler(WSReq), 219 | try Handler:websocket_terminate(Reason, WSReq, HandlerState) of 220 | _ -> 221 | case Reason of 222 | normal -> ok; 223 | _ -> error_info(Handler, Reason, HandlerState) 224 | end, 225 | exit(Reason) 226 | catch 227 | _:Reason2 -> 228 | error_info(Handler, Reason2, HandlerState), 229 | exit(Reason2) 230 | end. 231 | 232 | error_info(Handler, Reason, State) -> 233 | error_logger:error_msg( 234 | "** Websocket handler ~p terminating~n" 235 | "** for the reason ~p~n" 236 | "** Handler state was ~p~n" 237 | "** Stacktrace: ~p~n~n", 238 | [Handler, Reason, State, erlang:get_stacktrace()]). 239 | 240 | %% @doc Key sent in initial handshake 241 | -spec generate_ws_key() -> 242 | binary(). 243 | generate_ws_key() -> 244 | base64:encode(crypto:strong_rand_bytes(16)). 245 | 246 | %% @doc Validate handshake response challenge 247 | -spec validate_handshake(HandshakeResponse :: binary(), Key :: binary()) -> {ok, binary()} | {error, term()}. 248 | validate_handshake(HandshakeResponse, Key) -> 249 | Challenge = base64:encode( 250 | crypto:hash(sha, << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)), 251 | %% Consume the response... 252 | {ok, Status, Header, Buffer} = consume_response(HandshakeResponse), 253 | {_Version, Code, Message} = Status, 254 | case Code of 255 | % 101 means Switching Protocol 256 | 101 -> 257 | %% ...and make sure the challenge is valid. 258 | Challenge = proplists:get_value(<<"Sec-Websocket-Accept">>, Header), 259 | {ok, Buffer}; 260 | _ -> {error, {Code, Message}} 261 | end. 262 | 263 | %% @doc Consumes the HTTP response and extracts status, header and the body. 264 | consume_response(Response) -> 265 | {ok, {http_response, Version, Code, Message}, Header} = erlang:decode_packet(http_bin, Response, []), 266 | consume_response({Version, Code, Message}, Header, []). 267 | 268 | consume_response(Status, Response, HeaderAcc) -> 269 | case erlang:decode_packet(httph_bin, Response, []) of 270 | {ok, {http_header, _Length, Field, _Reserved, Value}, Rest} -> 271 | consume_response(Status, Rest, [{Field, Value} | HeaderAcc]); 272 | 273 | {ok, http_eoh, Body} -> 274 | {ok, Status, HeaderAcc, Body} 275 | end. 276 | 277 | %% @doc Start or continue continuation payload with length less than 126 bytes 278 | retrieve_frame(WSReq, HandlerWSReq, 279 | << 0:4, Opcode:4, 0:1, Len:7, Rest/bits >>) 280 | when Len < 126 -> 281 | WSReq1 = set_continuation_if_empty(WSReq, Opcode), 282 | WSReq2 = websocket_req:fin(0, WSReq1), 283 | retrieve_frame(WSReq2, HandlerWSReq, Opcode, Len, Rest, <<>>); 284 | %% @doc Start or continue continuation payload with length a 2 byte int 285 | retrieve_frame(WSReq, HandlerWSReq, 286 | << 0:4, Opcode:4, 0:1, 126:7, Len:16, Rest/bits >>) 287 | when Len > 125, Opcode < 8 -> 288 | WSReq1 = set_continuation_if_empty(WSReq, Opcode), 289 | WSReq2 = websocket_req:fin(0, WSReq1), 290 | retrieve_frame(WSReq2, HandlerWSReq, Opcode, Len, Rest, <<>>); 291 | %% @doc Start or continue continuation payload with length a 64 bit int 292 | retrieve_frame(WSReq, HandlerWSReq, 293 | << 0:4, Opcode:4, 0:1, 127:7, 0:1, Len:63, Rest/bits >>) 294 | when Len > 16#ffff, Opcode < 8 -> 295 | WSReq1 = set_continuation_if_empty(WSReq, Opcode), 296 | WSReq2 = websocket_req:fin(0, WSReq1), 297 | retrieve_frame(WSReq2, HandlerWSReq, Opcode, Len, Rest, <<>>); 298 | %% @doc Length is less 126 bytes 299 | retrieve_frame(WSReq, HandlerWSReq, 300 | << 1:1, 0:3, Opcode:4, 0:1, Len:7, Rest/bits >>) 301 | when Len < 126 -> 302 | WSReq1 = websocket_req:fin(1, WSReq), 303 | retrieve_frame(WSReq1, HandlerWSReq, Opcode, Len, Rest, <<>>); 304 | %% @doc Length is a 2 byte integer 305 | retrieve_frame(WSReq, HandlerWSReq, 306 | << 1:1, 0:3, Opcode:4, 0:1, 126:7, Len:16, Rest/bits >>) 307 | when Len > 125, Opcode < 8 -> 308 | WSReq1 = websocket_req:fin(1, WSReq), 309 | retrieve_frame(WSReq1, HandlerWSReq, Opcode, Len, Rest, <<>>); 310 | %% @doc Length is a 64 bit integer 311 | retrieve_frame(WSReq, HandlerWSReq, 312 | << 1:1, 0:3, Opcode:4, 0:1, 127:7, 0:1, Len:63, Rest/bits >>) 313 | when Len > 16#ffff, Opcode < 8 -> 314 | WSReq1 = websocket_req:fin(1, WSReq), 315 | retrieve_frame(WSReq1, HandlerWSReq, Opcode, Len, Rest, <<>>); 316 | %% @doc Need more data to read length properly 317 | retrieve_frame(WSReq, HandlerWSReq, Data) -> 318 | websocket_loop(WSReq, HandlerWSReq, Data). 319 | 320 | %% @doc Length known and still missing data 321 | retrieve_frame(WSReq, HandlerWSReq, Opcode, Len, Data, Buffer) 322 | when byte_size(Data) < Len -> 323 | Remaining = Len - byte_size(Data), 324 | WSReq1 = websocket_req:remaining(Remaining, WSReq), 325 | WSReq2 = websocket_req:opcode(Opcode, WSReq1), 326 | websocket_loop(WSReq2, HandlerWSReq, << Buffer/bits, Data/bits >>); 327 | %% @doc Length known and remaining data is appended to the buffer 328 | retrieve_frame(WSReq, HandlerState, Opcode, Len, Data, Buffer) -> 329 | [Handler, Continuation, ContinuationOpcode] = 330 | websocket_req:get([handler, continuation, continuation_opcode], WSReq), 331 | Fin = websocket_req:fin(WSReq), 332 | << Payload:Len/binary, Rest/bits >> = Data, 333 | FullPayload = << Buffer/binary, Payload/binary >>, 334 | OpcodeName = websocket_req:opcode_to_name(Opcode), 335 | case OpcodeName of 336 | ping -> 337 | %% If a ping is received, send a pong automatically 338 | ok = send({pong, FullPayload}, WSReq); 339 | _ -> 340 | ok 341 | end, 342 | case OpcodeName of 343 | close when byte_size(FullPayload) >= 2 -> 344 | << CodeBin:2/binary, _ClosePayload/binary >> = FullPayload, 345 | Code = binary:decode_unsigned(CodeBin), 346 | Reason = case Code of 347 | % 1000 indicates a normal closure, meaning that the purpose for 348 | % which the connection was established has been fulfilled. 349 | 1000 -> normal; 350 | 351 | % 1001 indicates that an endpoint is "going away", such as a server 352 | % going down or a browser having navigated away from a page. 353 | 1001 -> normal; 354 | 355 | % See https://tools.ietf.org/html/rfc6455#section-7.4.1 356 | % for error code descriptions. 357 | _ -> {remote, Code} 358 | end, 359 | websocket_close(WSReq, HandlerState, Reason); 360 | close -> 361 | websocket_close(WSReq, HandlerState, remote); 362 | %% Non-control continuation frame 363 | _ when Opcode < 8, Continuation =/= undefined, Fin == 0 -> 364 | %% Append to previously existing continuation payloads and continue 365 | Continuation1 = << Continuation/binary, FullPayload/binary >>, 366 | WSReq1 = websocket_req:continuation(Continuation1, WSReq), 367 | retrieve_frame(WSReq1, HandlerState, Rest); 368 | %% Terminate continuation frame sequence with non-control frame 369 | _ when Opcode < 8, Continuation =/= undefined, Fin == 1 -> 370 | DefragPayload = << Continuation/binary, FullPayload/binary >>, 371 | WSReq1 = websocket_req:continuation(undefined, WSReq), 372 | WSReq2 = websocket_req:continuation_opcode(undefined, WSReq1), 373 | ContinuationOpcodeName = websocket_req:opcode_to_name(ContinuationOpcode), 374 | try Handler:websocket_handle( 375 | {ContinuationOpcodeName, DefragPayload}, 376 | WSReq2, HandlerState) of 377 | HandlerResponse -> 378 | handle_response(websocket_req:remaining(undefined, WSReq1), 379 | HandlerResponse, Rest) 380 | catch _:Reason -> 381 | websocket_close(WSReq, HandlerState, {handler, Reason}) 382 | end; 383 | _ -> 384 | try Handler:websocket_handle( 385 | {OpcodeName, FullPayload}, 386 | WSReq, HandlerState) of 387 | HandlerResponse -> 388 | handle_response(websocket_req:remaining(undefined, WSReq), 389 | HandlerResponse, Rest) 390 | catch _:Reason -> 391 | websocket_close(WSReq, HandlerState, {handler, Reason}) 392 | end 393 | end. 394 | 395 | %% @doc Handles return values from the callback module 396 | handle_response(WSReq, {reply, Frame, HandlerState}, Buffer) -> 397 | [Socket, Transport] = websocket_req:get([socket, transport], WSReq), 398 | case Transport:send(Socket, encode_frame(Frame)) of 399 | ok -> 400 | %% we can still have more messages in buffer 401 | case websocket_req:remaining(WSReq) of 402 | %% buffer should not contain uncomplete messages 403 | undefined -> 404 | retrieve_frame(WSReq, HandlerState, Buffer); 405 | %% buffer contain uncomplete message that shouldnt be parsed 406 | _ -> 407 | websocket_loop(WSReq, HandlerState, Buffer) 408 | end; 409 | {error, Reason} -> 410 | websocket_close(WSReq, HandlerState, {local, Reason}) 411 | end; 412 | handle_response(WSReq, {ok, HandlerState}, Buffer) -> 413 | %% we can still have more messages in buffer 414 | case websocket_req:remaining(WSReq) of 415 | %% buffer should not contain uncomplete messages 416 | undefined -> retrieve_frame(WSReq, HandlerState, Buffer); 417 | %% buffer contain uncomplete message that shouldnt be parsed 418 | _ -> websocket_loop(WSReq, HandlerState, Buffer) 419 | end; 420 | 421 | handle_response(WSReq, {close, Payload, HandlerState}, _) -> 422 | send({close, Payload}, WSReq), 423 | websocket_close(WSReq, HandlerState, normal). 424 | 425 | %% @doc Encodes the data with a header (including a masking key) and 426 | %% masks the data 427 | -spec encode_frame(websocket_req:frame()) -> 428 | binary(). 429 | encode_frame({Type, Payload}) -> 430 | Opcode = websocket_req:name_to_opcode(Type), 431 | Len = iolist_size(Payload), 432 | BinLen = payload_length_to_binary(Len), 433 | MaskingKeyBin = crypto:strong_rand_bytes(4), 434 | << MaskingKey:32 >> = MaskingKeyBin, 435 | Header = << 1:1, 0:3, Opcode:4, 1:1, BinLen/bits, MaskingKeyBin/bits >>, 436 | MaskedPayload = mask_payload(MaskingKey, Payload), 437 | << Header/binary, MaskedPayload/binary >>; 438 | encode_frame(Type) when is_atom(Type) -> 439 | encode_frame({Type, <<>>}). 440 | 441 | %% @doc The payload is masked using a masking key byte by byte. 442 | %% Can do it in 4 byte chunks to save time until there is left than 4 bytes left 443 | mask_payload(MaskingKey, Payload) -> 444 | mask_payload(MaskingKey, Payload, <<>>). 445 | mask_payload(_, <<>>, Acc) -> 446 | Acc; 447 | mask_payload(MaskingKey, << D:32, Rest/bits >>, Acc) -> 448 | T = D bxor MaskingKey, 449 | mask_payload(MaskingKey, Rest, << Acc/binary, T:32 >>); 450 | mask_payload(MaskingKey, << D:24 >>, Acc) -> 451 | << MaskingKeyPart:24, _:8 >> = << MaskingKey:32 >>, 452 | T = D bxor MaskingKeyPart, 453 | << Acc/binary, T:24 >>; 454 | mask_payload(MaskingKey, << D:16 >>, Acc) -> 455 | << MaskingKeyPart:16, _:16 >> = << MaskingKey:32 >>, 456 | T = D bxor MaskingKeyPart, 457 | << Acc/binary, T:16 >>; 458 | mask_payload(MaskingKey, << D:8 >>, Acc) -> 459 | << MaskingKeyPart:8, _:24 >> = << MaskingKey:32 >>, 460 | T = D bxor MaskingKeyPart, 461 | << Acc/binary, T:8 >>. 462 | 463 | %% @doc Encode the payload length as binary in a variable number of bits. 464 | %% See RFC Doc for more details 465 | payload_length_to_binary(Len) when Len =<125 -> 466 | << Len:7 >>; 467 | payload_length_to_binary(Len) when Len =< 16#ffff -> 468 | << 126:7, Len:16 >>; 469 | payload_length_to_binary(Len) when Len =< 16#7fffffffffffffff -> 470 | << 127:7, Len:64 >>. 471 | 472 | %% @doc If this is the first continuation frame, set the opcode and initialize 473 | %% continuation to an empty binary. Otherwise, return the request object untouched. 474 | -spec set_continuation_if_empty(WSReq :: websocket_req:req(), 475 | Opcode :: websocket_req:opcode()) -> 476 | websocket_req:req(). 477 | set_continuation_if_empty(WSReq, Opcode) -> 478 | case websocket_req:continuation(WSReq) of 479 | undefined -> 480 | WSReq1 = websocket_req:continuation_opcode(Opcode, WSReq), 481 | websocket_req:continuation(<<>>, WSReq1); 482 | _ -> 483 | WSReq 484 | end. 485 | -------------------------------------------------------------------------------- /src/websocket_client_handler.erl: -------------------------------------------------------------------------------- 1 | -module(websocket_client_handler). 2 | 3 | -type state() :: any(). 4 | -type keepalive() :: integer(). 5 | 6 | -type close_reason() :: 7 | % Either: 8 | % - The websocket was closed by a handler via a `{closed, Reason, State}` tuple 9 | % returned from websocket_handle/3 or websocket_info/3. 10 | % - A 'close' frame was received with code 1000 or 1001. 11 | normal | 12 | % The local end failed to send (see http://erlang.org/doc/man/gen_tcp.html#send-2 13 | % or http://erlang.org/doc/man/ssl.html#send-2). The second element in the 14 | % tuple is the same term that was wrapped in an `{error, Reason}` tuple by 15 | % `send/2`, i.e. `{error, closed}` will become `{local, closed}`, and not 16 | % `{local, {error, closed}}`. 17 | {local, term()} | 18 | % The remote end either closed abruptly, or closed after sending a 'close' frame 19 | % without a status code. 20 | remote | 21 | % The remote end closed with a status code (see https://tools.ietf.org/html/rfc6455#section-7.4.1). 22 | {remote, integer()} | 23 | % An asynchronous exception was raised during message handling, either in 24 | % websocket_handle/3 or websocket_info/3. The term raised is passed as the 25 | % second element in this tuple. 26 | {handler, term()}. 27 | 28 | -callback init(list(), websocket_req:req()) -> 29 | {ok, state()} 30 | | {ok, state(), keepalive()}. 31 | 32 | -callback websocket_handle({text | binary | ping | pong, binary()}, websocket_req:req(), state()) -> 33 | {ok, state()} 34 | | {reply, websocket_req:frame(), state()} 35 | | {close, binary(), state()}. 36 | 37 | -callback websocket_info(any(), websocket_req:req(), state()) -> 38 | {ok, state()} 39 | | {reply, websocket_req:frame(), state()} 40 | | {close, binary(), state()}. 41 | 42 | -callback websocket_terminate(close_reason(), websocket_req:req(), state()) -> ok. 43 | -------------------------------------------------------------------------------- /src/websocket_req.erl: -------------------------------------------------------------------------------- 1 | %% @doc Accessor module for the #websocket_req{} record. 2 | -module(websocket_req). 3 | 4 | -record(websocket_req, { 5 | protocol :: protocol(), 6 | host :: string(), 7 | port :: inet:port_number(), 8 | path :: string(), 9 | keepalive = infinity :: infinity | integer(), 10 | keepalive_timer = undefined :: undefined | reference(), 11 | socket :: inet:socket() | ssl:sslsocket(), 12 | transport :: module(), 13 | handler :: module(), 14 | key :: binary(), 15 | remaining = undefined :: undefined | integer(), 16 | fin = undefined :: undefined | fin(), 17 | opcode = undefined :: undefined | opcode(), 18 | continuation = undefined :: undefined | binary(), 19 | continuation_opcode = undefined :: undefined | opcode() 20 | }). 21 | 22 | -opaque req() :: #websocket_req{}. 23 | -export_type([req/0]). 24 | 25 | -type protocol() :: ws | wss. 26 | 27 | -type frame() :: close | ping | pong 28 | | {text | binary | close | ping | pong, binary()} 29 | | {close, 1000..4999, binary()}. 30 | 31 | -type opcode() :: 0 | 1 | 2 | 8 | 9 | 10. 32 | -export_type([protocol/0, opcode/0, frame/0]). 33 | 34 | -type fin() :: 0 | 1. 35 | -export_type([fin/0]). 36 | 37 | -export([new/8, 38 | protocol/2, protocol/1, 39 | host/2, host/1, 40 | port/2, port/1, 41 | path/2, path/1, 42 | keepalive/2, keepalive/1, 43 | keepalive_timer/2, keepalive_timer/1, 44 | socket/2, socket/1, 45 | transport/2, transport/1, 46 | handler/2, handler/1, 47 | key/2, key/1, 48 | remaining/2, remaining/1, 49 | fin/2, fin/1, 50 | opcode/2, opcode/1, 51 | continuation/2, continuation/1, 52 | continuation_opcode/2, continuation_opcode/1, 53 | get/2, set/2 54 | ]). 55 | 56 | -export([ 57 | opcode_to_name/1, 58 | name_to_opcode/1 59 | ]). 60 | 61 | -spec new(protocol(), string(), inet:port_number(), 62 | string(), inet:socket() | ssl:sslsocket(), 63 | module(), module(), binary()) -> req(). 64 | new(Protocol, Host, Port, Path, Socket, Transport, Handler, Key) -> 65 | #websocket_req{ 66 | protocol = Protocol, 67 | host = Host, 68 | port = Port, 69 | path = Path, 70 | socket = Socket, 71 | transport = Transport, 72 | handler = Handler, 73 | key = Key 74 | }. 75 | 76 | 77 | %% @doc Mapping from opcode to opcode name 78 | -spec opcode_to_name(opcode()) -> 79 | atom(). 80 | opcode_to_name(0) -> continuation; 81 | opcode_to_name(1) -> text; 82 | opcode_to_name(2) -> binary; 83 | opcode_to_name(8) -> close; 84 | opcode_to_name(9) -> ping; 85 | opcode_to_name(10) -> pong. 86 | 87 | %% @doc Mapping from opcode to opcode name 88 | -spec name_to_opcode(atom()) -> 89 | opcode(). 90 | name_to_opcode(continuation) -> 0; 91 | name_to_opcode(text) -> 1; 92 | name_to_opcode(binary) -> 2; 93 | name_to_opcode(close) -> 8; 94 | name_to_opcode(ping) -> 9; 95 | name_to_opcode(pong) -> 10. 96 | 97 | 98 | -spec protocol(req()) -> protocol(). 99 | protocol(#websocket_req{protocol = P}) -> P. 100 | 101 | -spec protocol(protocol(), req()) -> req(). 102 | protocol(P, Req) -> 103 | Req#websocket_req{protocol = P}. 104 | 105 | 106 | -spec host(req()) -> string(). 107 | host(#websocket_req{host = H}) -> H. 108 | 109 | -spec host(string(), req()) -> req(). 110 | host(H, Req) -> 111 | Req#websocket_req{host = H}. 112 | 113 | 114 | -spec port(req()) -> inet:port_number(). 115 | port(#websocket_req{port = P}) -> P. 116 | 117 | -spec port(inet:port_number(), req()) -> req(). 118 | port(P, Req) -> 119 | Req#websocket_req{port = P}. 120 | 121 | 122 | -spec path(req()) -> string(). 123 | path(#websocket_req{path = P}) -> P. 124 | 125 | -spec path(string(), req()) -> req(). 126 | path(P, Req) -> 127 | Req#websocket_req{path = P}. 128 | 129 | 130 | -spec keepalive(req()) -> integer(). 131 | keepalive(#websocket_req{keepalive = K}) -> K. 132 | 133 | -spec keepalive(integer(), req()) -> req(). 134 | keepalive(K, Req) -> 135 | Req#websocket_req{keepalive = K}. 136 | 137 | 138 | -spec keepalive_timer(req()) -> undefined | reference(). 139 | keepalive_timer(#websocket_req{keepalive_timer = K}) -> K. 140 | 141 | -spec keepalive_timer(reference(), req()) -> req(). 142 | keepalive_timer(K, Req) -> 143 | Req#websocket_req{keepalive_timer = K}. 144 | 145 | 146 | -spec socket(req()) -> inet:socket() | ssl:sslsocket(). 147 | socket(#websocket_req{socket = S}) -> S. 148 | 149 | -spec socket(inet:socket() | ssl:sslsocket(), req()) -> req(). 150 | socket(S, Req) -> 151 | Req#websocket_req{socket = S}. 152 | 153 | 154 | -spec transport(req()) -> module(). 155 | transport(#websocket_req{transport = T}) -> T. 156 | 157 | -spec transport(module(), req()) -> req(). 158 | transport(T, Req) -> 159 | Req#websocket_req{transport = T}. 160 | 161 | 162 | -spec handler(req()) -> module(). 163 | handler(#websocket_req{handler = H}) -> H. 164 | 165 | -spec handler(module(), req()) -> req(). 166 | handler(H, Req) -> 167 | Req#websocket_req{handler = H}. 168 | 169 | 170 | -spec key(req()) -> binary(). 171 | key(#websocket_req{key = K}) -> K. 172 | 173 | -spec key(binary(), req()) -> req(). 174 | key(K, Req) -> 175 | Req#websocket_req{key = K}. 176 | 177 | 178 | -spec remaining(req()) -> undefined | integer(). 179 | remaining(#websocket_req{remaining = R}) -> R. 180 | 181 | -spec remaining(undefined | integer(), req()) -> req(). 182 | remaining(R, Req) -> 183 | Req#websocket_req{remaining = R}. 184 | 185 | -spec fin(req()) -> fin(). 186 | fin(#websocket_req{fin = F}) -> F. 187 | 188 | -spec fin(fin(), req()) -> req(). 189 | fin(F, Req) -> 190 | Req#websocket_req{fin = F}. 191 | 192 | -spec opcode(req()) -> opcode(). 193 | opcode(#websocket_req{opcode = O}) -> O. 194 | 195 | -spec opcode(opcode(), req()) -> req(). 196 | opcode(O, Req) -> 197 | Req#websocket_req{opcode = O}. 198 | 199 | -spec continuation(req()) -> undefined | binary(). 200 | continuation(#websocket_req{continuation = C}) -> C. 201 | 202 | -spec continuation(undefined | binary(), req()) -> req(). 203 | continuation(C, Req) -> 204 | Req#websocket_req{continuation = C}. 205 | 206 | -spec continuation_opcode(req()) -> undefined | opcode(). 207 | continuation_opcode(#websocket_req{continuation_opcode = C}) -> C. 208 | 209 | -spec continuation_opcode(undefined | opcode(), req()) -> req(). 210 | continuation_opcode(C, Req) -> 211 | Req#websocket_req{continuation_opcode = C}. 212 | 213 | 214 | -spec get(atom(), req()) -> any(); ([atom()], req()) -> [any()]. 215 | get(List, Req) when is_list(List) -> 216 | [g(Atom, Req) || Atom <- List]; 217 | get(Atom, Req) when is_atom(Atom) -> 218 | g(Atom, Req). 219 | 220 | g(protocol, #websocket_req{protocol = Ret}) -> Ret; 221 | g(host, #websocket_req{host = Ret}) -> Ret; 222 | g(port, #websocket_req{port = Ret}) -> Ret; 223 | g(path, #websocket_req{path = Ret}) -> Ret; 224 | g(keepalive, #websocket_req{keepalive = Ret}) -> Ret; 225 | g(keepalive_timer, #websocket_req{keepalive_timer = Ret}) -> Ret; 226 | g(socket, #websocket_req{socket = Ret}) -> Ret; 227 | g(transport, #websocket_req{transport = Ret}) -> Ret; 228 | g(handler, #websocket_req{handler = Ret}) -> Ret; 229 | g(key, #websocket_req{key = Ret}) -> Ret; 230 | g(remaining, #websocket_req{remaining = Ret}) -> Ret; 231 | g(fin, #websocket_req{fin = Ret}) -> Ret; 232 | g(opcode, #websocket_req{opcode = Ret}) -> Ret; 233 | g(continuation, #websocket_req{continuation = Ret}) -> Ret; 234 | g(continuation_opcode, #websocket_req{continuation_opcode = Ret}) -> Ret. 235 | 236 | 237 | -spec set([{atom(), any()}], Req) -> Req when Req::req(). 238 | set([{protocol, Val} | Tail], Req) -> set(Tail, Req#websocket_req{protocol = Val}); 239 | set([{host, Val} | Tail], Req) -> set(Tail, Req#websocket_req{host = Val}); 240 | set([{port, Val} | Tail], Req) -> set(Tail, Req#websocket_req{port = Val}); 241 | set([{path, Val} | Tail], Req) -> set(Tail, Req#websocket_req{path = Val}); 242 | set([{keepalive, Val} | Tail], Req) -> set(Tail, Req#websocket_req{keepalive = Val}); 243 | set([{keepalive_timer, Val} | Tail], Req) -> set(Tail, Req#websocket_req{keepalive_timer = Val}); 244 | set([{socket, Val} | Tail], Req) -> set(Tail, Req#websocket_req{socket = Val}); 245 | set([{transport, Val} | Tail], Req) -> set(Tail, Req#websocket_req{transport = Val}); 246 | set([{handler, Val} | Tail], Req) -> set(Tail, Req#websocket_req{handler = Val}); 247 | set([{key, Val} | Tail], Req) -> set(Tail, Req#websocket_req{key = Val}); 248 | set([{remaining, Val} | Tail], Req) -> set(Tail, Req#websocket_req{remaining = Val}); 249 | set([{fin, Val} | Tail], Req) -> set(Tail, Req#websocket_req{fin = Val}); 250 | set([{opcode, Val} | Tail], Req) -> set(Tail, Req#websocket_req{opcode = Val}); 251 | set([{continuation, Val} | Tail], Req) -> set(Tail, Req#websocket_req{continuation = Val}); 252 | set([{continuation_opcode, Val} | Tail], Req) -> set(Tail, Req#websocket_req{continuation_opcode = Val}); 253 | set([], Req) -> Req. 254 | --------------------------------------------------------------------------------