summaryrefslogblamecommitdiffstats
path: root/src/bullet_handler.erl
blob: ed29188df56d583eb566707b1d5f7a8870a9e79c (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                                                             













                                                                           
                                
                                     
 







                                 


                            

                                              

                                                         

   


                             
                                                     
                                    
                                                                 



                                                            
                                                                              
                                       
                                                                                         



                                                                   
                                        
                                                             
                                                  
                                        









                                                                            
                                                                       

                                                                                 
            
                                         








                                                                                 
                                                        


                                    
                                                

                                    
                                                                      
                              
                                    




                                                                                             
                                                                                            




                                                                                            


                  

                                                               


                                                                                          



                                                                          
                                                     






                                                                          

            
                                      
           
                                                                               





                                                             
                                                  


                                                        
                                                        
                                                                           
                                                    










                                                                                        


                                                                                     










                                                                                        


                                                                                     




                                                                       















                                                                 

                           







                                                                  




                                               







                                                                                       









                                                    
%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
%%
%% 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(bullet_handler).
-behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).

-export([init/3]).
-export([handle/2]).
-export([info/3]).
-export([terminate/3]).
-export([websocket_init/3]).
-export([websocket_handle/3]).
-export([websocket_info/3]).
-export([websocket_terminate/3]).

-record(state, {
	handler :: module(),
	handler_state :: term(),
	% poll or eventsource for GET requests
	get_mode :: 'undefined' | 'poll' | 'eventsource',
	timeout = infinity :: timeout()
}).

%% HTTP.

init(Transport, Req, Opts) ->
	case cowboy_req:header(<<"upgrade">>, Req) of
		{undefined, Req2} ->
			{Method, Req3} = cowboy_req:method(Req2),
			init(Transport, Req3, Opts, Method);
		{Bin, Req2} when is_binary(Bin) ->
			case cowboy_bstr:to_lower(Bin) of
				<<"websocket">> ->
					{upgrade, protocol, cowboy_websocket};
				_Any ->
					{ok, Req3} = cowboy_req:reply(501, [], [], Req2),
					{shutdown, Req3, undefined}
			end
	end.

