You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by wi...@apache.org on 2020/01/13 19:31:30 UTC
[couchdb-mochiweb] 01/08: * Support modern and legacy websockets
spec * Create websockets example * Currently websocket is 'passive',
call get_data() for each frame
This is an automated email from the ASF dual-hosted git repository.
willholley pushed a commit to tag active-passive
in repository https://gitbox.apache.org/repos/asf/couchdb-mochiweb.git
commit e6386a6651bd7ccf1754c0cbf826618f15f38bbc
Author: Richard Jones <rj...@metabrew.com>
AuthorDate: Fri Oct 8 20:14:56 2010 +0100
* Support modern and legacy websockets spec
* Create websockets example
* Currently websocket is 'passive', call get_data() for each frame
---
examples/websockets/index.html | 39 ++++++++++++++++
examples/websockets/websockets.erl | 58 +++++++++++++++++++++++
src/mochiweb_http.erl | 96 ++++++++++++++++++++++++++++++++++----
src/mochiweb_socket_server.erl | 3 ++
src/mochiweb_wsrequest.erl | 68 +++++++++++++++++++++++++++
5 files changed, 256 insertions(+), 8 deletions(-)
diff --git a/examples/websockets/index.html b/examples/websockets/index.html
new file mode 100644
index 0000000..208a602
--- /dev/null
+++ b/examples/websockets/index.html
@@ -0,0 +1,39 @@
+<html>
+<head>
+ <title>Websockets With Mochiweb Demo</title>
+</head>
+<body>
+<h1>Mochiweb websocket demo</h1>
+
+ <div id="connect">
+ <button id="btnConn">Connect</button>
+ State: <span id="connstate" style="font-weight:bold;"></span>
+ </div>
+ <br/><i>Protip: open your javascript error console, just in case..</i><br/>
+ <hr/>
+ <div id="connected">
+ <input id='phrase' type='text'/>
+ <input id='btnSend' class='button' type='submit' name='connect' value='Send'/>
+ </div>
+ <hr/>
+ <div id="msgs"></div>
+
+ <script type="text/javascript">
+ var ws;
+ if (!window.WebSocket) alert("WebSocket not supported by this browser");
+ function $() { return document.getElementById(arguments[0]); }
+ function go()
+ {
+ ws = new WebSocket("ws://localhost:8080");
+ ws.onopen = function(){ $('connstate').innerHTML='CONNECTED'; }
+ ws.onclose = function(){ $('connstate').innerHTML='CLOSED'; }
+ ws.onmessage = function(e){ $('msgs').innerHTML = $('msgs').innerHTML + "<pre>"+e.data+"</pre>"; }
+ }
+
+ $('btnConn').onclick = function(event) { go(); return false; };
+ $('btnSend').onclick = function(event) { ws.send($('phrase').value); $('phrase').value=''; return false; };
+
+ </script>
+ </body>
+</html>
+
diff --git a/examples/websockets/websockets.erl b/examples/websockets/websockets.erl
new file mode 100644
index 0000000..f50c5b4
--- /dev/null
+++ b/examples/websockets/websockets.erl
@@ -0,0 +1,58 @@
+-module(websockets).
+-author('author <rj...@metabrew.com>').
+
+-export([start/0, start/1, stop/0, loop/2, wsloop/1]).
+
+start() -> start([{port, 8080}, {docroot, "."}]).
+
+start(Options) ->
+ {DocRoot, Options1} = get_option(docroot, Options),
+ Loop = fun (Req) -> ?MODULE:loop(Req, DocRoot) end,
+ mochiweb_http:start([{name, ?MODULE},
+ {loop, Loop},
+ {wsloop, {?MODULE, wsloop}} | Options1]).
+
+stop() ->
+ mochiweb_http:stop(?MODULE).
+
+wsloop(Ws) ->
+ io:format("Websocket request, path: ~p~n", [Ws:get(path)]),
+ case Ws:get_data() of
+ closed -> ok;
+ closing -> ok;
+ timeout -> timeout;
+
+ % older websockets spec which is in the wild, messages are framed with
+ % 0x00...0xFF
+ {legacy_frame, Body} ->
+ Ws:send(["YOU SENT US LEGACY FRAME: ", Body]),
+ wsloop(Ws);
+
+ % current spec, each message has a 0xFF/<64bit length> header
+ % and must contain utf8 bytestream
+ {utf8_frame, Body} ->
+ Ws:send(["YOU SENT US MODERN FRAME: ", Body]),
+ wsloop(Ws)
+ end.
+
+loop(Req, DocRoot) ->
+ "/" ++ Path = Req:get(path),
+ case Req:get(method) of
+ Method when Method =:= 'GET'; Method =:= 'HEAD' ->
+ case Path of
+ _ ->
+ Req:serve_file(Path, DocRoot)
+ end;
+ 'POST' ->
+ case Path of
+ _ ->
+ Req:not_found()
+ end;
+ _ ->
+ Req:respond({501, [], []})
+ end.
+
+%% Internal API
+
+get_option(Option, Options) ->
+ {proplists:get_value(Option, Options), proplists:delete(Option, Options)}.
diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl
index 2414099..e51c33e 100644
--- a/src/mochiweb_http.erl
+++ b/src/mochiweb_http.erl
@@ -18,11 +18,13 @@
{port, 8888}]).
parse_options(Options) ->
- {loop, HttpLoop} = proplists:lookup(loop, Options),
+ HttpLoop = proplists:get_value(loop, Options),
+ WsLoop = proplists:get_value(wsloop, Options),
Loop = fun (S) ->
- ?MODULE:loop(S, HttpLoop)
+ ?MODULE:loop(S, {HttpLoop, WsLoop})
end,
- Options1 = [{loop, Loop} | proplists:delete(loop, Options)],
+ Options1 = [{loop, Loop}, {wsloop, WsLoop} |
+ proplists:delete(loop, proplists:delete(wsloop, Options))],
mochilists:set_defaults(?DEFAULTS, Options1).
stop() ->
@@ -117,14 +119,24 @@ headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) ->
%% Too many headers sent, bad request.
mochiweb_socket:setopts(Socket, [{packet, raw}]),
handle_invalid_request(Socket, Request, Headers);
-headers(Socket, Request, Headers, Body, HeaderCount) ->
+headers(Socket, Request, Headers, {WwwLoop, WsLoop} = Body, HeaderCount) ->
case mochiweb_socket:recv(Socket, 0, ?HEADERS_RECV_TIMEOUT) of
{ok, http_eoh} ->
mochiweb_socket:setopts(Socket, [{packet, raw}]),
- Req = mochiweb:new_request({Socket, Request,
- lists:reverse(Headers)}),
- call_body(Body, Req),
- ?MODULE:after_response(Body, Req);
+ % is this a websocket upgrade request:
+ case {string:to_lower(proplists:get_value('Upgrade', Headers, "")),
+ string:to_lower(proplists:get_value('Connection', Headers, ""))} of
+ {"websocket", "upgrade"} ->
+ {_, {abs_path,Path}, _} = Request,
+ ok = websocket_init(Socket, Path, Headers),
+ WsReq = mochiweb_wsrequest:new(Socket, Path),
+ call_body(WsLoop, WsReq);
+ _ -> % not websocket:
+ Req = mochiweb:new_request({Socket, Request,
+ lists:reverse(Headers)}),
+ call_body(WwwLoop, Req),
+ ?MODULE:after_response(Body, Req)
+ end;
{ok, {http_header, _, Name, _, Value}} ->
headers(Socket, Request, [{Name, Value} | Headers], Body,
1 + HeaderCount);
@@ -200,6 +212,74 @@ range_skip_length(Spec, Size) ->
invalid_range
end.
+websocket_init(Socket, Path, Headers) ->
+ Host = proplists:get_value('Host', Headers, ""),
+ %Origin = proplists:get_value("origin", Headers, ""), % TODO
+ SubProto= proplists:get_value("Sec-Websocket-Protocol", Headers, ""),
+ Key1 = proplists:get_value("Sec-Websocket-Key1", Headers, ""),
+ Key2 = proplists:get_value("Sec-Websocket-Key2", Headers, ""),
+ % read the 8 random bytes sent after the client headers for websockets:
+ {ok, Key3} = mochiweb_socket:recv(Socket, 8, ?HEADERS_RECV_TIMEOUT),
+ %io:format("Key1,2,3: ~p ~p ~p~n", [Key1, Key2, Key3]),
+ {N1,S1} = parse_seckey(Key1),
+ {N2,S2} = parse_seckey(Key2),
+ %io:format("{N1,S1} {N2,S2}: ~p ~p~n",[ {N1,S1}, {N2,S2} ] ),
+ case N1 > 4294967295 orelse
+ N2 > 4294967295 orelse
+ S1 == 0 orelse
+ S2 == 0 of
+ true ->
+ % This is a symptom of an attack.
+ exit(websocket_attack);
+ false ->
+ case N1 rem S1 /= 0 orelse
+ N2 rem S2 /= 0 of
+ true ->
+ % This can only happen if the client is not a conforming
+ % WebSocket client.
+ exit(dodgy_client);
+ false ->
+ Part1 = erlang:round(N1/S1),
+ Part2 = erlang:round(N2/S2),
+ %io:format("Part1 : ~p Part2: ~p~n", [Part1, Part2]),
+ Sig = crypto:md5( <<Part1:32/unsigned-integer,
+ Part2:32/unsigned-integer,
+ Key3/binary>> ),
+ Proto = case Socket of {ssl, _} -> "wss://"; _ -> "ws://" end,
+ SubProtoHeader = case SubProto of
+ "" -> "";
+ P -> ["Sec-WebSocket-Protocol: ", P, "\r\n"]
+ end,
+ Data = ["HTTP/1.1 101 Web Socket Protocol Handshake\r\n",
+ "Upgrade: WebSocket\r\n",
+ "Connection: Upgrade\r\n",
+ "Sec-WebSocket-Location: ", Proto,Host,Path, "\r\n",
+ "Sec-WebSocket-Origin: http://", Host, "\r\n",
+ SubProtoHeader,
+ "\r\n",
+ <<Sig/binary>>
+ ],
+ mochiweb_socket:send(Socket, Data),
+ ok
+ end
+ end.
+
+% websocket seckey parser:
+% extract integer by only looking at [0-9]+ in the string
+% count spaces in the string
+% returns: {int, numspaces}
+parse_seckey(Str) ->
+ parse_seckey1(Str, {"",0}).
+
+parse_seckey1("", {NumStr,NumSpaces}) ->
+ {list_to_integer(lists:reverse(NumStr)), NumSpaces};
+parse_seckey1([32|T], {Ret,NumSpaces}) -> % ASCII/dec space
+ parse_seckey1(T, {Ret, 1+NumSpaces});
+parse_seckey1([N|T], {Ret,NumSpaces}) when N >= 48, N =< 57 -> % ASCII/dec 0-9
+ parse_seckey1(T, {[N|Ret], NumSpaces});
+parse_seckey1([_|T], Acc) ->
+ parse_seckey1(T, Acc).
+
%%
%% Tests
%%
diff --git a/src/mochiweb_socket_server.erl b/src/mochiweb_socket_server.erl
index 3218195..e229402 100644
--- a/src/mochiweb_socket_server.erl
+++ b/src/mochiweb_socket_server.erl
@@ -17,6 +17,7 @@
-record(mochiweb_socket_server,
{port,
loop,
+ wsloop,
name=undefined,
%% NOTE: This is currently ignored.
max=2048,
@@ -85,6 +86,8 @@ parse_options([{ip, Ip} | Rest], State) ->
parse_options(Rest, State#mochiweb_socket_server{ip=ParsedIp});
parse_options([{loop, Loop} | Rest], State) ->
parse_options(Rest, State#mochiweb_socket_server{loop=Loop});
+parse_options([{wsloop, Loop} | Rest], State) ->
+ parse_options(Rest, State#mochiweb_socket_server{wsloop=Loop});
parse_options([{backlog, Backlog} | Rest], State) ->
parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog});
parse_options([{nodelay, NoDelay} | Rest], State) ->
diff --git a/src/mochiweb_wsrequest.erl b/src/mochiweb_wsrequest.erl
new file mode 100644
index 0000000..aeca063
--- /dev/null
+++ b/src/mochiweb_wsrequest.erl
@@ -0,0 +1,68 @@
+%% @author Richard Jones <rj...@metabrew.com>
+%% @see http://www.whatwg.org/specs/web-socket-protocol/
+%% As of August 2010
+%%
+%% However, at time of writing (Oct 8, 2010) Chrome 6 and Firefox 4 implement
+%% an older version of the websocket spec, where messages are framed 0x00...0xFF
+%% so the newer protocol with length headers has not been tested with a browser.
+
+-module(mochiweb_wsrequest, [Socket, Path]).
+-define(TIMEOUT, 999999).
+-export([get/1, get_data/0, send/1]).
+
+get(path) -> Path;
+get(socket) -> Socket.
+
+get_data() ->
+ io:format("get_data...~n",[]),
+ % read FrameType byte
+ case mochiweb_socket:recv(Socket, 1, ?TIMEOUT) of
+ {error, closed} ->
+ closed;
+ {error, timeout} ->
+ timeout;
+ {ok, FrameType} ->
+ case FrameType of
+ <<255>> -> % Modern UTF-8 bytestream message with 64bit length
+ erlang:put(legacy, false),
+ {ok, <<Len:64/unsigned-integer>>} =
+ mochiweb_socket:recv(Socket, 8, ?TIMEOUT),
+ {ok, Frame} = mochiweb_socket:recv(Socket, Len, ?TIMEOUT),
+ {utf8_frame, Frame};
+ <<0>> -> % modern close request, or older no-length-frame msg
+ case mochiweb_socket:recv(Socket, 1, ?TIMEOUT) of
+ {ok, <<0>>} ->
+ % invalid for legacy protocol
+ % probably followed by 7 more 0 in modern
+ closing;
+ {ok, <<255>>} ->
+ % empty legacy frame.
+ erlang:put(legacy, true),
+ {legacy_frame, <<>>};
+ {ok, Byte2} ->
+ % Read up to the first 0xFF for the body
+ erlang:put(legacy, true),
+ Body = read_until_FF(Socket, Byte2),
+ {legacy_frame, Body}
+ end
+ end
+ end.
+
+send(Body) ->
+ case erlang:get(legacy) of
+ true ->
+ % legacy spec, msgs are framed with 0x00..0xFF
+ mochiweb_socket:send(Socket, [0, Body, 255]);
+ _ ->
+ % header is 0xFF then 64bit big-endian int of the msg length
+ Len = iolist_size(Body),
+ mochiweb_socket:send(Socket, [255,
+ <<Len:64/unsigned-integer>>,
+ Body])
+ end.
+
+read_until_FF(Socket, Acc) when is_binary(Acc) ->
+ case mochiweb_socket:recv(Socket, 1, ?TIMEOUT) of
+ {ok, <<255>>} -> Acc;
+ {ok, B} -> read_until_FF(Socket, <<Acc/binary, B/binary>>)
+ end.