0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 04:10:24 +00:00

Initial commit.

This commit is contained in:
Loïc Hoguin 2011-03-07 22:59:22 +01:00
commit da72255940
16 changed files with 987 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
ebin

13
LICENSE Normal file
View file

@ -0,0 +1,13 @@
Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

13
Makefile Normal file
View file

@ -0,0 +1,13 @@
# See LICENSE for licensing information.
all: app
app:
@./rebar compile
clean:
@./rebar clean
rm -f erl_crash.dump
test:
@./rebar eunit

69
README.md Normal file
View file

@ -0,0 +1,69 @@
Cowboy
======
Cowboy is a small, fast and modular HTTP server written in Erlang.
Goals
-----
Cowboy aims to provide the following advantages:
* **Small** codebase.
* Damn **fast**.
* **Modular**: transport, protocol and handlers are replaceable. (see below)
* Easy to **embed** inside another application.
* Selectively **dispatch** requests to handlers, allowing you to send some
requests to your embedded code and others to a FastCGI application in
PHP or Ruby.
* No parameterized module. No process dictionary. **Clean** Erlang code.
The server is currently in early development stage. Comments, suggestions are
more than welcome. To contribute, either open bug reports, or fork the project
and send us pull requests with new or improved functionality. Of course you
might want to discuss your plans with us before you do any serious work so
we can share ideas and save everyone time.
Embedding Cowboy
----------------
* Add Cowboy as a rebar dependency to your application.
* Start Cowboy and add one or more listeners.
* Write handlers.
Starting and stopping
---------------------
Cowboy can be started and stopped like any other application. However the
Cowboy application do not start any listener, those must be started manually.
A listener is a special kind of supervisor that handles a pool of acceptor
processes. An acceptor simply accept connections and forward them to a
protocol module, for example HTTP. You must thus define the transport and
protocol module to use for the listener, their options and the number of
acceptors in the pool before you can start a listener supervisor.
For HTTP applications the transport can be either TCP or SSL for HTTP and
HTTPS respectively. On the other hand, the protocol is of course HTTP.
Code speaks more than words:
application:start(cowboy),
Dispatch = [
%% Host, Path, Handler, Opts
{'_', '_', my_handler, []}
],
%% NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
cowboy_listener_sup:start_link(100,
cowboy_tcp_transport, [{port, 8080}],
cowboy_http_protocol, [{dispatch, Dispatch}]
).
You must also write the `my_handler` module to process requests. You can
use one of the predefined handlers or write your own. An hello world HTTP
handler could be written like this:
-module(my_handler).
-export([handle/2]).
handle(Opts, Req) ->
{reply, 200, [], "Hello World!"}.

29
include/http.hrl Normal file
View file

@ -0,0 +1,29 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-record(http_req, {
listener = undefined :: undefined | atom(), %% todo
method = 'GET' :: http_method(),
version = {1, 1} :: http_version(),
peer = undefined :: undefined | {Address::ip_address(), Port::port_number()},
host = undefined :: undefined | path_tokens(), %% todo
raw_host = undefined :: undefined | string(), %% todo
path = undefined :: undefined | path_tokens(), %% todo
raw_path = undefined :: undefined | string(), %% todo
qs_vals = undefined :: undefined | bindings(), %% todo
raw_qs = undefined :: undefined | string(),
bindings = undefined :: undefined | bindings(),
headers = [] :: http_headers()
%% cookies = undefined :: undefined | http_cookies() %% @todo
}).

57
include/types.hrl Normal file
View file

