Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

SSL implementation, based on mochiweb and #57.

  • Loading branch information...
commit 7534e4bb46c6f8237821ba7a1ec6dce4a1b7c144 1 parent 2d7f253
Knut Nesheim authored
44 src/elli.erl
View
@@ -10,13 +10,13 @@
-include("../include/elli.hrl").
%% API
--export([start_link/0
- , start_link/1
- , stop/1
- , get_acceptors/1
- , get_open_reqs/1
- , get_open_reqs/2
- , set_callback/3
+-export([start_link/0,
+ start_link/1,
+ stop/1,
+ get_acceptors/1,
+ get_open_reqs/1,
+ get_open_reqs/2,
+ set_callback/3
]).
%% gen_server callbacks
@@ -40,9 +40,8 @@
start_link() -> start_link(?EXAMPLE_CONF).
start_link(Opts) ->
- %% Validate options
- Callback = required_opt(callback, Opts),
- valid_callback(Callback) orelse throw(invalid_callback),
+ valid_callback(required_opt(callback, Opts))
+ orelse throw(invalid_callback),
case proplists:get_value(name, Opts) of
undefined ->
@@ -83,6 +82,15 @@ init([Opts]) ->
Port = proplists:get_value(port, Opts, 8080),
MinAcceptors = proplists:get_value(min_acceptors, Opts, 20),
+ UseSSL = proplists:get_value(ssl, Opts, false),
+ KeyFile = proplists:get_value(keyfile, Opts),
+ CertFile = proplists:get_value(certfile, Opts),
+ SockType = case UseSSL of true -> ssl; false -> plain end,
+ SSLSockOpts = case UseSSL of
+ true -> [{keyfile, KeyFile},
+ {certfile, CertFile}];
+ false -> [] end,
+
AcceptTimeout = proplists:get_value(accept_timeout, Opts, 10000),
RequestTimeout = proplists:get_value(request_timeout, Opts, 60000),
HeaderTimeout = proplists:get_value(header_timeout, Opts, 10000),
@@ -100,13 +108,14 @@ init([Opts]) ->
%% tables, etc.
ok = Callback:handle_event(elli_startup, [], CallbackArgs),
- {ok, Socket} = gen_tcp:listen(Port, [binary,
- {ip, IPAddress},
- {reuseaddr, true},
- {backlog, 32768},
- {packet, raw},
- {active, false}
- ]),
+ {ok, Socket} = elli_tcp:listen(SockType, Port, [binary,
+ {ip, IPAddress},
+ {reuseaddr, true},
+ {backlog, 32768},
+ {packet, raw},
+ {active, false}
+ | SSLSockOpts
+ ]),
Acceptors = [elli_http:start_link(self(), Socket, Options,
{Callback, CallbackArgs})
|| _ <- lists:seq(1, MinAcceptors)],
@@ -180,7 +189,6 @@ required_opt(Name, Opts) ->
Value
end.
-
valid_callback(Mod) ->
lists:member({handle, 2}, Mod:module_info(exports)) andalso
lists:member({handle_event, 3}, Mod:module_info(exports)).
60 src/elli_http.erl
View
@@ -1,6 +1,6 @@
%% @doc: Elli HTTP request implementation
%%
-%% An elli_http process blocks in gen_tcp:accept/2 until a client
+%% An elli_http process blocks in elli_tcp:accept/2 until a client
%% connects. It then handles requests on that connection until it's
%% closed either by the client timing out or explicitly by the user.
-module(elli_http).
@@ -28,7 +28,7 @@ start_link(Server, ListenSocket, Options, Callback) ->
%% transfer. If accept doesn't give us a socket within 10 seconds, we
%% loop to allow code upgrades.
accept(Server, ListenSocket, Options, Callback) ->
- case catch gen_tcp:accept(ListenSocket, accept_timeout(Options)) of
+ case catch elli_tcp:accept(ListenSocket, accept_timeout(Options)) of
{ok, Socket} ->
t(accepted),
gen_server:cast(Server, accepted),
@@ -54,7 +54,7 @@ keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) ->
{keep_alive, NewBuffer} ->
?MODULE:keepalive_loop(Socket, NumRequests, NewBuffer, Options, Callback);
{close, _} ->
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
ok
end.
@@ -137,7 +137,7 @@ mk_req(Method, RawPath, RequestHeaders, RequestBody, V, Socket, Callback) ->
handle_event(Mod, request_parse_error,
[{Reason, {Method, RawPath}}], Args),
send_bad_request(Socket),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal)
end.
@@ -155,7 +155,7 @@ send_response(Socket, Method, Code, Headers, UserBody, {Mod, Args}) ->
encode_headers(Headers), <<"\r\n">>,
Body],
- case gen_tcp:send(Socket, Response) of
+ case elli_tcp:send(Socket, Response) of
ok -> ok;
{error, closed} ->
handle_event(Mod, client_closed, [before_response], Args),
@@ -175,9 +175,9 @@ send_file(Socket, Code, Headers, Filename, {Offset, Length}, {Mod, Args}) ->
case file:open(Filename, [read, raw, binary]) of
{ok, Fd} ->
- try gen_tcp:send(Socket, ResponseHeaders) of
+ try elli_tcp:send(Socket, ResponseHeaders) of
ok ->
- case file:sendfile(Fd, Socket, Offset, Length, []) of
+ case elli_tcp:sendfile(Fd, Socket, Offset, Length, []) of
{ok, _BytesSent} ->
ok;
{error, closed} ->
@@ -201,7 +201,7 @@ send_bad_request(Socket) ->
Response = [<<"HTTP/1.1 ">>, status(400), <<"\r\n">>,
<<"Content-Length: ">>, integer_to_list(size(Body)), <<"\r\n">>,
<<"\r\n">>],
- gen_tcp:send(Socket, Response).
+ elli_tcp:send(Socket, Response).
%% @doc: Executes the user callback, translating failure into a proper
%% response.
@@ -255,7 +255,7 @@ handle_event(Mod, Name, EventArgs, ElliArgs) ->
start_chunk_loop(Socket) ->
%% Set the socket to active so we receive the tcp_closed message
%% if the client closes the connection
- inet:setopts(Socket, [{active, once}]),
+ elli_tcp:setopts(Socket, [{active, once}]),
?MODULE:chunk_loop(Socket).
chunk_loop(Socket) ->
@@ -264,17 +264,17 @@ chunk_loop(Socket) ->
{error, client_closed};
{chunk, <<>>} ->
- case gen_tcp:send(Socket, <<"0\r\n\r\n">>) of
+ case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of
ok ->
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
ok;
{error, closed} ->
{error, client_closed}
end;
{chunk, <<>>, From} ->
- case gen_tcp:send(Socket, <<"0\r\n\r\n">>) of
+ case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of
ok ->
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
From ! {self(), ok},
ok;
{error, closed} ->
@@ -301,7 +301,7 @@ chunk_loop(Socket) ->
send_chunk(Socket, Data) ->
Size = integer_to_list(iolist_size(Data), 16),
Response = [Size, <<"\r\n">>, Data, <<"\r\n">>],
- gen_tcp:send(Socket, Response).
+ elli_tcp:send(Socket, Response).
%%
@@ -312,17 +312,17 @@ send_chunk(Socket, Data) ->
get_request(Socket, Buffer, Options, {Mod, Args} = Callback) ->
case erlang:decode_packet(http_bin, Buffer, []) of
{more, _} ->
- case gen_tcp:recv(Socket, 0, request_timeout(Options)) of
+ case elli_tcp:recv(Socket, 0, request_timeout(Options)) of
{ok, Data} ->
NewBuffer = <<Buffer/binary, Data/binary>>,
get_request(Socket, NewBuffer, Options, Callback);
{error, timeout} ->
handle_event(Mod, request_timeout, [], Args),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal);
{error, closed} ->
handle_event(Mod, request_closed, [], Args),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal)
end;
{ok, {http_request, Method, RawPath, Version}, Rest} ->
@@ -330,10 +330,10 @@ get_request(Socket, Buffer, Options, {Mod, Args} = Callback) ->
{ok, {http_error, _}, _} ->
handle_event(Mod, request_parse_error, [Buffer], Args),
send_bad_request(Socket),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal);
{ok, {http_response, _, _, _}, _} ->
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal)
end.
@@ -348,7 +348,7 @@ get_headers(Socket, _, Headers, HeadersCount, _Opts, {Mod, Args})
when HeadersCount >= 100 ->
handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args),
send_bad_request(Socket),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal);
get_headers(Socket, Buffer, Headers, HeadersCount, Opts, {Mod, Args} = Callback) ->
case erlang:decode_packet(httph_bin, Buffer, []) of
@@ -360,17 +360,17 @@ get_headers(Socket, Buffer, Headers, HeadersCount, Opts, {Mod, Args} = Callback)
{ok, {http_error, _}, Rest} ->
get_headers(Socket, Rest, Headers, HeadersCount, Opts, Callback);
{more, _} ->
- case gen_tcp:recv(Socket, 0, header_timeout(Opts)) of
+ case elli_tcp:recv(Socket, 0, header_timeout(Opts)) of
{ok, Data} ->
get_headers(Socket, <<Buffer/binary, Data/binary>>,
Headers, HeadersCount, Opts, Callback);
{error, closed} ->
handle_event(Mod, client_closed, [receiving_headers], Args),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal);
{error, timeout} ->
handle_event(Mod, client_timeout, [receiving_headers], Args),
- gen_tcp:close(Socket),
+ elli_tcp:close(Socket),
exit(normal)
end
end.
@@ -401,16 +401,16 @@ get_body(Socket, Headers, Buffer, Opts, {Mod, Args} = Callback) ->
0 ->
{Buffer, <<>>};
N when N > 0 ->
- case gen_tcp:recv(Socket, N, body_timeout(Opts)) of
+ case elli_tcp:recv(Socket, N, body_timeout(Opts)) of
{ok, Data} ->
{<<Buffer/binary, Data/binary>>, <<>>};
{error, closed} ->
handle_event(Mod, client_closed, [receiving_body], Args),
- ok = gen_tcp:close(Socket),
+ ok = elli_tcp:close(Socket),
exit(normal);
{error, timeout} ->
handle_event(Mod, client_timeout, [receiving_body], Args),
- ok = gen_tcp:close(Socket),
+ ok = elli_tcp:close(Socket),
exit(normal)
end;
_ ->
@@ -437,13 +437,13 @@ check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) ->
case ContentLength < max_body_size(Opts) * 2 of
true ->
OnSocket = ContentLength - size(Buffer),
- gen_tcp:recv(Socket, OnSocket, 60000),
+ elli_tcp:recv(Socket, OnSocket, 60000),
Response = [<<"HTTP/1.1 ">>, status(413), <<"\r\n">>,
<<"Content-Length: 0">>, <<"\r\n\r\n">>],
- gen_tcp:send(Socket, Response),
- gen_tcp:close(Socket);
+ elli_tcp:send(Socket, Response),
+ elli_tcp:close(Socket);
false ->
- gen_tcp:close(Socket)
+ elli_tcp:close(Socket)
end,
exit(normal);
74 src/elli_tcp.erl
View
@@ -0,0 +1,74 @@
+%% @doc: Wrapper for plain and SSL sockets. Based on
+%% mochiweb_socket.erl
+
+-module(elli_tcp).
+-export([listen/3, accept/2, recv/3, send/2, close/1, setopts/2, sendfile/5]).
+
+listen(plain, Port, Opts) ->
+ case gen_tcp:listen(Port, Opts) of
+ {ok, Socket} ->
+ {ok, {plain, Socket}};
+ {error, Reason} ->
+ {error, Reason}
+ end;
+
+listen(ssl, Port, Opts) ->
+ case ssl:listen(Port, Opts) of
+ {ok, Socket} ->
+ {ok, {ssl, Socket}};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+
+
+accept({plain, Socket}, Timeout) ->
+ case gen_tcp:accept(Socket, Timeout) of
+ {ok, S} ->
+ {ok, {plain, S}};
+ {error, Reason} ->
+ {error, Reason}
+ end;
+accept({ssl, Socket}, Timeout) ->
+ case ssl:transport_accept(Socket, Timeout) of
+ {ok, S} ->
+ case ssl:ssl_accept(S, Timeout) of
+ ok ->
+ {ok, {ssl, S}};
+ {error, Reason} ->
+ {error, Reason}
+ end;
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+
+recv({plain, Socket}, Size, Timeout) ->
+ gen_tcp:recv(Socket, Size, Timeout);
+recv({ssl, Socket}, Size, Timeout) ->
+ ssl:recv(Socket, Size, Timeout).
+
+send({plain, Socket}, Data) ->
+ gen_tcp:send(Socket, Data);
+send({ssl, Socket}, Data) ->
+ ssl:send(Socket, Data).
+
+close({plain, Socket}) ->
+ gen_tcp:close(Socket);
+close({ssl, Socket}) ->
+ ssl:close(Socket).
+
+setopts({plain, Socket}, Opts) ->
+ inet:setopts(Socket, Opts);
+setopts({ssl, Socket}, Opts) ->
+ ssl:setopts(Socket, Opts).
+
+
+
+sendfile(Fd, {plain, Socket}, Offset, Length, Opts) ->
+ file:sendfile(Fd, Socket, Offset, Length, []);
+sendfile(_Fd, {ssl, Socket}, _Offset, _Length, _Opts) ->
+ throw(ssl_sendfile_not_supported).
+
+
+
61 test/elli_ssl_tests.erl
View
@@ -0,0 +1,61 @@
+-module(elli_ssl_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+
+elli_ssl_test_() ->
+ {setup,
+ fun setup/0, fun teardown/1,
+ [
+ ?_test(hello_world())
+ ]}.
+
+%%
+%% TESTS
+%%
+
+
+hello_world() ->
+ {ok, Response} = httpc:request("https://localhost:3443/hello/world"),
+ ?assertEqual(200, status(Response)).
+
+%%
+%% INTERNAL HELPERS
+%%
+
+
+setup() ->
+ application:start(crypto),
+ application:start(public_key),
+ application:start(ssl),
+ inets:start(),
+
+ EbinDir = filename:dirname(code:which(?MODULE)),
+ CertDir = filename:join([EbinDir, "..", "test"]),
+ CertFile = filename:join(CertDir, "server_cert.pem"),
+ KeyFile = filename:join(CertDir, "server_key.pem"),
+
+ {ok, P} = elli:start_link([
+ {port, 3443},
+ ssl,
+ {keyfile, KeyFile},
+ {certfile, CertFile},
+ {callback, elli_example_callback}
+ ]),
+ unlink(P),
+ [P].
+
+teardown(Pids) ->
+ inets:stop(),
+ application:stop(ssl),
+ application:stop(public_key),
+ application:stop(crypto),
+ [elli:stop(P) || P <- Pids].
+
+status({{_, Status, _}, _, _}) ->
+ Status.
+
+body({_, _, Body}) ->
+ Body.
+
+headers({_, Headers, _}) ->
+ lists:sort(Headers).
3  test/elli_tests.erl
View
@@ -57,8 +57,7 @@ teardown(Pids) ->
%%
hello_world() ->
- URL = "http://localhost:3001/hello/world",
- {ok, Response} = httpc:request(URL),
+ {ok, Response} = httpc:request("http://localhost:3001/hello/world"),
?assertEqual(200, status(Response)),
?assertEqual([{"connection", "Keep-Alive"},
{"content-length", "12"}], headers(Response)),
19 test/server_cert.pem
View
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIJAJLkNZzERPIUMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
+BAMTCWxvY2FsaG9zdDAeFw0xMDAzMTgxOTM5MThaFw0yMDAzMTUxOTM5MThaMBQx
+EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAJeUCOZxbmtngF4S5lXckjSDLc+8C+XjMBYBPyy5eKdJY20AQ1s9/hhp3ulI
+8pAvl+xVo4wQ+iBSvOzcy248Q+Xi6+zjceF7UNRgoYPgtJjKhdwcHV3mvFFrS/fp
+9ggoAChaJQWDO1OCfUgTWXImhkw+vcDR11OVMAJ/h73dqzJPI9mfq44PTTHfYtgr
+v4LAQAOlhXIAa2B+a6PlF6sqDqJaW5jLTcERjsBwnRhUGi7JevQzkejujX/vdA+N
+jRBjKH/KLU5h3Q7wUchvIez0PXWVTCnZjpA9aR4m7YV05nKQfxtGd71czYDYk+j8
+hd005jetT4ir7JkAWValBybJVksCAwEAAaN1MHMwHQYDVR0OBBYEFJl9s51SnjJt
+V/wgKWqV5Q6jnv1ZMEQGA1UdIwQ9MDuAFJl9s51SnjJtV/wgKWqV5Q6jnv1ZoRik
+FjAUMRIwEAYDVQQDEwlsb2NhbGhvc3SCCQCS5DWcxETyFDAMBgNVHRMEBTADAQH/
+MA0GCSqGSIb3DQEBBQUAA4IBAQB2ldLeLCc+lxK5i0EZquLamMBJwDIjGpT0JMP9
+b4XQOK2JABIu54BQIZhwcjk3FDJz/uOW5vm8k1kYni8FCjNZAaRZzCUfiUYTbTKL
+Rq9LuIAODyP2dnTqyKaQOOJHvrx9MRZ3XVecXPS0Tib4aO57vCaAbIkmhtYpTWmw
+e3t8CAIDVtgvjR6Se0a1JA4LktR7hBu22tDImvCSJn1nVAaHpani6iPBPPdMuMsP
+TBoeQfj8VpqBUjCStqJGa8ytjDFX73YaxV2mgrtGwPNme1x3YNRR11yTu7tksyMO
+GrmgxNriqYRchBhNEf72AKF0LR1ByKwfbDB9rIsV00HtCgOp
+-----END CERTIFICATE-----
27 test/server_key.pem
View
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAl5QI5nFua2eAXhLmVdySNIMtz7wL5eMwFgE/LLl4p0ljbQBD
+Wz3+GGne6UjykC+X7FWjjBD6IFK87NzLbjxD5eLr7ONx4XtQ1GChg+C0mMqF3Bwd
+Xea8UWtL9+n2CCgAKFolBYM7U4J9SBNZciaGTD69wNHXU5UwAn+Hvd2rMk8j2Z+r
+jg9NMd9i2Cu/gsBAA6WFcgBrYH5ro+UXqyoOolpbmMtNwRGOwHCdGFQaLsl69DOR
+6O6Nf+90D42NEGMof8otTmHdDvBRyG8h7PQ9dZVMKdmOkD1pHibthXTmcpB/G0Z3
+vVzNgNiT6PyF3TTmN61PiKvsmQBZVqUHJslWSwIDAQABAoIBACI8Ky5xHDFh9RpK
+Rn/KC7OUlTpADKflgizWJ0Cgu2F9L9mkn5HyFHvLHa+u7CootbWJOiEejH/UcBtH
+WyMQtX0snYCpdkUpJv5wvMoebGu+AjHOn8tfm9T/2O6rhwgckLyMb6QpGbMo28b1
+p9QiY17BJPZx7qJQJcHKsAvwDwSThlb7MFmWf42LYWlzybpeYQvwpd+UY4I0WXLu
+/dqJIS9Npq+5Y5vbo2kAEAssb2hSCvhCfHmwFdKmBzlvgOn4qxgZ1iHQgfKI6Z3Y
+J0573ZgOVTuacn+lewtdg5AaHFcl/zIYEr9SNqRoPNGbPliuv6k6N2EYcufWL5lR
+sCmmmHECgYEAxm+7OpepGr++K3+O1e1MUhD7vSPkKJrCzNtUxbOi2NWj3FFUSPRU
+adWhuxvUnZgTcgM1+KuQ0fB2VmxXe9IDcrSFS7PKFGtd2kMs/5mBw4UgDZkOQh+q
+kDiBEV3HYYJWRq0w3NQ/9Iy1jxxdENHtGmG9aqamHxNtuO608wGW2S8CgYEAw4yG
+ZyAic0Q/U9V2OHI0MLxLCzuQz17C2wRT1+hBywNZuil5YeTuIt2I46jro6mJmWI2
+fH4S/geSZzg2RNOIZ28+aK79ab2jWBmMnvFCvaru+odAuser4N9pfAlHZvY0pT+S
+1zYX3f44ygiio+oosabLC5nWI0zB2gG8pwaJlaUCgYEAgr7poRB+ZlaCCY0RYtjo
+mYYBKD02vp5BzdKSB3V1zeLuBWM84pjB6b3Nw0fyDig+X7fH3uHEGN+USRs3hSj6
+BqD01s1OT6fyfbYXNw5A1r+nP+5h26Wbr0zblcKxdQj4qbbBZC8hOJNhqTqqA0Qe
+MmzF7jiBaiZV/Cyj4x1f9BcCgYEAhjL6SeuTuOctTqs/5pz5lDikh6DpUGcH8qaV
+o6aRAHHcMhYkZzpk8yh1uUdD7516APmVyvn6rrsjjhLVq4ZAJjwB6HWvE9JBN0TR
+bILF+sREHUqU8Zn2Ku0nxyfXCKIOnxlx/J/y4TaGYqBqfXNFWiXNUrjQbIlQv/xR
+K48g/MECgYBZdQlYbMSDmfPCC5cxkdjrkmAl0EgV051PWAi4wR+hLxIMRjHBvAk7
+IweobkFvT4TICulgroLkYcSa5eOZGxB/DHqcQCbWj3reFV0VpzmTDoFKG54sqBRl
+vVntGt0pfA40fF17VoS7riAdHF53ippTtsovHEsg5tq5NrBl5uKm2g==
+-----END RSA PRIVATE KEY-----
Please sign in to comment.
Something went wrong with that request. Please try again.