Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

update WebSockets implementation to support RFC 6455

This change allows websocket connections to be set up between browsers
and the yaws server. RFC 6455 for WebSocket connections is supported,
in addition to the hybi working group RFC drafts 10 to 17.

The quickest way to try this out is by compiling yaws as usual, then
visiting /websockets_example.yaws at the default local installation
host. This can be done using Google Chrome 14+, Firefox 7+ or any
other browser supporting WebSocket version 8 or above. Information
about getting started with WebSockets using this implementation is
given in /websockets.yaws.

This drops support for the older draft RFCs, specifically those of the
hixie working group which were previously supported by yaws but are
significantly different from the hybi working group's specification.

The interface for using WebSocket with yaws has changed
somewhat. Instead of spawning a websocket owner process which
maintains a server loop such as that shown in the old
websockets_endpoint.yaws, the application developer now implements a
callback module such as those in src/basic_echo_callback.erl or
src/advanced_echo_callback.erl -- the difference being that the
advanced callback style is only necessary if you need advanced
features of WebSocket such as fragmented messages. One suggested way
to deploy your callback module and its dependencies is as part of an
application in an OTP release, with yaws as a dependency. Rebar can be
used to build the dependencies, fetch and build yaws, and create a
release which will ensure the modules are in the path of the runtime
system.

Most behaviour tested by the Autobahn test suite 0.43 pass when
configured to connect to the /websockets_autobahn_endpoint.yaws and
/websockets_example_endpoint.yaws over an unencrypted
connection. Significantly, websocket connection closing is not
implemented and the socket is left to be cleaned up by the Runtime
System when either the connection is lost or the owning processes
dies. Secondly, certain cases where websocket frames are fragmented
within UTF-8 code points cause the check for valid text type messages
to incorrectly fail the connection.

Subprotocols are not currently supported.

Augment yaws.tex with a new WebSocket Protocol chapter (Steve
Vinoski).
  • Loading branch information...
