Source

chut / src / chut_webserver.erl

-module(chut_webserver).
-behaviour(gen_server).
-define(SERVER, ?MODULE).
%% TODO: replace error returns par throws.
%% API
-export([start_link/1, dispatch_requests/2, stop/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

start_link(Port) ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).

init([Port]) ->
    process_flag(trap_exit, true),
    %% load conf
    {ok, Conf} = file:consult("conf/auth.cfg"),
    io:format("~p (~p) starting on port ~p...~n", [?MODULE, self(), Port]),
    mochiweb_http:start([{port, Port},
                         {loop, fun(Req) -> dispatch_requests(Req, Conf) end}]),
    {ok, []}.

stop() ->
    gen_server:cast(?SERVER, stop).

dispatch_requests(Req, Conf) ->
    Path = Req:get(path),
    try 
        handle(Path, Req, Conf)
    catch
        throw:{params, Names} -> 
            Req:respond({400,
                        [],
                        ["Some parameters are undefined:",
                        string:join(Names, ", ")]});
        throw:authentify ->
            Req:respond({403, [], "Wrong Token"});
        throw:can_contact ->
            Req:respond({403, [], "Can not contact user"})
    end.

handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

handle_cast(stop, State) ->
    {stop, normal, State};

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info({'DOWN', _, _, {mochiweb_http, _}, _}, State) ->
    {stop, normal, State};

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    mochiweb_http:stop(),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% Internal functions
handle("/message", Req, Conf) ->
    io:format("message call~n"),
    Params = Req:parse_qs(),
    From = proplists:get_value("from", Params),
    To = proplists:get_value("to", Params),
    Message = proplists:get_value("msg", Params),
    Token = proplists:get_value("token", Params),
    check_params([From, To, Message, Token], ["from", "to", "msg", "token"]),
    permissions(From, To, Token, Conf),
    {ok, H} = chut_client:connect(From),
    chut_client:message(From, To, Message),
    chut_client:disconnect(From, H),
    %% we clear the event messages because mochiweb re-uses the same procs
    %% over and over again in a pool. Otherwise, memory leaks and when the
    %% listen call gets the right message process, a backlog gets printed to
    %% the user.
    lib:flush_receive(),
    reply(Req, mochijson2:encode(ok), Params);

handle("/listen", Req, Conf) ->
    io:format("listen call~n"),
    Params = Req:parse_qs(),
    Nick = proplists:get_value("id", Params),
    Token = proplists:get_value("token", Params),
    check_params([Nick, Token], ["id", "token"]),
    permissions(Nick, Token, Conf),
    {ok,Handler} = chut_client:connect(Nick),
    Msgs = chut_client:listen(Nick,Handler),
    JSON = mochijson2:encode([json_prepare(M) || M <- Msgs]),
    chut_client:disconnect(Nick, Handler),
    reply(Req, JSON, Params);

handle("/history", Req, Conf) ->
    io:format("history call~n"),
    Params = Req:parse_qs(),
    Nick = proplists:get_value("id",Params),
    Token = proplists:get_value("token", Params),
    check_params([Nick, Token], ["id", "token"]),
    permissions(Nick, Token, Conf),
    {ok, Handler} = chut_client:connect(Nick),
    Msgs = chut_client:history(Nick),
    JSON = mochijson2:encode([json_prepare(M) || M <- Msgs]),
    chut_client:disconnect(Nick, Handler),
    reply(Req, JSON, Params);

%% Test functions for auth and permissions
handle("/auth", Req, _) ->
    io:format("auth call~n"),
    Params = Req:parse_post(),
    io:format("Params: ~p~n",[Params]),
    Req:respond({200, [{"Content-Type", "text/plain"}], "true"});
handle("/can-contact", Req, _) ->
    io:format("auth call~n"),
    Params = Req:parse_post(),
    io:format("Params: ~p~n",[Params]),
    Req:respond({200, [{"Content-Type", "text/plain"}], "true"});

handle(Path, Req, _) ->
    io:format("Unexpected ~p call~n", [Path]),
    Params = Req:parse_qs(),
    reply(Req, mochijson2:encode([error,list_to_binary(Path)]), Params).

%% have to convert lists to binaries to be strings in json
%% also gotta change the internal format of things to make it fit
json_prepare({Verb=[_|_],To,Msg}) -> json_prepare({unicode:characters_to_binary(Verb),To,Msg});
json_prepare({Verb,To=[_|_],Msg}) -> json_prepare({Verb,unicode:characters_to_binary(To),Msg});
json_prepare({Verb,To,Msg=[_|_]}) -> json_prepare({Verb,To,unicode:characters_to_binary(Msg)});
json_prepare({Verb,To,Msg}) ->
    {struct, [{action,Verb},{to,To},{message,Msg}]}.

reply(Req, JSON, Params) ->
    Callback = proplists:get_value("callback", Params),
    if Callback =/= undefined ->
        Req:respond({200, [{"Content-Type", "text/javascript"}],
                    [Callback,$(,JSON,$),$;]});
       Callback =:= undefined ->
        Req:respond({400, [], "Callback undefined"})
    end.

            
%% replies with an error whenever some parameters are undefined      
check_params(Params, Names) ->
    case lists:any(fun(X) -> X =:= undefined end, Params) of
        true  -> throw({params, Names});
        false -> ok
    end.

permissions(From, Token, Conf) ->
    {base, Url} = lists:keyfind(base, 1, Conf),
    {auth, Auth} = lists:keyfind(auth, 1, Conf),
    case chut_auth:authentify(Url++Auth, From, Token) of
        true -> ok;
        false -> throw(authentify)
    end.
    
permissions(From, To, Token, Conf) ->
    permissions(From, Token, Conf),
    {base, Url} = lists:keyfind(base, 1, Conf),
    {can_contact, Can} = lists:keyfind(can_contact, 1, Conf), 
    case chut_auth:can_contact(Url++Can, From, To) of
        true -> ok;
        false -> throw(can_contact)
    end.