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>
+     &nbsp; 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.