commit 39c8d4969550308a7eb81d9efa0af6fe8192976f 1 parent 8e3ec41
@jbothma jbothma authored vinoski committed
View
145 doc/yaws.tex
@@ -107,13 +107,13 @@ \chapter{Introduction}
\item Application Modules where virtual directory hierarchies can
be made.
\item Embedded mode
-\item WebSockets
+\item WebSockets (RFC 6455)
\end{enumerate}
\section{Prerequisites}
This document requires that the reader:
\begin{itemize}
-\item Is well acquainted with the \Erlang\ programming language
+\item Is well acquainted with the \Erlang\ programming language.
\item Understands basic Web technologies.
\end{itemize}
@@ -1663,6 +1663,10 @@ \section{All out/1 return values}
is \verb+{header, H}+ with the effect of accumulating the HTTP
header \verb+H+ for page \verb+Page+.
+\item \verb+{websocket, CallbackModule, Options}+ Tell \Yaws\ to use
+ \verb+CallbackModule+ as a WebSockets Protocol handler for traffic
+ on the client socket. See chapter \ref{websockets} for more details.
+
\item \verb+[ListOfValues]+ It is possible to return a list of the
above defined return values. Any occurrence of
\verb+stream_content+, \verb+get_more+, or \verb+page+ in this list
@@ -2672,5 +2676,142 @@ \section{Configuration Examples}
</server>
\end{verbatim}
+\chapter{WebSocket Protocol Support}
+\label{websockets}
+
+\Yaws\ supports the WebSocket Protocol (RFC 6455), which enables
+two-way communication between clients and web servers. \Yaws\ also
+provides support for working drafts of the WebSocket protocol,
+specifically drafts 10 to 17 of the hybi working group. No support for
+other drafts, such as those from the hixie working group, is provided.
+
+You can find example usage of the WebSocket Protocol in the file
+\verb+www/websockets_example.yaws+. This example, intended for use
+with any browser supporting RFC 6455, returns HTML and JavaScript that
+allow the client to establish a WebSocket connection to the
+server. These connections are handled by the code in
+\verb+www/websockets_example_endpoint.yaws+, which when invoked simply
+establishes \\ \verb+src/basic_echo_callback.erl+ as the WebSocket
+callback module for the connection.
+
+\section{WebSocket Callback Modules}
+
+A WebSocket callback module implements either the
+\verb+handle_message/1+ callback function or the
+\\ \verb+handle_message/2+ callback function, depending on whether
+it's a basic or advanced callback module.
+
+\subsection{Basic Callback Modules}
+
+The argument passed to \verb+handle_message/1+ callback function takes
+one of the following forms:
+
+\begin{itemize}
+
+\item \verb+{text, Text}+ --- the callback receives an unfragmented
+ text message.
+
+\item \verb+{binary, Message}+ --- the callback receives an
+ unfragmented binary message.
+
+\end{itemize}
+
+The \verb+handle_message/1+ callback function supplies one of the
+following as a return value:
+
+\begin{itemize}
+
+\item \verb+noreply+ --- do nothing, just wait for the next message.
+
+\item \verb+{reply, {Type, Data}}+ --- reply to the
+ message. \verb+Type+ must be either \verb+text+ or \verb+binary+ to
+ indicate the type of data in the reply message, and \verb+Data+ is
+ the reply message itself.
+
+\item \verb+{close, Reason}+ --- close the connection and exit the
+ handling process with \verb+Reason+. For a regular non-error close,
+ \verb+Reason+ should be the atom \verb+normal+.
+
+\end{itemize}
+
+To inform \Yaws\ of the details of your callback module, return
+\verb+{websocket, CallbackModule, Options}+ from your \verb+out/1+
+function, where \verb+CallbackModule+ is the name of your callback
+module and \verb+Options+ is a list of options. The following options
+are available:
+
+\begin{itemize}
+
+\item \verb+{callback, CallbackType}+ --- supply this atom to indicate
+ the type of the callback module. \\ \verb+CallbackType+ can be
+ either of the following:
+
+\begin{itemize}
+
+\item \verb+basic+ --- specify this to indicate your callback module
+ is the basic type. This is the default.
+
+\item \verb+{advanced, InitialState}+ --- specify this to indicate
+ your callback module is an advanced callback module. Here,
+ \verb+InitialState+ is the callback's initial state for handling
+ this client. See \ref{advanced_ws} for more details.
+
+\end{itemize}
+
+\item \verb+{origin, Origin}+ --- specify the \verb+Origin+ URL from
+ which messages will be accepted. This is useful for protecting
+ against cross-site attacks. This option defaults to \verb+any+,
+ meaning calls will be accepted from any origin.
+
+\end{itemize}
+
+\subsection{Advanced Callback Modules}
+\label{advanced_ws}
+
+Advanced callback modules---those that want to supply their own
+initial state and are prepared to handle fragmented messages
+themselves---supply a \verb+handle_message/2+ callback function.
+
+To indicate an advanced callback module, include
+\verb+{callback, {advanced, InitialState}}+ in the \verb+Options+ list
+when you return \verb+{websocket, CallbackModule, Options}+ from your
+\verb+out/1+ function, as described above.
+
+The arguments to the \verb+handle_message/2+ callback
+function are as follows:
+
+\begin{itemize}
+
+\item \verb+#ws_frame_info+ --- this record, defined in
+ \verb+include/yaws_api.hrl+, provides all details of a frame
+ section. See section 5 of RFC 6455 for details.
+
+\item \verb+State+ --- this is the callback module's current
+ state. The initial state is supplied when you return
+ \verb+{callback, {advanced, InitialState}}+ as part of the options
+ list you returned from your \verb+out/1+ function to establish the
+ WebSocket callback module.
+
+\end{itemize}
+
+The return values for the \verb+handle_message/2+ callback function
+can be any of the following:
+
+\begin{itemize}
+
+\item \verb+{noreply, State}+ --- do nothing, just wait for the next
+ message. \verb+State+ is the (possibly updated) state for the
+ callback module.
+
+\item \verb+{reply, Reply, State}+ --- reply to the received message
+ with \verb+Reply+, which is either \verb+{text, Data}+ or
+ \verb+{binary, Data}+. \verb+State+ is the (possibly updated) state
+ for the callback module.
+
+\item \verb+{close, Reason}+ --- close the connection and exit the
+ handling process with \verb+Reason+. For a regular non-error close,
+ \verb+Reason+ should be the atom \verb+normal+.
+
+\end{itemize}
\end{document}
View
33 include/yaws_api.hrl
@@ -112,6 +112,33 @@
%% to append to the url
}).
-
-
-
+%% Corresponds to the frame sections as in
+%% http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-08#section-4
+%% plus 'data' and 'ws_state'
+-record(ws_frame_info, {
+ fin,
+ rsv,
+ opcode,
+ masked,
+ masking_key,
+ length,
+ payload,
+ data, %% The unmasked payload. Makes payload redundant.
+ ws_state %% The ws_state after unframing this frame.
+ %% This is useful for the endpoint to know what type of
+ %% fragment a potentially fragmented message is.
+ }).
+
+%----------------------------------------------------------------------
+% The state of a WebSocket connection.
+% This is held by the ws owner process and passed in calls to yaws_api.
+%----------------------------------------------------------------------
+-type frag_type() :: text
+ | binary
+ | none. %% The WebSocket is not expecting continuation
+ %% of any fragmented message.
+-record(ws_state, {
+ vsn :: integer(), % WebSocket version number
+ sock, % gen_tcp or gen_ssl socket
+ frag_type :: frag_type()
+ }).
View
74 src/advanced_echo_callback.erl
@@ -0,0 +1,74 @@
+%%%==============================================================
+%%% compiled using erlc -I include src/advanced_echo_callback.erl
+%%%==============================================================
+
+-module(advanced_echo_callback).
+
+-export([handle_message/2]).
+
+-include("yaws_api.hrl").
+
+%% define callback state to accumulate a fragmented WS message
+%% which we echo back when all fragments are in, returning to
+%% initial state.
+-record(state, {frag_type = none, % fragment type
+ acc = <<>>}). % accumulate fragment data
+
+%% start of a fragmented message
+handle_message(#ws_frame_info{fin=0,
+ opcode=FragType,
+ data=Data},
+ #state{frag_type=none, acc = <<>>}) ->
+ {noreply, #state{frag_type=FragType, acc=Data}};
+
+%% non-final continuation of a fragmented message
+handle_message(#ws_frame_info{fin=0,
+ data=Data,
+ opcode=continuation},
+ #state{frag_type = FragType, acc = Acc}) ->
+ {noreply, #state{frag_type=FragType, acc = <<Acc/binary,Data/binary>>}};
+
+%% end of text fragmented message
+handle_message(#ws_frame_info{fin=1,
+ opcode=continuation,
+ data=Data},
+ #state{frag_type=text, acc=Acc}) ->
+ Unfragged = <<Acc/binary, Data/binary>>,
+ {reply, {text, Unfragged}, #state{frag_type=none, acc = <<>>}};
+
+%% one full non-fragmented message
+handle_message(#ws_frame_info{opcode=text, data=Data}, State) ->
+ {reply, {text, Data}, State};
+
+%% end of binary fragmented message
+handle_message(#ws_frame_info{fin=1,
+ opcode=continuation,
+ data=Data},
+ #state{frag_type=binary, acc=Acc}) ->
+ Unfragged = <<Acc/binary, Data/binary>>,
+ io:format("echoing back binary message~n",[]),
+ {reply, {binary, Unfragged}, #state{frag_type=none, acc = <<>>}};
+
+%% one full non-fragmented binary message
+handle_message(#ws_frame_info{opcode=binary,
+ data=Data},
+ State) ->
+ io:format("echoing back binary message~n",[]),
+ {reply, {binary, Data}, State};
+
+handle_message(#ws_frame_info{opcode=ping,
+ data=Data},
+ State) ->
+ io:format("replying pong to ping~n",[]),
+ {reply, {pong, Data}, State};
+
+handle_message(#ws_frame_info{opcode=pong}, State) ->
+ %% A response to an unsolicited pong frame is not expected.
+ %% http://tools.ietf.org/html/\
+ %% draft-ietf-hybi-thewebsocketprotocol-08#section-4
+ io:format("ignoring unsolicited pong~n",[]),
+ {noreply, State};
+
+handle_message(#ws_frame_info{}=FrameInfo, State) ->
+ io:format("WS Endpoint Unhandled message: ~p~n~p~n", [FrameInfo, State]),
+ {close, {error, {unhandled_message, FrameInfo}}}.
View
36 src/basic_echo_callback.erl
@@ -0,0 +1,36 @@
+%%%===========================================================
+%%% compiled using erlc -I include src/basic_echo_callback.erl
+%%%===========================================================
+
+-module(basic_echo_callback).
+
+%% Export for websocket callbacks
+-export([handle_message/1]).
+
+%% Export for apply
+-export([say_hi/1]).
+
+handle_message({text, <<"bye">>}) ->
+ io:format("User said bye.~n", []),
+ {close, normal};
+
+handle_message({text, <<"something">>}) ->
+ io:format("Some action without a reply~n", []),
+ noreply;
+
+handle_message({text, <<"say hi later">>}) ->
+ io:format("saying hi in 3s.~n", []),
+ timer:apply_after(3000, ?MODULE, say_hi, [self()]),
+ {reply, {text, <<"I'll say hi in a bit...">>}};
+
+handle_message({text, Message}) ->
+ io:format("basic echo handler got ~p~n", [Message]),
+ {reply, {text, <<Message/binary>>}};
+
+handle_message({binary, Message}) ->
+ {reply, {binary, Message}}.
+
+
+say_hi(Pid) ->
+ io:format("asynchronous greeting~n", []),
+ yaws_api:websocket_send(Pid, {text, <<"hi there!">>}).
View
37 src/yaws_api.erl
@@ -37,8 +37,7 @@
stream_chunk_end/1]).
-export([stream_process_deliver/2, stream_process_deliver_chunk/2,
stream_process_deliver_final_chunk/2, stream_process_end/2]).
--export([websocket_send/2, websocket_receive/1,
- websocket_unframe_data/1, websocket_setopts/2]).
+-export([websocket_send/2]).
-export([new_cookie_session/1, new_cookie_session/2, new_cookie_session/3,
cookieval_to_opaque/1, request_url/1,
print_cookie_sessions/0,
@@ -995,37 +994,9 @@ stream_process_end(Sock, YawsPid) ->
YawsPid ! endofstreamcontent.
-websocket_send(Socket, IoList) ->
- DataFrame = [0, IoList, 255],
- case Socket of
- {sslsocket,_,_} ->
- ssl:send(Socket, DataFrame);
- _ ->
- gen_tcp:send(Socket, DataFrame)
- end.
-
-websocket_receive(Socket) ->
- R = case Socket of
- {sslsocket,_,_} ->
- ssl:recv(Socket, 0);
- _ ->
- gen_tcp:recv(Socket, 0)
- end,
- case R of
- {ok, DataFrames} ->
- ReceivedMsgs = yaws_websockets:unframe_all(DataFrames, []),
- {ok, ReceivedMsgs};
- _ -> R
- end.
-
-websocket_unframe_data(DataFrameBin) ->
- {ok, Msg, <<>>} = yaws_websockets:unframe_one(DataFrameBin),
- Msg.
-
-websocket_setopts({sslsocket,_,_}=Socket, Opts) ->
- ssl:setopts(Socket, Opts);
-websocket_setopts(Socket, Opts) ->
- inet:setopts(Socket, Opts).
+%% Pid must the the process in control of the websocket connection.
+websocket_send(Pid, {Type, Data}) ->
+ yaws_websockets:send(Pid, {Type, Data}).
%% Return new cookie string
View
6 src/yaws_server.erl
@@ -2677,10 +2677,10 @@ deliver_dyn_part(CliSock, % essential params
Priv = deliver_accumulated(Arg, CliSock,
no, undefined, stream),
wait_for_streamcontent_pid(Priv, CliSock, Pid);
- {websocket, OwnerPid, SocketMode} ->
+ {websocket, CallbackMod, Opts} ->
%% The handshake passes control over the socket to OwnerPid
%% and terminates the Yaws worker!
- yaws_websockets:handshake(Arg, OwnerPid, SocketMode);
+ yaws_websockets:start(Arg, CallbackMod, Opts);
_ ->
DeliverCont(Arg)
end.
@@ -3123,7 +3123,7 @@ handle_out_reply({streamcontent_from_pid, MimeType, Pid},
yaws:outh_set_content_type(MimeType),
{streamcontent_from_pid, MimeType, Pid};
-handle_out_reply({websocket, _OwnerPid, _SocketMode}=Reply,
+handle_out_reply({websocket, _CallbackMod, _Opts}=Reply,
_LineNo,_YawsFile, _UT, _ARG) ->
yaws:accumulate_header({connection, erase}),
Reply;
View
653 src/yaws_websockets.erl
@@ -14,119 +14,552 @@
-include("yaws_debug.hrl").
-include_lib("kernel/include/file.hrl").
--export([handshake/3, unframe_one/1, unframe_all/2]).
-handshake(Arg, ContentPid, SocketMode) ->
+-define(MAX_PAYLOAD, 16777216). %16MB
+
+%% API
+-export([start/3, send/2]).
+
+%% Exported for spawn
+-export([receive_control/4]).
+
+start(Arg, CallbackMod, Opts) ->
+ SC = get(sc),
+ CliSock = Arg#arg.clisock,
+ PrepdOpts = preprocess_opts(Opts),
+ OwnerPid = spawn(?MODULE, receive_control,
+ [Arg, SC, CallbackMod, PrepdOpts]),
+ CliSock = Arg#arg.clisock,
+ case SC#sconf.ssl of
+ undefined ->
+ inet:setopts(CliSock, [{packet, raw}, {active, once}]),
+ TakeOverResult =
+ gen_tcp:controlling_process(CliSock, OwnerPid);
+ _ ->
+ ssl:setopts(CliSock, [{packet, raw}, {active, once}]),
+ TakeOverResult =
+ ssl:controlling_process(CliSock, OwnerPid)
+ end,
+ case TakeOverResult of
+ ok ->
+ OwnerPid ! ok,
+ exit(normal);
+ {error, Reason} ->
+ OwnerPid ! {error, Reason},
+ exit({websocket, Reason})
+ end.
+
+send(#ws_state{sock=Socket, vsn=ProtoVsn}, {Type, Data}) ->
+ DataFrame = frame(ProtoVsn, Type, Data),
+ case Socket of
+ {sslsocket,_,_} ->
+ ssl:send(Socket, DataFrame);
+ _ ->
+ gen_tcp:send(Socket, DataFrame)
+ end;
+
+send(Pid, {Type, Data}) ->
+ Pid ! {send, {Type, Data}}.
+
+preprocess_opts(GivenOpts) ->
+ Fun = fun({Key, Default}, Opts) ->
+ case lists:keyfind(Key, 1, Opts) of
+ false ->
+ [{Key, Default}|Opts];
+ _ -> Opts
+ end
+ end,
+ Defaults = [{origin, any},
+ {callback, basic}],
+ lists:foldl(Fun, GivenOpts, Defaults).
+
+receive_control(Arg, SC, CallbackMod, Opts) ->
+ receive
+ ok ->
+ handshake(Arg, SC, CallbackMod, Opts);
+ {error, Reason} ->
+ exit(Reason)
+ end.
+
+handshake(Arg, SC, CallbackMod, Opts) ->
CliSock = Arg#arg.clisock,
- case get_origin_header(Arg#arg.headers) of
- undefined ->
- %% Yaws will take care of closing the socket
- ContentPid ! discard;
- Origin ->
- ProtocolVersion = ws_version(Arg#arg.headers),
- Protocol = get_protocol_header(Arg#arg.headers),
- Host = (Arg#arg.headers)#headers.host,
- {abs_path, Path} = (Arg#arg.req)#http_request.path,
- SC = get(sc),
- WebSocketLocation =
- case SC#sconf.ssl of
+ OriginOpt = lists:keyfind(origin, 1, Opts),
+ Origin = get_origin_header(Arg#arg.headers),
+ case origin_check(Origin, OriginOpt) of
+ {error, Error} ->
+ error_logger:error_msg(Error),
+ exit({error, Error});
+ ok ->
+ ProtocolVersion = ws_version(Arg#arg.headers),
+ Protocol = get_protocol_header(Arg#arg.headers),
+ Host = (Arg#arg.headers)#headers.host,
+ {abs_path, Path} = (Arg#arg.req)#http_request.path,
+
+ WebSocketLocation =
+ case SC#sconf.ssl of
undefined -> "ws://" ++ Host ++ Path;
_ -> "wss://" ++ Host ++ Path
- end,
- Handshake = handshake(ProtocolVersion, Arg, CliSock,
+ end,
+
+ Handshake = handshake(ProtocolVersion, Arg, CliSock,
WebSocketLocation, Origin, Protocol),
- case SC#sconf.ssl of
- undefined ->
- gen_tcp:send(CliSock, Handshake),
- inet:setopts(CliSock, [{packet, raw},{active, SocketMode}]),
- TakeOverResult =
- gen_tcp:controlling_process(CliSock, ContentPid);
- _ ->
- ssl:send(CliSock, Handshake),
- ssl:setopts(CliSock, [{packet, raw}, {active, SocketMode}]),
- TakeOverResult =
- ssl:controlling_process(CliSock, ContentPid)
- end,
- case TakeOverResult of
- ok ->
- ContentPid ! {ok, CliSock};
- {error, Reason} ->
- ContentPid ! discard,
- exit({websocket, Reason})
- end
- end,
- exit(normal).
-
-handshake(ws_76, Arg, CliSock, WebSocketLocation, Origin, Protocol) ->
- {ok, Challenge} = case CliSock of
- {sslsocket, _, _} ->
- ssl:recv(CliSock, 8);
- _ ->
- gen_tcp:recv(CliSock, 8)
- end,
- Key1 = secret_key("sec-websocket-key1", Arg#arg.headers),
- Key2 = secret_key("sec-websocket-key2", Arg#arg.headers),
- ChallengeResponse = challenge(Key1, Key2, binary_to_list(Challenge)),
- ["HTTP/1.1 101 Web Socket Protocol Handshake\r\n",
- "Upgrade: WebSocket\r\n",
- "Connection: Upgrade\r\n",
- "Sec-WebSocket-Origin: ", Origin, "\r\n",
- "Sec-WebSocket-Location: ", WebSocketLocation, "\r\n",
- "Sec-WebSocket-Protocol: ", Protocol, "\r\n",
- "\r\n", ChallengeResponse];
-
-handshake(ws_75, _Arg, _CliSock, WebSocketLocation, Origin, _Protocol) ->
- ["HTTP/1.1 101 Web Socket Protocol Handshake\r\n",
- "Upgrade: WebSocket\r\n",
+ gen_tcp:send(CliSock, Handshake), % TODO: use the yaws way of
+ % supporting normal
+ % and ssl sockets
+ {callback, CallbackType} = lists:keyfind(callback, 1, Opts),
+ WSState = #ws_state{sock = CliSock,
+ vsn = ProtocolVersion,
+ frag_type = none},
+ CallbackState = case CallbackType of
+ basic ->
+ {none, <<>>};
+ {advanced, InitialState} ->
+ InitialState
+ end,
+ loop(CallbackMod, WSState, CallbackState, CallbackType)
+ end.
+
+origin_check(_Origin, {origin, any}) ->
+ ok;
+origin_check(Actual, {origin, _Expected=Actual}) ->
+ ok;
+origin_check(Actual, {origin, Expected}) ->
+ Error = io_lib:format("Expected origin ~p but found ~p.",
+ [Expected, Actual]),
+ {error, Error}.
+
+handshake(8, Arg, _CliSock, _WebSocketLocation, _Origin, _Protocol) ->
+ Key = get_nonce_header(Arg#arg.headers),
+ AcceptHash = hash_nonce(Key),
+ ["HTTP/1.1 101 Switching Protocols\r\n",
+ "Upgrade: websocket\r\n",
"Connection: Upgrade\r\n",
- "WebSocket-Origin: ", Origin, "\r\n",
- "WebSocket-Location: ", WebSocketLocation, "\r\n",
+ "Sec-WebSocket-Accept: ", AcceptHash , "\r\n",
"\r\n"].
+loop(CallbackMod, #ws_state{sock=Socket}=WSState, CallbackState, CallbackType) ->
+ receive
+ {send, {Type, Data}} ->
+ send(WSState, {Type, Data}),
+ loop(CallbackMod, WSState, CallbackState, CallbackType);
+ {tcp, Socket, FirstPacket} ->
+ FrameInfos = unframe_active_once(WSState, FirstPacket),
+ case CallbackType of
+ basic ->
+ {BasicMessages, NewCallbackState} =
+ basic_messages(FrameInfos, CallbackState),
+ CallbackResults = lists:map(
+ fun(M) ->
+ CallbackMod:handle_message(M)
+ end,
+ BasicMessages),
+ lists:map(handle_result_fun(WSState), CallbackResults);
+ {advanced,_} ->
+ NewCallbackState = lists:foldl(
+ do_callback_fun(WSState, CallbackMod),
+ CallbackState,
+ FrameInfos)
+ end,
+ Last = lists:last(FrameInfos),
+ NewWSState = Last#ws_frame_info.ws_state,
+ loop(CallbackMod, NewWSState, NewCallbackState, CallbackType);
+ {tcp_closed, Socket} ->
+ ok;
+ _Any ->
+ loop(CallbackMod, WSState, CallbackState, CallbackType)
+ end.
+
+handle_result_fun(WSState) ->
+ fun(Result) ->
+ case Result of
+ {reply, {Type, Data}} ->
+ send(WSState, {Type, Data});
+ noreply ->
+ ok;
+ {close, Reason} ->
+ exit(Reason)
+
+ end
+ end.
+
+do_callback_fun(WSState, CallbackMod) ->
+ fun(FrameInfo, CallbackState) ->
+ case CallbackMod:handle_message(FrameInfo, CallbackState) of
+ {reply, {Type, Data}, NewCallbackState} ->
+ send(WSState, {Type, Data}),
+ NewCallbackState;
+ {noreply, NewCallbackState} ->
+ NewCallbackState;
+ {close, Reason} ->
+ exit(Reason)
+ end
+ end.
+
+basic_messages(FrameInfos, {FragType, FragAcc}) ->
+ {Messages, NewFragType, NewFragAcc}
+ = lists:foldl(fun handle_message/2, {[], FragType, FragAcc}, FrameInfos),
+ {Messages, {NewFragType, NewFragAcc}}.
+
+%% start of a fragmented message
+handle_message(#ws_frame_info{fin=0,
+ opcode=FragType,
+ data=Data},
+ {Messages, none, <<>>}) ->
+ {Messages, FragType, Data};
+
+%% non-final continuation of a fragmented message
+handle_message(#ws_frame_info{fin=0,
+ data=Data,
+ opcode=continuation},
+ {Messages, FragType, FragAcc}) ->
+ {Messages, FragType, <<FragAcc/binary,Data/binary>>};
+
+%% end of text fragmented message
+handle_message(#ws_frame_info{fin=1,
+ opcode=continuation,
+ data=Data},
+ {Messages, text, FragAcc}) ->
+ Unfragged = <<FragAcc/binary, Data/binary>>,
+ NewMessage = {text, Unfragged},
+ {[NewMessage | Messages], none, <<>>};
+
+%% unfragmented text message
+handle_message(#ws_frame_info{opcode=text, data=Data},
+ {Messages, none, <<>>}) ->
+ NewMessage = {text, Data},
+ {[NewMessage | Messages], none, <<>>};
+
+%% end of binary fragmented message
+handle_message(#ws_frame_info{fin=1,
+ opcode=continuation,
+ data=Data},
+ {Messages, binary, FragAcc}) ->
+ Unfragged = <<FragAcc/binary, Data/binary>>,
+ NewMessage = {binary, Unfragged},
+ {[NewMessage|Messages], none, <<>>};
+
+handle_message(#ws_frame_info{opcode=binary,
+ data=Data},
+ {Messages, none, <<>>}) ->
+ NewMessage = {binary, Data},
+ {[NewMessage|Messages], none, <<>>};
+
+handle_message(#ws_frame_info{opcode=ping,
+ data=Data,
+ ws_state=State},
+ Acc) ->
+ send(State, {pong, Data}),
+ Acc;
+
+handle_message(#ws_frame_info{opcode=pong}, Acc) ->
+ %% A response to an unsolicited pong frame is not expected.
+ %% http://tools.ietf.org/html/\
+ %% draft-ietf-hybi-thewebsocketprotocol-08#section-4
+ Acc;
+
+handle_message(#ws_frame_info{}, Acc) ->
+ Acc.
+
ws_version(Headers) ->
- case query_header("sec-websocket-key1", Headers) of
- undefined -> ws_75;
- _ -> ws_76
+ VersionVal = query_header("sec-websocket-version", Headers),
+ case VersionVal of
+ "8" -> 8;
+ "13" -> 8 % treat 13 like 8. Right now 13 support is as good as that
+ % of 8, according to autobahn 0.4.3
end.
-%% This should take care of all the Data Framing scenarios specified in
-%% http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-66#page-26
-unframe_one(DataFrames) ->
- <<Type, _/bitstring>> = DataFrames,
- case Type of
- T when (T =< 127) ->
- %% {ok, FF_Ended_Frame} = re:compile("^.(.*)\\xFF(.*?)", [ungreedy]),
- FF_Ended_Frame = {re_pattern,2,0,
- <<69,82,67,80,71,0,0,0,16,2,0,0,5,0,0,0,2,0,0,0,
- 0,0,255,2,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,0,
- 27,25,12,94,0,7,0,1,57,12,84,0,7,27,255,94,0,7,
- 0,2,56,12,84,0,7,84,0,27,0>>},
- {match, [Data, NextFrame]} =
- re:run(DataFrames, FF_Ended_Frame,
- [{capture, all_but_first, binary}]),
- {ok, Data, NextFrame};
-
- _ -> %% Type band 16#80 =:= 16#80
- {Length, LenBytes} = unpack_length(DataFrames, 0, 0),
- <<_, _:LenBytes/bytes, Data:Length/bytes,
- NextFrame/bitstring>> = DataFrames,
- {ok, Data, NextFrame}
+buffer(Socket, Len, Buffered) ->
+ case Buffered of
+ <<_Expected:Len/binary>> = Return -> % exactly enough
+ %% debug(val, {buffering, "got:", Len}),
+ Return;
+ <<_Expected:Len/binary,_Extra/binary>> = Return-> % more than expected
+ %% debug(val, {buffering, "got:", Len, "and more!"}),
+ Return;
+ _ ->
+ %% not enough
+ %% debug(val, {buffering, "need:", Len, "waiting for more..."}),
+ %% TODO: take care of ssl sockets
+ Needed = Len - binary_length(Buffered),
+ {ok, More} = gen_tcp:recv(Socket, Needed),
+ <<Buffered/binary, More/binary>>
end.
-unframe_all(<<>>, Acc) ->
- lists:reverse(Acc);
-unframe_all(DataFramesBin, Acc) ->
- {ok, Msg, Rem} = unframe_one(DataFramesBin),
- unframe_all(Rem, [Msg|Acc]).
+binary_length(<<>>) ->
+ 0;
+binary_length(<<_First:1/binary, Rest/binary>>) ->
+ 1 + binary_length(Rest).
+
+
+checks(Unframed) ->
+ check_reserved_bits(Unframed).
+
+check_control_frame(Len, Opcode, Fin) ->
+ if
+ (Len > 125) and (Opcode > 7) ->
+ %% http://tools.ietf.org/html/\
+ %% draft-ietf-hybi-thewebsocketprotocol-08#section-4.5
+ {fail_connection, "control frame > 125 bytes"};
+ (Fin == 0) and (Opcode > 7) ->
+ {fail_connection, "control frame may not be fragmented"};
+ true ->
+ ok
+ end.
+
+%% no extensions are supported yet.
+%% http://tools.ietf.org/html/\
+%% draft-ietf-hybi-thewebsocketprotocol-08#section-4.2
+check_reserved_bits(Unframed = #ws_frame_info{rsv=0}) ->
+ check_utf8(Unframed);
+check_reserved_bits(#ws_frame_info{rsv=RSV}) ->
+ {fail_connection,
+ "rsv bits were " ++ integer_to_list(RSV) ++ " but should be unset."}.
+
+%% http://www.erlang.org/doc/apps/stdlib/unicode_usage.html#id191467
+%% Heuristic identification of UTF-8
+check_utf8(Unframed = #ws_frame_info{opcode = text, data=Bin})
+ when is_binary(Bin) ->
+ case unicode:characters_to_binary(Bin,utf8,utf8) of
+ Bin ->
+ Unframed;
+ _ ->
+ {fail_connection, "not valid utf-8."}
+ end;
+check_utf8(Unframed) ->
+ check_reserved_opcode(Unframed).
+
+check_reserved_opcode(#ws_frame_info{opcode = undefined}) ->
+ {fail_connection, "Reserved opcode."};
+check_reserved_opcode(Unframed) ->
+ Unframed.
+
+
+ws_frame_info(#ws_state{sock=Socket},
+ <<Fin:1, Rsv:3, Opcode:4, Masked:1, Len1:7, Rest/binary>>) ->
+ case check_control_frame(Len1, Opcode, Fin) of
+ ok ->
+ {ws_frame_info_secondary, Length, MaskingKey, Payload, Excess}
+ = ws_frame_info_secondary(Socket, Len1, Rest),
+ FrameInfo = #ws_frame_info{fin=Fin,
+ rsv=Rsv,
+ opcode=opcode_to_atom(Opcode),
+ masked=Masked,
+ masking_key=MaskingKey,
+ length=Length,
+ payload=Payload},
+ {FrameInfo, Excess};
+ Other ->
+ Other
+ end;
+
+ws_frame_info(State = #ws_state{sock=Socket}, FirstPacket) ->
+ ws_frame_info(State, buffer(Socket, 2,FirstPacket)).
+
+ws_frame_info_secondary(Socket, Len1, Rest) ->
+ case Len1 of
+ 126 ->
+ <<Len:16, MaskingKey:4/binary, Rest2/binary>> =
+ buffer(Socket, 6, Rest);
+ 127 ->
+ <<Len:64, MaskingKey:4/binary, Rest2/binary>> =
+ buffer(Socket, 12, Rest);
+ Len ->
+ <<MaskingKey:4/binary, Rest2/binary>> = buffer(Socket, 4, Rest)
+ end,
+ if
+ Len > ?MAX_PAYLOAD ->
+ Error = io_lib:format(
+ "Payload length ~p longer than max allowed of ~p",
+ [Len, ?MAX_PAYLOAD]),
+ exit({error, Error});
+ true ->
+ <<Payload:Len/binary, Excess/binary>> = buffer(Socket, Len, Rest2),
+ {ws_frame_info_secondary, Len, MaskingKey, Payload, Excess}
+ end.
+
+unframe_active_once(State, FirstPacket) ->
+ Frames = unframe(State, FirstPacket),
+ websocket_setopts(State, [{active, once}]),
+ Frames.
+
+%% Returns all the WebSocket frames fully or partially contained in FirstPacket,
+%% reading exactly as many more bytes from Socket as are needed to finish
+%% unframing the last frame partially included in FirstPacket, if needed.
+%%
+%% The length of this list and depth of this recursion is limited by
+%% the size of your socket receive buffer.
+%%
+%% -> { #ws_state, [#ws_frame_info,...,#ws_frame_info] }
+unframe(_State, <<>>) ->
+ [];
+unframe(State, FirstPacket) ->
+ case unframe_one(State, FirstPacket) of
+ {FrameInfo = #ws_frame_info{ws_state = NewState}, RestBin} ->
+ %% Every new recursion uses the #ws_state from the calling recursion.
+ [FrameInfo | unframe(NewState, RestBin)];
+ Fail ->
+ [Fail]
+ end.
+
+%% -> {#ws_frame_info, RestBin} | {fail_connection, Reason}
+unframe_one(State = #ws_state{vsn=8}, FirstPacket) ->
+ {FrameInfo = #ws_frame_info{}, RestBin} = ws_frame_info(State, FirstPacket),
+ Unmasked = mask(FrameInfo#ws_frame_info.masking_key,
+ FrameInfo#ws_frame_info.payload),
+ NewState = frag_state_machine(State, FrameInfo),
+ Unframed = FrameInfo#ws_frame_info{data = Unmasked,
+ ws_state = NewState},
+
+ case checks(Unframed) of
+ #ws_frame_info{} when is_record(NewState, ws_state) ->
+ {Unframed, RestBin};
+ #ws_frame_info{} when not is_record(NewState, ws_state) ->
+ NewState; %% pass back the error details
+ Fail ->
+ Fail
+ end.
+
+websocket_setopts(#ws_state{sock=Socket={sslsocket,_,_}}, Opts) ->
+ ssl:setopts(Socket, Opts);
+websocket_setopts(#ws_state{sock=Socket}, Opts) ->
+ inet:setopts(Socket, Opts).
+
+is_control_op(Op) ->
+ atom_to_opcode(Op) > 7.
+
+%% Unfragmented message
+frag_state_machine(State = #ws_state{frag_type = none},
+ #ws_frame_info{fin = 1}) ->
+ State;
+
+%% Beginning of fragmented text message
+frag_state_machine(State = #ws_state{frag_type = none},
+ #ws_frame_info{fin = 0,
+ opcode = text}) ->
+ State#ws_state{frag_type = text};
+
+%% Beginning of fragmented binary message
+frag_state_machine(State = #ws_state{frag_type = none},
+ #ws_frame_info{fin = 0,
+ opcode = binary}) ->
+ State#ws_state{frag_type = binary};
+
+%% Expecting text continuation
+frag_state_machine(State = #ws_state{frag_type = text},
+ #ws_frame_info{fin = 0,
+ opcode = continuation}) ->
+ State;
+
+%% Expecting binary continuation
+frag_state_machine(State = #ws_state{frag_type = binary},
+ #ws_frame_info{fin = 0,
+ opcode = continuation}) ->
+ State;
+
+%% End of fragmented text message
+frag_state_machine(State = #ws_state{frag_type = text},
+ #ws_frame_info{fin = 1,
+ opcode = continuation}) ->
+ State#ws_state{frag_type = none};
+
+%% End of fragmented binary message
+frag_state_machine(State = #ws_state{frag_type = binary},
+ #ws_frame_info{fin = 1,
+ opcode = continuation}) ->
+ State#ws_state{frag_type = none};
+
+
+frag_state_machine(State, #ws_frame_info{opcode = Op}) ->
+ IsControl = is_control_op(Op),
+ if
+ IsControl == true ->
+ %% Control message never changes fragmentation state
+ State;
+ true ->
+ %% Everything else is wrong
+ {error, "fragmentation rules violated"}
+ end.
+
+
+opcode_to_atom(16#0) -> continuation;
+opcode_to_atom(16#1) -> text;
+opcode_to_atom(16#2) -> binary;
+opcode_to_atom(16#8) -> close;
+opcode_to_atom(16#9) -> ping;
+opcode_to_atom(16#A) -> pong;
+opcode_to_atom(_) -> undefined.
+
+atom_to_opcode(continuation) -> 16#0;
+atom_to_opcode(text) -> 16#1;
+atom_to_opcode(binary) -> 16#2;
+atom_to_opcode(close) -> 16#8;
+atom_to_opcode(ping) -> 16#9;
+atom_to_opcode(pong) -> 16#A.
+
+
+frame(8, Type, Data) ->
+ %% FIN=true because we're not fragmenting.
+ %% OPCODE=1 for text
+ FirstByte = 128 bor atom_to_opcode(Type),
+ ByteList = binary_to_list(Data),
+ Length = length(ByteList),
+ if
+ Length < 126 ->
+ << FirstByte, 0:1, Length:7, Data:Length/binary >>;
+ Length =< 65535 ->
+ << FirstByte, 0:1, 126:7, Length:16, Data:Length/binary >>;
+ true ->
+ Defined = Length =< math:pow(2,64),
+ %% TODO: Is the correctness of this pow call
+ %% better than the speed and danger of not checking?
+ case Defined of
+ true ->
+ << FirstByte, 0:1, 127:7, Length:64, Data:Length/binary >>;
+ _ ->
+ undefined
+ end
+ end.
+
+
+mask(MaskBin, Data) ->
+ list_to_binary(rmask(MaskBin, Data)).
+
+%% unmask == mask. It's XOR of the four-byte masking key.
+rmask(_,<<>>) ->
+ [<<>>];
+
+rmask(MaskBin = <<Mask:4/integer-unit:8>>,
+ <<Data:4/integer-unit:8, Rest/binary>>) ->
+ Masked = Mask bxor Data,
+ MaskedRest = rmask(MaskBin, Rest),
+ [<<Masked:4/integer-unit:8>> | MaskedRest ];
+
+rmask(<<Mask:3/integer-unit:8, _Rest/binary>>, <<Data:3/integer-unit:8>>) ->
+ Masked = Mask bxor Data,
+ [<<Masked:3/integer-unit:8>>];
+
+rmask(<<Mask:2/integer-unit:8, _Rest/binary>>, <<Data:2/integer-unit:8>>) ->
+ Masked = Mask bxor Data,
+ [<<Masked:2/integer-unit:8>>];
+
+rmask(<<Mask:1/integer-unit:8, _Rest/binary>>, <<Data:1/integer-unit:8>>) ->
+ Masked = Mask bxor Data,
+ [<<Masked:1/integer-unit:8>>].
%% Internal functions
get_origin_header(Headers) ->
- query_header("origin", Headers).
+ case query_header("origin", Headers) of
+ undefined -> query_header("sec-websocket-origin", Headers);
+ Origin -> Origin
+ end.
get_protocol_header(Headers) ->
query_header("sec-websocket-protocol", Headers, "unknown").
+get_nonce_header(Headers) ->
+ query_header("sec-websocket-key", Headers).
+
query_header(HeaderName, Headers) ->
query_header(HeaderName, Headers, undefined).
@@ -148,35 +581,7 @@ query_header(Header, #headers{other=L}, Default) ->
Acc
end, Default, L).
-secret_key(KeyName, Headers) ->
- case query_header(KeyName, Headers) of
- undefined ->
- 0;
- Key ->
- Digits = lists:filter(fun(C) -> C >= 48 andalso C =< 57 end, Key),
- NumberOfSpaces = length(lists:filter(fun(C) -> C == 32 end, Key)),
- list_to_integer(Digits) div NumberOfSpaces
- end.
-
-challenge(Key1, Key2, Challenge) ->
- erlang:md5(digits32(Key1) ++ digits32(Key2) ++ Challenge).
-
-digits32(Num) ->
- Digit4 = Num rem 256,
- Num2 = Num div 256,
- Digit3 = Num2 rem 256,
- Num3 = Num2 div 256,
- Digit2 = Num3 rem 256,
- Digit1 = Num3 div 256,
- [Digit1, Digit2, Digit3, Digit4].
-
-unpack_length(Binary, LenBytes, Length) ->
- <<_, _:LenBytes/bytes, B, _/bitstring>> = Binary,
- B_v = B band 16#7F,
- NewLength = (Length * 128) + B_v,
- case B band 16#80 of
- 16#80 ->
- unpack_length(Binary, LenBytes + 1, NewLength);
- 0 ->
- {NewLength, LenBytes + 1}
- end.
+hash_nonce(Nonce) ->
+ Salted = Nonce ++ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
+ HashBin = crypto:sha(Salted),
+ base64:encode_to_string(HashBin).
View
51 www/websockets.yaws
@@ -28,44 +28,19 @@ out(A) ->
{p, [], "First start by returning:"},
- box(" {websocket, OwnerPid, SocketMode}"),
-
- {p,[], "from the out/1 function."
- " This makes the erlang process within yaws processing that particular page do a protocol upgrade from HTTP to the Web Socket Protocol, after which the OwnerPid can use the socket to interface directly with the Web Sockets client."},
-
- {p,[], "When yaws completes the Web Sockets handshake, it sends one of the following messages to OwnerPid:"},
-
- {ul,[],
- [{li,[]," {ok, WebSocket} indicates to OwnerPid that the handshake completed successfully and that the WebSocket is set up and can be used to communicate with the web sockets client."},
- {li,[]," discard indicates to OwnerPid that the handshake failed and no web sockets connection is available."}]},
-
- {p,[], "SocketMode defines how the messages sent by the client are to be delivered to the OwnerPid."},
-
- {ul, [], [
- {li, [], [
- {p,[], "On passive mode (SocketMode=false) it is up to the receiving process to issue a synchronous request to: "},
- box("yaws_api:websocket_receive(WebSocket)"),
- {p, [], " to grab the incoming messages."}
- ]},
- {li, [], [
- {p, [], "On active mode (SocketMode=true) the incoming Web Socket data frames are delivered to OwnerPid as messages. On this mode the following messages are to be expected:"},
- box("{tcp, WebSocket, DataFrame}
-{tcp_closed, WebSocket}"),
- {p, [], "To extract the data from a Web Socket data frame you can use this function: "},
- box("yaws_api:websocket_unframe_data(DataFrame)")
- ]},
- {li, [], [
- {p, [], "If SocketMode=once then only ONE message will be sent as if in active mode and after that we're back to passive mode."}
- ]}
- ]},
-
- {p,[], "For switching between the various \"receive modes\" you can do this:"},
-
- box("yaws_api:websocket_setopts(WebSocket, [{active, NewSocketMode}])"),
-
- {p,[],
- "This function just wraps (gen_tcp|ssl):setops/2 to absctract away from using a regular/secure http connection."},
-
+ box(" {websocket, CallbackMod, Options}"),
+
+ {p,[], "from the out/1 function, where CallbackMod is an atom identifying your Websocket callback module, and Options can be an empty list."
+ " This makes the erlang process within yaws processing that particular page do a protocol upgrade from HTTP to the Web Socket Protocol, after which messages received by the server call handle_message in the callback module, and messages can be sent on the Websocket using yaws_api:websocket_send/2."},
+ {p, [], "The callback module should implement a function handle_message as follows:"},
+ box("handle_message({Type, Data}) -> HandlerResult
+Type :: text|binary
+Data :: binary()
+HandlerResult :: {reply, {Type, Data}}
+ | {noreply}
+ | {close, Reason}"),
+ {p, [], "When not replying to a message from a client, you can send messages using"},
+ box("yaws_api:websocket_send(Pid, {Type, Data})"),
{p, [], "Enough theory for now. Sample echo server follows!"},
ssi("websockets_example_endpoint.yaws"),
View
7 www/websockets_autobahn_endpoint.yaws
@@ -0,0 +1,7 @@
+<erl>
+out(A) ->
+ CallbackMod = advanced_echo_callback,
+ InitialState = {state, none, <<>>},
+ Opts = [{callback, {advanced, InitialState}}],
+ {websocket, CallbackMod, Opts}.
+</erl>
View
12 www/websockets_example.yaws
@@ -16,7 +16,10 @@ html_body(WebSocketLocation) ->
<head>
<title>Basic WebSocket Example</title>
<script type=\"text/javascript\">
- if (!window.WebSocket)
+ var WS = false;
+ if (window.WebSocket) WS = WebSocket;
+ if (!WS && window.MozWebSocket) WS = MozWebSocket;
+ if (!WS)
alert(\"WebSocket not supported by this browser\");
// Get an Element
@@ -26,7 +29,7 @@ html_body(WebSocketLocation) ->
var client = {
connect: function(){
- this._ws=new WebSocket(\"ws://" ++ WebSocketLocation ++ "\");
+ this._ws=new WS(\"ws://" ++ WebSocketLocation ++ "\");
this._ws.onopen=this._onopen;
this._ws.onmessage=this._onmessage;
this._ws.onclose=this._onclose;
@@ -73,6 +76,9 @@ html_body(WebSocketLocation) ->
</head>
<body>
<h1>Basic Echo Example</h1>
+ <ul><li>Sending \"bye\" closes the connection.</li>
+ <li>Sending \"say hi later\" sends \"hi there!\" asynchronously.</li>
+ <li>Sending \"something\" does nothing.</li></ul>
<div id=\"msgs\"></div>
<div id=\"connect\">
<input id='cA' class='button' type='submit' name='connect' value='Connect'/>
@@ -85,7 +91,7 @@ html_body(WebSocketLocation) ->
<script type='text/javascript'>
$('cA').onclick = function(event) { client.connect(); return false; };
- $('sendB').onclick = function(event) { client.chat($F('phrase')); $('phrase').value=''; return false; };
+ $('sendB').onclick = function(event) { client.chat($F('phrase')); $('phrase').value=''; $('phrase').focus(); return false; };
</script>
</body>
</html>".
View
69 www/websockets_example_endpoint.yaws
@@ -1,63 +1,6 @@
-<erl>
-out(A) ->
- case get_upgrade_header(A#arg.headers) of
- undefined ->
- {content, "text/plain", "You're not a web sockets client! Go away!"};
- "WebSocket" ->
- WebSocketOwner = spawn(fun() -> websocket_owner() end),
- {websocket, WebSocketOwner, passive}
- end.
-
-websocket_owner() ->
- receive
- {ok, WebSocket} ->
- %% This is how we read messages (plural!!) from websockets on passive mode
- case yaws_api:websocket_receive(WebSocket) of
- {error,closed} ->
- io:format("The websocket got disconnected right from the start. "
- "This wasn't supposed to happen!!~n");
- {ok, Messages} ->
- case Messages of
- [<<"client-connected">>] ->
- yaws_api:websocket_setopts(WebSocket, [{active, true}]),
- echo_server(WebSocket);
- Other ->
- io:format("websocket_owner got: ~p. Terminating~n", [Other])
- end
- end;
- _ -> ok
- end.
-
-echo_server(WebSocket) ->
- receive
- {tcp, WebSocket, DataFrame} ->
- Data = yaws_api:websocket_unframe_data(DataFrame),
- io:format("Got data from Websocket: ~p~n", [Data]),
- yaws_api:websocket_send(WebSocket, Data),
- echo_server(WebSocket);
- {tcp_closed, WebSocket} ->
- io:format("Websocket closed. Terminating echo_server...~n");
- Any ->
- io:format("echo_server received msg:~p~n", [Any]),
- echo_server(WebSocket)
- end.
-
-get_upgrade_header(#headers{other=L}) ->
- lists:foldl(fun({http_header,_,K0,_,V}, undefined) ->
- K = case is_atom(K0) of
- true ->
- atom_to_list(K0);
- false ->
- K0
- end,
- case string:to_lower(K) of
- "upgrade" ->
- V;
- _ ->
- undefined
- end;
- (_, Acc) ->
- Acc
- end, undefined, L).
-
-</erl>
+<erl>
+out(A) ->
+ CallbackMod = basic_echo_callback,
+ Opts = [{origin, "http://" ++ (A#arg.headers)#headers.host}],

please add a comment that explains this line because it stops cross-domain communications and must be set to
Opts = [{origin,any}]
to accept requests coming from external sources

@vinoski Collaborator
vinoski added a note

Comment added as requested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ {websocket, CallbackMod, Opts}.
+</erl>
@aladhami5

please add a comment that explains this line because it stops cross-domain communications and must be set to
Opts = [{origin,any}]
to accept requests coming from external sources

@vinoski

Comment added as requested.

Please sign in to comment.
Something went wrong with that request. Please try again.