init(Transport, Req, Opts, <<"GET">>) ->
	{handler, Handler} = lists:keyfind(handler, 1, Opts),
	Timeout = get_value(timeout, Opts, 60000),
	State = #state{handler=Handler},
	{GetMode, Req2} = get_mode(Req),
	Active = case GetMode of
		poll -> once;
		eventsource -> true
	end,
	case Handler:init(Transport, Req2, Opts, Active) of
		{ok, Req3, HandlerState} ->
			{ok, Req4} = start_get_mode(GetMode, Req3),
			Req5 = cowboy_req:compact(Req4),
			{loop, Req5, State#state{handler_state=HandlerState,
				get_mode=GetMode}, Timeout, hibernate};
		{shutdown, Req3, HandlerState} ->
			{shutdown, Req3, State#state{handler_state=HandlerState}}
	end;
init(Transport, Req, Opts, <<"POST">>) ->
	{handler, Handler} = lists:keyfind(handler, 1, Opts),
	State = #state{handler=Handler},
	case Handler:init(Transport, Req, Opts, false) of
		{ok, Req2, HandlerState} ->
			{ok, Req2, State#state{handler_state=HandlerState}};
		{shutdown, Req2, HandlerState} ->
			{shutdown, Req2, State#state{handler_state=HandlerState}}
	end;
init(_Transport, Req, _Opts, _Method) ->
	{ok, Req2} = cowboy_req:reply(405, [], [], Req),
	{shutdown, Req2, undefined}.

handle(Req, State) ->
	{Method, Req2} = cowboy_req:method(Req),
	handle(Req2, State, Method).

handle(Req, State=#state{handler=Handler, handler_state=HandlerState},
		<<"POST">>) ->
	case cowboy_req:body(Req) of
		{ok, Data, Req2} ->
			case Handler:stream(Data, Req2, HandlerState) of
				{ok, Req3, HandlerState2} ->
					{ok, Req3, State#state{handler_state=HandlerState2}};
				{reply, Reply, Req3, HandlerState2} ->
					{ok, Req4} = cowboy_req:reply(200, [], Reply, Req3),
					{ok, Req4, State#state{handler_state=HandlerState2}}
			end;
		{error, _} ->
			%% An error occurred, stop there.
			{ok, Req, State}
	end.

info(Message, Req,
		State=#state{get_mode=GetMode, handler=Handler,
		handler_state=HandlerState}) ->
	case Handler:info(Message, Req, HandlerState) of
		{ok, Req2, HandlerState2} ->
			{loop, Req2, State#state{handler_state=HandlerState2}, hibernate};
        {shutdown, Req2, HandlerState2} ->
			State2 = State#state{handler_state=HandlerState2},
            {ok, Req3} = shutdown_get_mode(GetMode, Req2),
            {ok, Req3, State2};
		{reply, Data, Req2, HandlerState2} ->
			State2 = State#state{handler_state=HandlerState2},
			case reply_get_mode(GetMode, Data, Req2) of
				{ok, Req3} ->
					{ok, Req3, State2};
				{loop, Req3} ->
					{loop, Req3, State2, hibernate}
			end
	end.

terminate(_Reason, _Req, undefined) ->
	ok;
terminate(_Reason, Req, #state{handler=Handler, handler_state=HandlerState}) ->
	Handler:terminate(Req, HandlerState).

%% Websocket.

websocket_init(Transport, Req, Opts) ->
	{handler, Handler} = lists:keyfind(handler, 1, Opts),
	Timeout = get_value(timeout, Opts, 60000),
	State = #state{handler=Handler},
	case Handler:init(Transport, Req, Opts, true) of
		{ok, Req2, HandlerState} ->
			Req3 = cowboy_req:compact(Req2),
			{ok, Req3, State#state{handler_state=HandlerState},
				Timeout, hibernate};
		{shutdown, Req2, _HandlerState} ->
			{shutdown, Req2}
	end.

websocket_handle({text, Data}, Req,
		State=#state{handler=Handler, handler_state=HandlerState}) ->
	case Handler:stream(Data, Req, HandlerState) of
		{ok, Req2, HandlerState2} ->
			{ok, Req2, State#state{handler_state=HandlerState2}, hibernate};
		{reply, Reply, Req2, HandlerState2} ->
			{reply, {text, Reply}, Req2,
				State#state{handler_state=HandlerState2}, hibernate};
		{shutdown, Req2, HandlerState2} ->
			{shutdown, Req2, State#state{handler_state=HandlerState2}}
	end;
websocket_handle(_Frame, Req, State) ->
	{ok, Req, State, hibernate}.

websocket_info(Info, Req, State=#state{
		handler=Handler, handler_state=HandlerState}) ->
	case Handler:info(Info, Req, HandlerState) of
		{ok, Req2, HandlerState2} ->
			{ok, Req2, State#state{handler_state=HandlerState2}, hibernate};
		{reply, Reply, Req2, HandlerState2} ->
			{reply, {text, Reply}, Req2,
				State#state{handler_state=HandlerState2}, hibernate};
		{shutdown, Req2, HandlerState2} ->
			{shutdown, Req2, State#state{handler_state=HandlerState2}}
	end.

websocket_terminate(_Reason, Req,
		#state{handler=Handler, handler_state=HandlerState}) ->
	Handler:terminate(Req, HandlerState).

%% Eventsource and poll utilities

get_mode(Req) ->
	case cowboy_req:parse_header(<<"accept">>, Req) of
		{ok, Accepts, Req2} ->
			get_mode(Accepts, Req2);
		_ ->
			{poll, Req}
	end.

get_mode([{{<<"text">>, <<"event-stream">>, _}, _, _}|_], Req) ->
	{eventsource, Req};
get_mode([_|Accepts], Req) ->
	get_mode(Accepts, Req);
get_mode([], Req) ->
	{poll, Req};
get_mode(undefined, Req) ->
	{poll, Req}.

start_get_mode(poll, Req) ->
	{ok, Req};
start_get_mode(eventsource, Req) ->
	Headers = [{<<"content-type">>, <<"text/event-stream">>}],
	{ok, _} = cowboy_req:chunked_reply(200, Headers, Req).

shutdown_get_mode(poll, Req) ->
	cowboy_req:reply(204, [], <<"">>, Req);
shutdown_get_mode(eventsource, Req) ->
    {ok, Req}.

reply_get_mode(poll, Data, Req) ->
	{ok, _} = cowboy_req:reply(200, [], Data, Req);
reply_get_mode(eventsource, Data, Req) ->
	Bin = iolist_to_binary(Data),
	Event = [[<<"data: ">>, Line, <<"\n">>] ||
		Line <- binary:split(Bin, [<<"\r\n">>, <<"\r">>, <<"\n">>], [global])],
	ok = cowboy_req:chunk([Event, <<"\n">>], Req),
	{loop, Req}.

%% Internal.

%% @doc Faster alternative to proplists:get_value/3.
%% @private
get_value(Key, Opts, Default) ->
	case lists:keyfind(Key, 1, Opts) of
		{_, Value} -> Value;
		_ -> Default
	end.