@ -0,0 +1,57 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-type application_start_type() :: normal |
{takeover, Node::node()} | {failover, Node::node()}.
-type posix() :: atom().
-opaque socket() :: term().
-opaque sslsocket() :: term().
-type ipv4_address() :: {0..255, 0..255, 0..255, 0..255}.
-type ipv6_address() :: {0..65535, 0..65535, 0..65535, 0..65535,
0..65535, 0..65535, 0..65535, 0..65535}.
-type ip_address() :: ipv4_address() | ipv6_address().
-type port_number() :: 0..65535.
-type bindings() :: list({Key::atom(), Value::string()}).
-type path_tokens() :: list(string()).
-type match() :: '_' | list(string() | '_' | atom()).
-type dispatch_rules() :: {Host::match(), Path::match(),
Handler::module(), Opts::term()}.
-type dispatch() :: list(dispatch_rules()).
-type http_method() :: 'OPTIONS' | 'GET' | 'HEAD'
| 'POST' | 'PUT' | 'DELETE' | 'TRACE' | string().
-type http_uri() :: '*' | {absoluteURI, http | https, Host::string(),
Port::integer() | undefined, Path::string()}
| {scheme, Scheme::string(), string()}
| {abs_path, string()} | string().
-type http_version() :: {Major::integer(), Minor::integer()}.
-type http_header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma'
| 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset'
| 'Accept-Encoding' | 'Accept-Language' | 'Authorization' | 'From' | 'Host'
| 'If-Modified-Since' | 'If-Match' | 'If-None-Match' | 'If-Range'
| 'If-Unmodified-Since' | 'Max-Forwards' | 'Proxy-Authorization' | 'Range'
| 'Referer' | 'User-Agent' | 'Age' | 'Location' | 'Proxy-Authenticate'
| 'Public' | 'Retry-After' | 'Server' | 'Vary' | 'Warning'
| 'Www-Authenticate' | 'Allow' | 'Content-Base' | 'Content-Encoding'
| 'Content-Language' | 'Content-Length' | 'Content-Location'
| 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag'
| 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
| 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
| 'Proxy-Connection' | string().
-type http_headers() :: list({http_header(), string()}).
%% -type http_cookies() :: term(). %% @todo
-type http_status() :: non_neg_integer() | string().

26
src/cowboy.app.src Normal file
View file

@ -0,0 +1,26 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
{application, cowboy, [
{description, "Small, fast, modular HTTP server."},
{vsn, "0.1.0"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib
]},
{mod, {cowboy_app, []}},
{env, []}
]}.

44
src/cowboy_acceptor.erl Normal file
View file

@ -0,0 +1,44 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_acceptor).
-export([start_link/4]). %% API.
-export([acceptor/4]). %% Internal.
-include("include/types.hrl").
%% API.
-spec start_link(LSocket::socket(), Transport::module(),
Protocol::module(), Opts::term()) -> {ok, Pid::pid()}.
start_link(LSocket, Transport, Protocol, Opts) ->
Pid = spawn_link(?MODULE, acceptor, [LSocket, Transport, Protocol, Opts]),
{ok, Pid}.
%% Internal.
-spec acceptor(LSocket::socket(), Transport::module(),
Protocol::module(), Opts::term()) -> no_return().
acceptor(LSocket, Transport, Protocol, Opts) ->
case Transport:accept(LSocket) of
{ok, CSocket} ->
{ok, Pid} = supervisor:start_child(cowboy_protocols_sup,
[CSocket, Transport, Protocol, Opts]),
ok = Transport:controlling_process(CSocket, Pid);
{error, _Reason} ->
%% @todo Probably do something here. If the socket was closed,
%% we may want to try and listen again on the port?
ignore
end,
?MODULE:acceptor(LSocket, Transport, Protocol, Opts).

30
src/cowboy_app.erl Normal file
View file

@ -0,0 +1,30 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_app).
-behaviour(application).
-export([start/2, stop/1]). %% API.
-include("include/types.hrl").
%% API.
-spec start(Type::application_start_type(), Args::term()) -> {ok, Pid::pid()}.
start(_Type, _Args) ->
cowboy_sup:start_link().
-spec stop(State::term()) -> ok.
stop(_State) ->
ok.

153
src/cowboy_dispatcher.erl Normal file
View file

