Skip to content

Commit e4b2477

Browse files
committed
Merge pull request mochi#144 from mochi/websocket-ssl
Fix ssl receive support for websocket
2 parents 8b6fab5 + 324d703 commit e4b2477

File tree

5 files changed

+213
-112
lines changed

5 files changed

+213
-112
lines changed

src/mochiweb_websocket.erl

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
-export([loop/5, upgrade_connection/2, request/5]).
2929
-export([send/3]).
30+
-ifdef(TEST).
31+
-compile(export_all).
32+
-endif.
3033

3134
loop(Socket, Body, State, WsVersion, ReplyChannel) ->
3235
ok = mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}]),
@@ -44,7 +47,7 @@ request(Socket, Body, State, WsVersion, ReplyChannel) ->
4447
{tcp_error, _, _} ->
4548
mochiweb_socket:close(Socket),
4649
exit(normal);
47-
{tcp, _, WsFrames} ->
50+
{Proto, _, WsFrames} when Proto =:= tcp orelse Proto =:= ssl ->
4851
case parse_frames(WsVersion, WsFrames, Socket) of
4952
close ->
5053
mochiweb_socket:close(Socket),
@@ -214,7 +217,7 @@ parse_hybi_frames(Socket, <<_Fin:1,
214217
{tcp_error, _, _} ->
215218
mochiweb_socket:close(Socket),
216219
exit(normal);
217-
{tcp, _, Continuation} ->
220+
{Proto, _, Continuation} when Proto =:= tcp orelse Proto =:= ssl ->
218221
parse_hybi_frames(Socket, <<PartFrame/binary, Continuation/binary>>,
219222
Acc);
220223
_ ->
@@ -276,11 +279,3 @@ parse_hixie(<<255, Rest/binary>>, Buffer) ->
276279
{Buffer, Rest};
277280
parse_hixie(<<H, T/binary>>, Buffer) ->
278281
parse_hixie(T, <<Buffer/binary, H>>).
279-
280-
%%
281-
%% Tests
282-
%%
283-
-ifdef(TEST).
284-
-include_lib("eunit/include/eunit.hrl").
285-
-compile(export_all).
286-
-endif.

test/mochiweb_test_util.erl

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
-module(mochiweb_test_util).
2+
-export([with_server/3, client_request/4, sock_fun/2]).
3+
-include("mochiweb_test_util.hrl").
4+
5+
ssl_cert_opts() ->
6+
EbinDir = filename:dirname(code:which(?MODULE)),
7+
CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
8+
CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
9+
KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
10+
[{certfile, CertFile}, {keyfile, KeyFile}].
11+
12+
with_server(Transport, ServerFun, ClientFun) ->
13+
ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
14+
ServerOpts = case Transport of
15+
plain ->
16+
ServerOpts0;
17+
ssl ->
18+
ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
19+
end,
20+
{ok, Server} = mochiweb_http:start_link(ServerOpts),
21+
Port = mochiweb_socket_server:get(Server, port),
22+
Res = (catch ClientFun(Transport, Port)),
23+
mochiweb_http:stop(Server),
24+
Res.
25+
26+
sock_fun(Transport, Port) ->
27+
Opts = [binary, {active, false}, {packet, http}],
28+
case Transport of
29+
plain ->
30+
{ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
31+
fun (recv) ->
32+
gen_tcp:recv(Socket, 0);
33+
({recv, Length}) ->
34+
gen_tcp:recv(Socket, Length);
35+
({send, Data}) ->
36+
gen_tcp:send(Socket, Data);
37+
({setopts, L}) ->
38+
inet:setopts(Socket, L);
39+
(get) ->
40+
Socket
41+
end;
42+
ssl ->
43+
{ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
44+
fun (recv) ->
45+
ssl:recv(Socket, 0);
46+
({recv, Length}) ->
47+
ssl:recv(Socket, Length);
48+
({send, Data}) ->
49+
ssl:send(Socket, Data);
50+
({setopts, L}) ->
51+
ssl:setopts(Socket, L);
52+
(get) ->
53+
{ssl, Socket}
54+
end
55+
end.
56+
57+
client_request(Transport, Port, Method, TestReqs) ->
58+
client_request(sock_fun(Transport, Port), Method, TestReqs).
59+
60+
client_request(SockFun, _Method, []) ->
61+
{the_end, {error, closed}} = {the_end, SockFun(recv)},
62+
ok;
63+
client_request(SockFun, Method,
64+
[#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
65+
Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
66+
client_headers(Body, Rest =:= []),
67+
"\r\n",
68+
Body],
69+
ok = SockFun({send, Request}),
70+
case Method of
71+
'GET' ->
72+
{ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
73+
'POST' ->
74+
{ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
75+
'CONNECT' ->
76+
{ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
77+
end,
78+
ok = SockFun({setopts, [{packet, httph}]}),
79+
{ok, {http_header, _, 'Server', _, "MochiWeb" ++ _}} = SockFun(recv),
80+
{ok, {http_header, _, 'Date', _, _}} = SockFun(recv),
81+
{ok, {http_header, _, 'Content-Type', _, _}} = SockFun(recv),
82+
{ok, {http_header, _, 'Content-Length', _, ConLenStr}} = SockFun(recv),
83+
ContentLength = list_to_integer(ConLenStr),
84+
{ok, http_eoh} = SockFun(recv),
85+
ok = SockFun({setopts, [{packet, raw}]}),
86+
{payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
87+
ok = SockFun({setopts, [{packet, http}]}),
88+
client_request(SockFun, Method, Rest).
89+
90+
client_headers(Body, IsLastRequest) ->
91+
["Host: localhost\r\n",
92+
case Body of
93+
<<>> ->
94+
"";
95+
_ ->
96+
["Content-Type: application/octet-stream\r\n",
97+
"Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
98+
end,
99+
case IsLastRequest of
100+
true ->
101+
"Connection: close\r\n";
102+
false ->
103+
""
104+
end].
105+
106+
drain_reply(_SockFun, 0, Acc) ->
107+
Acc;
108+
drain_reply(SockFun, Length, Acc) ->
109+
Sz = erlang:min(Length, 1024),
110+
{ok, B} = SockFun({recv, Sz}),
111+
drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).

test/mochiweb_test_util.hrl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-record(treq, {path, body= <<>>, xreply= <<>>}).

test/mochiweb_tests.erl

Lines changed: 4 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,9 @@
11
-module(mochiweb_tests).
22
-include_lib("eunit/include/eunit.hrl").
3-
4-
-record(treq, {path, body= <<>>, xreply= <<>>}).
5-
6-
ssl_cert_opts() ->
7-
EbinDir = filename:dirname(code:which(?MODULE)),
8-
CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
9-
CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
10-
KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
11-
[{certfile, CertFile}, {keyfile, KeyFile}].
3+
-include("mochiweb_test_util.hrl").
124

135
with_server(Transport, ServerFun, ClientFun) ->
14-
ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
15-
ServerOpts = case Transport of
16-
plain ->
17-
ServerOpts0;
18-
ssl ->
19-
ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
20-
end,
21-
{ok, Server} = mochiweb_http:start_link(ServerOpts),
22-
Port = mochiweb_socket_server:get(Server, port),
23-
Res = (catch ClientFun(Transport, Port)),
24-
mochiweb_http:stop(Server),
25-
Res.
6+
mochiweb_test_util:with_server(Transport, ServerFun, ClientFun).
267

278
request_test() ->
289
R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
@@ -148,6 +129,7 @@ do_GET(PathPrefix, Transport, Times) ->
148129
ClientFun = new_client_fun('GET', TestReqs),
149130
ok = with_server(Transport, ServerFun, ClientFun),
150131
ok.
132+
151133
do_POST(Transport, Size, Times) ->
152134
ServerFun = fun (Req) ->
153135
Body = Req:recv_body(),
@@ -165,86 +147,6 @@ do_POST(Transport, Size, Times) ->
165147

166148
new_client_fun(Method, TestReqs) ->
167149
fun (Transport, Port) ->
168-
client_request(Transport, Port, Method, TestReqs)
150+
mochiweb_test_util:client_request(Transport, Port, Method, TestReqs)
169151
end.
170152

171-
client_request(Transport, Port, Method, TestReqs) ->
172-
Opts = [binary, {active, false}, {packet, http}],
173-
SockFun = case Transport of
174-
plain ->
175-
{ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
176-
fun (recv) ->
177-
gen_tcp:recv(Socket, 0);
178-
({recv, Length}) ->
179-
gen_tcp:recv(Socket, Length);
180-
({send, Data}) ->
181-
gen_tcp:send(Socket, Data);
182-
({setopts, L}) ->
183-
inet:setopts(Socket, L)
184-
end;
185-
ssl ->
186-
{ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
187-
fun (recv) ->
188-
ssl:recv(Socket, 0);
189-
({recv, Length}) ->
190-
ssl:recv(Socket, Length);
191-
({send, Data}) ->
192-
ssl:send(Socket, Data);
193-
({setopts, L}) ->
194-
ssl:setopts(Socket, L)
195-
end
196-
end,
197-
client_request(SockFun, Method, TestReqs).
198-
199-
client_request(SockFun, _Method, []) ->
200-
{the_end, {error, closed}} = {the_end, SockFun(recv)},
201-
ok;
202-
client_request(SockFun, Method,
203-
[#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
204-
Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
205-
client_headers(Body, Rest =:= []),
206-
"\r\n",
207-
Body],
208-
ok = SockFun({send, Request}),
209-
case Method of
210-
'GET' ->
211-
{ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
212-
'POST' ->
213-
{ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
214-
'CONNECT' ->
215-
{ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
216-
end,
217-
ok = SockFun({setopts, [{packet, httph}]}),
218-
{ok, {http_header, _, 'Server', _, "MochiWeb" ++ _}} = SockFun(recv),
219-
{ok, {http_header, _, 'Date', _, _}} = SockFun(recv),
220-
{ok, {http_header, _, 'Content-Type', _, _}} = SockFun(recv),
221-
{ok, {http_header, _, 'Content-Length', _, ConLenStr}} = SockFun(recv),
222-
ContentLength = list_to_integer(ConLenStr),
223-
{ok, http_eoh} = SockFun(recv),
224-
ok = SockFun({setopts, [{packet, raw}]}),
225-
{payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
226-
ok = SockFun({setopts, [{packet, http}]}),
227-
client_request(SockFun, Method, Rest).
228-
229-
client_headers(Body, IsLastRequest) ->
230-
["Host: localhost\r\n",
231-
case Body of
232-
<<>> ->
233-
"";
234-
_ ->
235-
["Content-Type: application/octet-stream\r\n",
236-
"Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
237-
end,
238-
case IsLastRequest of
239-
true ->
240-
"Connection: close\r\n";
241-
false ->
242-
""
243-
end].
244-
245-
drain_reply(_SockFun, 0, Acc) ->
246-
Acc;
247-
drain_reply(SockFun, Length, Acc) ->
248-
Sz = erlang:min(Length, 1024),
249-
{ok, B} = SockFun({recv, Sz}),
250-
drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).

test/mochiweb_websocket_tests.erl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,95 @@ hixie_frames_decode_test() ->
8282
mochiweb_websocket:parse_hixie_frames(
8383
<<0,102,111,111,255,0,98,97,114,255>>,
8484
[])).
85+
86+
end_to_end_test_factory(ServerTransport) ->
87+
mochiweb_test_util:with_server(
88+
ServerTransport,
89+
fun end_to_end_server/1,
90+
fun (Transport, Port) ->
91+
end_to_end_client(mochiweb_test_util:sock_fun(Transport, Port))
92+
end).
93+
94+
end_to_end_server(Req) ->
95+
?assertEqual("Upgrade", Req:get_header_value("connection")),
96+
?assertEqual("websocket", Req:get_header_value("upgrade")),
97+
{ReentryWs, _ReplyChannel} = mochiweb_websocket:upgrade_connection(
98+
Req,
99+
fun end_to_end_ws_loop/3),
100+
ReentryWs(ok).
101+
102+
end_to_end_ws_loop(Payload, State, ReplyChannel) ->
103+
%% Echo server
104+
lists:foreach(ReplyChannel, Payload),
105+
State.
106+
107+
end_to_end_client(S) ->
108+
%% Key and Accept per https://tools.ietf.org/html/rfc6455
109+
UpgradeReq = string:join(
110+
["GET / HTTP/1.1",
111+
"Host: localhost",
112+
"Upgrade: websocket",
113+
"Connection: Upgrade",
114+
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
115+
"",
116+
""], "\r\n"),
117+
ok = S({send, UpgradeReq}),
118+
{ok, {http_response, {1,1}, 101, _}} = S(recv),
119+
ok = S({setopts, [{packet, httph}]}),
120+
D = read_expected_headers(
121+
S,
122+
gb_from_list(
123+
[{'Upgrade', "websocket"},
124+
{'Connection', "Upgrade"},
125+
{'Content-Length', "0"},
126+
{"Sec-Websocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}])),
127+
?assertEqual([], gb_trees:to_list(D)),
128+
ok = S({setopts, [{packet, raw}]}),
129+
%% The first message sent over telegraph :)
130+
SmallMessage = <<"What hath God wrought?">>,
131+
ok = S({send,
132+
<< 1:1, %% Fin
133+
0:1, %% Rsv1
134+
0:1, %% Rsv2
135+
0:1, %% Rsv3
136+
2:4, %% Opcode, 1 = text frame
137+
1:1, %% Mask on
138+
(byte_size(SmallMessage)):7, %% Length, <125 case
139+
0:32, %% Mask (trivial)
140+
SmallMessage/binary >>}),
141+
{ok, WsFrames} = S(recv),
142+
<< 1:1, %% Fin
143+
0:1, %% Rsv1
144+
0:1, %% Rsv2
145+
0:1, %% Rsv3
146+
1:4, %% Opcode, text frame (all mochiweb suports for now)
147+
MsgSize:8, %% Expecting small size
148+
SmallMessage/binary >> = WsFrames,
149+
?assertEqual(MsgSize, byte_size(SmallMessage)),
150+
ok.
151+
152+
gb_from_list(L) ->
153+
lists:foldl(
154+
fun ({K, V}, D) -> gb_trees:insert(K, V, D) end,
155+
gb_trees:empty(),
156+
L).
157+
158+
read_expected_headers(S, D) ->
159+
case S(recv) of
160+
{ok, http_eoh} ->
161+
D;
162+
{ok, {http_header, _, K, _, V}} ->
163+
case gb_trees:lookup(K, D) of
164+
{value, V1} ->
165+
?assertEqual({K, V}, {K, V1}),
166+
read_expected_headers(S, gb_trees:delete(K, D));
167+
none ->
168+
read_expected_headers(S, D)
169+
end
170+
end.
171+
172+
end_to_end_http_test() ->
173+
end_to_end_test_factory(plain).
174+
175+
end_to_end_https_test() ->
176+
end_to_end_test_factory(ssl).

0 commit comments

Comments
 (0)