From da7225594010d0df20779a0915474ccedd25d7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 7 Mar 2011 22:59:22 +0100 Subject: Initial commit. --- .gitignore | 1 + LICENSE | 13 +++ Makefile | 13 +++ README.md | 69 ++++++++++++++ include/http.hrl | 29 ++++++ include/types.hrl | 57 ++++++++++++ src/cowboy.app.src | 26 ++++++ src/cowboy_acceptor.erl | 44 +++++++++ src/cowboy_app.erl | 30 ++++++ src/cowboy_dispatcher.erl | 153 +++++++++++++++++++++++++++++++ src/cowboy_http_protocol.erl | 214 +++++++++++++++++++++++++++++++++++++++++++ src/cowboy_http_req.erl | 157 +++++++++++++++++++++++++++++++ src/cowboy_listener_sup.erl | 43 +++++++++ src/cowboy_protocols_sup.erl | 41 +++++++++ src/cowboy_sup.erl | 37 ++++++++ src/cowboy_tcp_transport.erl | 60 ++++++++++++ 16 files changed, 987 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 include/http.hrl create mode 100644 include/types.hrl create mode 100644 src/cowboy.app.src create mode 100644 src/cowboy_acceptor.erl create mode 100644 src/cowboy_app.erl create mode 100644 src/cowboy_dispatcher.erl create mode 100644 src/cowboy_http_protocol.erl create mode 100644 src/cowboy_http_req.erl create mode 100644 src/cowboy_listener_sup.erl create mode 100644 src/cowboy_protocols_sup.erl create mode 100644 src/cowboy_sup.erl create mode 100644 src/cowboy_tcp_transport.erl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfb2329 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ebin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7de99bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2011, Loïc Hoguin + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ec86c18 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc8b127 --- /dev/null +++ b/README.md @@ -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!"}. diff --git a/include/http.hrl b/include/http.hrl new file mode 100644 index 0000000..dc78b34 --- /dev/null +++ b/include/http.hrl @@ -0,0 +1,29 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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 +}). diff --git a/include/types.hrl b/include/types.hrl new file mode 100644 index 0000000..faee24e --- /dev/null +++ b/include/types.hrl @@ -0,0 +1,57 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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(). diff --git a/src/cowboy.app.src b/src/cowboy.app.src new file mode 100644 index 0000000..d3dd958 --- /dev/null +++ b/src/cowboy.app.src @@ -0,0 +1,26 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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, []} +]}. diff --git a/src/cowboy_acceptor.erl b/src/cowboy_acceptor.erl new file mode 100644 index 0000000..9c35edb --- /dev/null +++ b/src/cowboy_acceptor.erl @@ -0,0 +1,44 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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). diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl new file mode 100644 index 0000000..4f81ff6 --- /dev/null +++ b/src/cowboy_app.erl @@ -0,0 +1,30 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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. diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl new file mode 100644 index 0000000..3e9a7e0 --- /dev/null +++ b/src/cowboy_dispatcher.erl @@ -0,0 +1,153 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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. diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl new file mode 100644 index 0000000..726f33c --- /dev/null +++ b/src/cowboy_http_protocol.erl @@ -0,0 +1,214 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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. diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl new file mode 100644 index 0000000..27583c1 --- /dev/null +++ b/src/cowboy_http_req.erl @@ -0,0 +1,157 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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. diff --git a/src/cowboy_listener_sup.erl b/src/cowboy_listener_sup.erl new file mode 100644 index 0000000..3c8b3c2 --- /dev/null +++ b/src/cowboy_listener_sup.erl @@ -0,0 +1,43 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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}}. diff --git a/src/cowboy_protocols_sup.erl b/src/cowboy_protocols_sup.erl new file mode 100644 index 0000000..04f662e --- /dev/null +++ b/src/cowboy_protocols_sup.erl @@ -0,0 +1,41 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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]}]}}. diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl new file mode 100644 index 0000000..2cb9c10 --- /dev/null +++ b/src/cowboy_sup.erl @@ -0,0 +1,37 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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}}. diff --git a/src/cowboy_tcp_transport.erl b/src/cowboy_tcp_transport.erl new file mode 100644 index 0000000..dec7940 --- /dev/null +++ b/src/cowboy_tcp_transport.erl @@ -0,0 +1,60 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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). -- cgit v1.2.3