@ -0,0 +1,153 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_dispatcher).
-export([split_host/1, split_path/1, match/3]). %% API.
-include("include/types.hrl").
-include_lib("eunit/include/eunit.hrl").
%% API.
-spec split_host(Host::string()) -> Tokens::path_tokens().
split_host(Host) ->
string:tokens(Host, ".").
-spec split_path(Path::string()) -> {Tokens::path_tokens(), Qs::string()}.
split_path(Path) ->
case string:chr(Path, $?) of
0 ->
{string:tokens(Path, "/"), []};
N ->
{Path2, [$?|Qs]} = lists:split(N - 1, Path),
{string:tokens(Path2, "/"), Qs}
end.
-spec match(Host::path_tokens(), Path::path_tokens(), Dispatch::dispatch())
-> {ok, Handler::module(), Opts::term(), Binds::bindings()} | {error, notfound}.
match(_Host, _Path, []) ->
{error, notfound};
match(_Host, _Path, [{'_', '_', Handler, Opts}|_Tail]) ->
{ok, Handler, Opts, []};
match(Host, Path, [{HostMatch, PathMatch, Handler, Opts}|Tail]) ->
case try_match(host, Host, HostMatch) of
false -> match(Host, Path, Tail);
{true, HostBinds} -> case try_match(path, Path, PathMatch) of
false -> match(Host, Path, Tail);
{true, PathBinds} -> {ok, Handler, Opts, HostBinds ++ PathBinds}
end
end.
%% Internal.
-spec try_match(Type::host | path, List::path_tokens(), Match::match())
-> {true, Binds::bindings()} | false.
try_match(_Type, _List, '_') ->
{true, []};
try_match(_Type, List, Match) when length(List) =/= length(Match) ->
false;
try_match(host, List, Match) ->
list_match(lists:reverse(List), lists:reverse(Match), []);
try_match(path, List, Match) ->
list_match(List, Match, []).
-spec list_match(List::path_tokens(), Match::match(), Binds::bindings())
-> {true, Binds::bindings()} | false.
%% Atom '_' matches anything, continue.
list_match([_E|Tail], ['_'|TailMatch], Binds) ->
list_match(Tail, TailMatch, Binds);
%% Both values match, continue.
list_match([E|Tail], [E|TailMatch], Binds) ->
list_match(Tail, TailMatch, Binds);
%% Bind E to the variable name V and continue.
list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
list_match(Tail, TailMatch, [{V, E}|Binds]);
%% Values don't match, stop.
list_match([_E|_Tail], [_F|_TailMatch], _Binds) ->
false;
%% Match complete.
list_match([], [], Binds) ->
{true, Binds}.
%% Tests.
-ifdef(TEST).
split_host_test_() ->
%% {Host, Result}
Tests = [
{"", []},
{".........", []},
{"*", ["*"]},
{"cowboy.dev-extend.eu", ["cowboy", "dev-extend", "eu"]},
{"dev-extend..eu", ["dev-extend", "eu"]},
{"dev-extend.eu", ["dev-extend", "eu"]},
{"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z",
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]}
],
[{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
split_path_test_() ->
%% {Path, Result, QueryString}
Tests = [
{"?", [], []},
{"???", [], "??"},
{"*", ["*"], []},
{"/", [], []},
{"/users", ["users"], []},
{"/users?", ["users"], []},
{"/users?a", ["users"], "a"},
{"/users/42/friends?a=b&c=d&e=notsure?whatever",
["users", "42", "friends"], "a=b&c=d&e=notsure?whatever"}
],
[{P, fun() -> {R, Qs} = split_path(P) end} || {P, R, Qs} <- Tests].
match_test_() ->
Dispatch = [
{["www", '_', "dev-extend", "eu"], ["users", '_', "mails"],
match_any_subdomain_users, []},
{["dev-extend", "eu"], ["users", id, "friends"],
match_extend_users_friends, []},
{["dev-extend", var], ["threads", var],
match_duplicate_vars, [we, {expect, two}, var, here]},
{["dev-extend", "eu"], '_', match_extend, []},
{["erlang", ext], '_', match_erlang_ext, []},
{'_', ["users", id, "friends"], match_users_friends, []},
{'_', '_', match_any, []}
],
%% {Host, Path, Result}
Tests = [
{["any"], [], {ok, match_any, [], []}},
{["www", "any", "dev-extend", "eu"], ["users", "42", "mails"],
{ok, match_any_subdomain_users, [], []}},
{["www", "dev-extend", "eu"], ["users", "42", "mails"],
{ok, match_any, [], []}},
{["www", "any", "dev-extend", "eu"], ["not_users", "42", "mails"],
{ok, match_any, [], []}},
{["dev-extend", "eu"], [], {ok, match_extend, [], []}},
{["dev-extend", "eu"], ["users", "42", "friends"],
{ok, match_extend_users_friends, [], [{id, "42"}]}},
{["erlang", "fr"], '_', {ok, match_erlang_ext, [], [{ext, "fr"}]}},
{["any"], ["users", "444", "friends"],
{ok, match_users_friends, [], [{id, "444"}]}},
{["dev-extend", "eu"], ["threads", "987"],
{ok, match_duplicate_vars, [we, {expect, two}, var, here],
[{var, "eu"}, {var, "987"}]}}
],
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
R = match(H, P, Dispatch)
end} || {H, P, R} <- Tests].
-endif.

View file

@ -0,0 +1,214 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_http_protocol).
-export([start_link/3]). %% API.
-export([init/3]). %% FSM.
-include("include/types.hrl").
-include("include/http.hrl").
-record(state, {
socket :: socket(),
transport :: module(),
dispatch :: dispatch(),
timeout :: timeout(),
connection = keepalive :: keepalive | close
}).
%% API.
-spec start_link(Socket::socket(), Transport::module(), Opts::term())
-> {ok, Pid::pid()}.
start_link(Socket, Transport, Opts) ->
Pid = spawn_link(?MODULE, init, [Socket, Transport, Opts]),
{ok, Pid}.
%% FSM.
-spec init(Socket::socket(), Transport::module(), Opts::term())
-> ok | {error, no_ammo}.
init(Socket, Transport, Opts) ->
Dispatch = proplists:get_value(dispatch, Opts, []),
Timeout = proplists:get_value(timeout, Opts, 5000),
wait_request(#state{socket=Socket, transport=Transport,
dispatch=Dispatch, timeout=Timeout}).
-spec wait_request(State::#state{}) -> ok.
wait_request(State=#state{socket=Socket, transport=Transport, timeout=T}) ->
Transport:setopts(Socket, [{packet, http}]),
case Transport:recv(Socket, 0, T) of
{ok, Request} -> request(Request, State);
{error, timeout} -> error_terminate(408, State);
{error, closed} -> terminate(State)
end.
-spec request({http_request, Method::http_method(), URI::http_uri(),
Version::http_version()}, State::#state{}) -> ok.
%% @todo We probably want to handle some things differently between versions.
request({http_request, _Method, _URI, Version}, State)
when Version =/= {1, 0}, Version =/= {1, 1} ->
error_terminate(505, State);
%% @todo We need to cleanup the URI properly.
request({http_request, Method, {abs_path, AbsPath}, Version},
State=#state{socket=Socket, transport=Transport}) ->
{Path, Qs} = cowboy_dispatcher:split_path(AbsPath),
{ok, Peer} = Transport:peername(Socket),
wait_header(#http_req{method=Method, version=Version,
peer=Peer, path=Path, raw_qs=Qs}, State).
-spec wait_header(Req::#http_req{}, State::#state{}) -> ok.
%% @todo We don't want to wait T at each header...
%% We want to wait T total until we reach the body.
wait_header(Req, State=#state{socket=Socket,
transport=Transport, timeout=T}) ->
case Transport:recv(Socket, 0, T) of
{ok, Header} -> header(Header, Req, State);
{error, timeout} -> error_terminate(408, State);
{error, closed} -> terminate(State)
end.
-spec header({http_header, I::integer(), Field::http_header(), R::term(),
Value::string()} | http_eoh, Req::#http_req{}, State::#state{}) -> ok.
header({http_header, _I, 'Host', _R, Value}, Req, State) ->
Host = cowboy_dispatcher:split_host(Value),
%% @todo We have Host and Path at this point, dispatch right away and
%% error_terminate(404) early if it fails.
wait_header(Req#http_req{host=Host,
headers=[{'Host', Value}|Req#http_req.headers]}, State);
header({http_header, _I, 'Connection', _R, Connection}, Req, State) ->
wait_header(Req#http_req{
headers=[{'Connection', Connection}|Req#http_req.headers]},
State#state{connection=connection_to_atom(Connection)});
header({http_header, _I, Field, _R, Value}, Req, State) ->
wait_header(Req#http_req{headers=[{Field, Value}|Req#http_req.headers]},
State);
%% The Host header is required.
header(http_eoh, #http_req{host=undefined}, State) ->
error_terminate(400, State);
header(http_eoh, Req=#http_req{host=Host, path=Path},
State=#state{dispatch=Dispatch}) ->
%% @todo We probably want to filter the Host and Patch here to allow
%% things like url rewriting.
dispatch(cowboy_dispatcher:match(Host, Path, Dispatch), Req, State).
-spec dispatch({ok, Handler::module(), Opts::term(), Binds::bindings()}
| {error, notfound}, Req::#http_req{}, State::#state{}) -> ok.
dispatch({ok, Handler, Opts, Binds}, Req, State) ->
case Handler:handle(Opts, Req#http_req{bindings=Binds}) of
{reply, RCode, RHeaders, RBody} ->
reply(RCode, RHeaders, RBody, State)
%% @todo stream_reply, request_body, stream_request_body...
end;
dispatch({error, notfound}, _Req, State) ->
error_terminate(404, State).
-spec error_terminate(Code::http_status(), State::#state{}) -> ok.
error_terminate(Code, State) ->
reply(Code, [], [], State#state{connection=close}),
terminate(State).
-spec terminate(State::#state{}) -> ok.
terminate(#state{socket=Socket, transport=Transport}) ->
Transport:close(Socket),
ok.
-spec reply(Code::http_status(), Headers::http_headers(), Body::iolist(),
State::#state{}) -> ok.
%% @todo Don't be naive about the headers!
reply(Code, Headers, Body, State=#state{socket=Socket,
transport=TransportMod, connection=Connection}) ->
StatusLine = ["HTTP/1.1 ", status(Code), "\r\n"],
BaseHeaders = ["Connection: ", atom_to_connection(Connection),
"\r\nContent-Length: ", integer_to_list(iolist_size(Body)), "\r\n"],
TransportMod:send(Socket,
[StatusLine, BaseHeaders, Headers, "\r\n", Body]),
next_request(State).
-spec next_request(State::#state{}) -> ok.
next_request(State=#state{connection=keepalive}) ->
wait_request(State);
next_request(State=#state{connection=close}) ->
terminate(State).
%% Internal.
-spec connection_to_atom(Connection::string()) -> keepalive | close.
connection_to_atom(Connection) ->
case string:to_lower(Connection) of
"close" -> close;
_Any -> keepalive
end.
-spec atom_to_connection(Atom::keepalive | close) -> string().
atom_to_connection(keepalive) ->
"keep-alive";
atom_to_connection(close) ->
"close".
-spec status(Code::http_status()) -> string().
status(100) -> "100 Continue";
status(101) -> "101 Switching Protocols";
status(102) -> "102 Processing";
status(200) -> "200 OK";
status(201) -> "201 Created";
status(202) -> "202 Accepted";
status(203) -> "203 Non-Authoritative Information";
status(204) -> "204 No Content";
status(205) -> "205 Reset Content";
status(206) -> "206 Partial Content";
status(207) -> "207 Multi-Status";
status(226) -> "226 IM Used";
status(300) -> "300 Multiple Choices";
status(301) -> "301 Moved Permanently";
status(302) -> "302 Found";
status(303) -> "303 See Other";
status(304) -> "304 Not Modified";
status(305) -> "305 Use Proxy";
status(306) -> "306 Switch Proxy";
status(307) -> "307 Temporary Redirect";
status(400) -> "400 Bad Request";
status(401) -> "401 Unauthorized";
status(402) -> "402 Payment Required";
status(403) -> "403 Forbidden";
status(404) -> "404 Not Found";
status(405) -> "405 Method Not Allowed";
status(406) -> "406 Not Acceptable";
status(407) -> "407 Proxy Authentication Required";
status(408) -> "408 Request Timeout";
status(409) -> "409 Conflict";
status(410) -> "410 Gone";
status(411) -> "411 Length Required";
status(412) -> "412 Precondition Failed";
status(413) -> "413 Request Entity Too Large";
status(414) -> "414 Request-URI Too Long";
status(415) -> "415 Unsupported Media Type";
status(416) -> "416 Requested Range Not Satisfiable";
status(417) -> "417 Expectation Failed";
status(418) -> "418 I'm a teapot";
status(422) -> "422 Unprocessable Entity";
status(423) -> "423 Locked";
status(424) -> "424 Failed Dependency";
status(425) -> "425 Unordered Collection";
status(426) -> "426 Upgrade Required";
status(500) -> "500 Internal Server Error";
status(501) -> "501 Not Implemented";
status(502) -> "502 Bad Gateway";
status(503) -> "503 Service Unavailable";
status(504) -> "504 Gateway Timeout";
status(505) -> "505 HTTP Version Not Supported";
status(506) -> "506 Variant Also Negotiates";
status(507) -> "507 Insufficient Storage";
status(510) -> "510 Not Extended";
status(L) when is_list(L) -> L.

157
src/cowboy_http_req.erl Normal file
View file

@ -0,0 +1,157 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_http_req).
-export([
listener/1, method/1, version/1, peer/1,
host/1, raw_host/1,
path/1, raw_path/1,
qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
binding/2, binding/3, bindings/1,
header/2, header/3, headers/1
%% cookie/2, cookie/3, cookies/1 @todo
]). %% API.
-include("include/types.hrl").
-include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl").
%% API.
-spec listener(Req::#http_req{}) -> {Listener::atom(), Req::#http_req{}}.
listener(Req) ->
{Req#http_req.listener, Req}.
-spec method(Req::#http_req{}) -> {Method::http_method(), Req::#http_req{}}.
method(Req) ->
{Req#http_req.method, Req}.
-spec version(Req::#http_req{}) -> {Version::http_version(), Req::#http_req{}}.
version(Req) ->
{Req#http_req.version, Req}.
-spec peer(Req::#http_req{})
-> {{Address::ip_address(), Port::port_number()}, Req::#http_req{}}.
peer(Req) ->
{Req#http_req.peer, Req}.
-spec host(Req::#http_req{}) -> {Host::path_tokens(), Req::#http_req{}}.
host(Req) ->
{Req#http_req.host, Req}.
-spec raw_host(Req::#http_req{}) -> {RawHost::string(), Req::#http_req{}}.
raw_host(Req) ->
{Req#http_req.raw_host, Req}.
-spec path(Req::#http_req{}) -> {Path::path_tokens(), Req::#http_req{}}.
path(Req) ->
{Req#http_req.path, Req}.
-spec raw_path(Req::#http_req{}) -> {RawPath::string(), Req::#http_req{}}.
raw_path(Req) ->
{Req#http_req.raw_path, Req}.
-spec qs_val(Name::atom(), Req::#http_req{})
-> {Value::string(), Req::#http_req{}}.
qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
QsVals = parse_qs(RawQs),
qs_val(Name, Req#http_req{qs_vals=QsVals});
qs_val(Name, Req) ->
{Name, Value} = lists:keyfind(Name, 1, Req#http_req.qs_vals),
{Value, Req}.
-spec qs_val(Name::atom(), Default::term(), Req::#http_req{})
-> {Value::string() | term(), Req::#http_req{}}.
qs_val(Name, Default, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
QsVals = parse_qs(RawQs),
qs_val(Name, Default, Req#http_req{qs_vals=QsVals});
qs_val(Name, Default, Req) ->
Value = proplists:get_value(Name, Req#http_req.qs_vals, Default),
{Value, Req}.
-spec qs_vals(Req::#http_req{}) -> list({Name::atom(), Value::string()}).
qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
QsVals = parse_qs(RawQs),
qs_vals(Req#http_req{qs_vals=QsVals});
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
{QsVals, Req}.
-spec raw_qs(Req::#http_req{}) -> {RawQs::string(), Req::#http_req{}}.
raw_qs(Req) ->
{Req#http_req.raw_qs, Req}.
-spec binding(Name::atom(), Req::#http_req{})
-> {Value::string(), Req::#http_req{}}.
binding(Name, Req) ->
{Name, Value} = lists:keyfind(Name, 1, Req#http_req.bindings),
{Value, Req}.
-spec binding(Name::atom(), Default::term(), Req::#http_req{})
-> {Value::string() | term(), Req::#http_req{}}.
binding(Name, Default, Req) ->
Value = proplists:get_value(Name, Req#http_req.bindings, Default),
{Value, Req}.
-spec bindings(Req::#http_req{})
-> {list({Name::atom(), Value::string()}), Req::#http_req{}}.
bindings(Req) ->
{Req#http_req.bindings, Req}.
-spec header(Name::atom() | string(), Req::#http_req{})
-> {Value::string(), Req::#http_req{}}.
header(Name, Req) ->
{Name, Value} = lists:keyfind(Name, 1, Req#http_req.headers),
{Value, Req}.
-spec header(Name::atom() | string(), Default::term(), Req::#http_req{})
-> {Value::string() | term(), Req::#http_req{}}.
header(Name, Default, Req) ->
Value = proplists:get_value(Name, Req#http_req.headers, Default),
{Value, Req}.
-spec headers(Req::#http_req{})
-> {list({Name::atom() | string(), Value::string()}), Req::#http_req{}}.
headers(Req) ->
{Req#http_req.headers, Req}.
%% Internal.
-spec parse_qs(Qs::string()) -> list({Name::string(), Value::string()}).
parse_qs(Qs) ->
Tokens = string:tokens(Qs, "&"),
[case string:chr(Token, $=) of
0 ->
{Token, true};
N ->
{Name, [$=|Value]} = lists:split(N - 1, Token),
{Name, Value}
end || Token <- Tokens].
%% Tests.
-ifdef(TEST).
parse_qs_test_() ->
%% {Qs, Result}
Tests = [
{"", []},
{"a=b", [{"a", "b"}]},
{"aaa=bbb", [{"aaa", "bbb"}]},
{"a&b", [{"a", true}, {"b", true}]},
{"a=b&c&d=e", [{"a", "b"}, {"c", true}, {"d", "e"}]},
{"a=b=c=d=e&f=g", [{"a", "b=c=d=e"}, {"f", "g"}]}
],
[{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
-endif.

View file

@ -0,0 +1,43 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_listener_sup).
-behaviour(supervisor).
-export([start_link/5]). %% API.
-export([init/1]). %% supervisor.
%% API.
-spec start_link(NbAcceptors::non_neg_integer(), Transport::module(),
TransOpts::term(), Protocol::module(), ProtoOpts::term())
-> {ok, Pid::pid()}.
start_link(NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) ->
case Transport:listen(TransOpts) of
{ok, LSocket} ->
supervisor:start_link(?MODULE, [LSocket,
NbAcceptors, Transport, Protocol, ProtoOpts]);
{error, Reason} ->
{error, Reason}
end.
%% supervisor.
%% @todo These specs should be improved.
-spec init(list(term())) -> term().
init([LSocket, NbAcceptors, Transport, Protocol, ProtoOpts]) ->
Procs = [{{acceptor, self(), N}, {cowboy_acceptor, start_link,
[LSocket, Transport, Protocol, ProtoOpts]}, permanent,
brutal_kill, worker, dynamic} || N <- lists:seq(1, NbAcceptors)],
{ok, {{one_for_one, 10, 10}, Procs}}.

View file

@ -0,0 +1,41 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_protocols_sup).
-behaviour(supervisor).
-export([start_link/0, start_protocol/4]). %% API.
-export([init/1]). %% supervisor.
-include("include/types.hrl").
-define(SUPERVISOR, ?MODULE).
%% API.
-spec start_link() -> {ok, Pid::pid()}.
start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
-spec start_protocol(Socket::socket(), Transport::module(),
Protocol::module(), Opts::term()) -> {ok, Pid::pid()}.
start_protocol(Socket, Transport, Protocol, Opts) ->
Protocol:start_link(Socket, Transport, Opts).
%% supervisor.
-spec init([]) -> term(). %% @todo These specs should be improved.
init([]) ->
{ok, {{simple_one_for_one, 0, 1}, [{?MODULE, {?MODULE, start_protocol, []},
temporary, brutal_kill, worker, [?MODULE]}]}}.

37
src/cowboy_sup.erl Normal file
View file

@ -0,0 +1,37 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_sup).
-behaviour(supervisor).
-export([start_link/0]). %% API.
-export([init/1]). %% supervisor.
-define(SUPERVISOR, ?MODULE).
%% API.
-spec start_link() -> {ok, Pid::pid()}.
start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
%% supervisor.
-spec init([]) -> term(). %% @todo These specs should be improved.
init([]) ->
Procs = [
{cowboy_protocols_sup, {cowboy_protocols_sup, start_link, []},
permanent, 5000, supervisor, [cowboy_protocols_sup]}
],
{ok, {{one_for_one, 10, 10}, Procs}}.

View file

@ -0,0 +1,60 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_tcp_transport).
-export([listen/1, accept/1, recv/3, send/2, setopts/2,
controlling_process/2, peername/1, close/1]).
-include("include/types.hrl").
-spec listen([{port, Port::port_number()}])
-> {ok, LSocket::socket()} | {error, Reason::posix()}.
listen(Opts) ->
{port, Port} = lists:keyfind(port, 1, Opts),
gen_tcp:listen(Port, [binary, {active, false},
{packet, raw}, {reuseaddr, true}]).
-spec accept(LSocket::socket())
-> {ok, Socket::socket()} | {error, Reason::closed | timeout | posix()}.
accept(LSocket) ->
gen_tcp:accept(LSocket).
-spec recv(Socket::socket(), Length::integer(), Timeout::timeout())
-> {ok, Packet::term()} | {error, Reason::closed | posix()}.
recv(Socket, Length, Timeout) ->
gen_tcp:recv(Socket, Length, Timeout).
-spec send(Socket::socket(), Packet::iolist())
-> ok | {error, Reason::posix()}.
send(Socket, Packet) ->
gen_tcp:send(Socket, Packet).
-spec setopts(Socket::socket(), Opts::list(term()))
-> ok | {error, Reason::posix()}.
setopts(Socket, Opts) ->
inet:setopts(Socket, Opts).
-spec controlling_process(Socket::socket(), Pid::pid())
-> ok | {error, Reason::closed | not_owner | posix()}.
controlling_process(Socket, Pid) ->
gen_tcp:controlling_process(Socket, Pid).
-spec peername(Socket::socket())
-> {ok, {Address::ip_address(), Port::port_number()}} | {error, posix()}.
peername(Socket) ->
inet:peername(Socket).
-spec close(Socket::socket()) -> ok | {error, Reason::posix}.
close(Socket) ->
gen_tcp:close(Socket).