Permalink
Browse files

Add Cowboy Support (Thanks to @tuncer and @essen)

  • Loading branch information...
1 parent b5c7a1a commit 121ace453dd113968ead8387d97cb3d1acdbcbb5 @choptastic choptastic committed Jan 22, 2012
@@ -6,4 +6,5 @@
-record(cookie, { name, value, path="/", minutes_to_live=20 }).
-record(header, { name, value }).
-record(response, { statuscode=200, headers=[], cookies=[], data=[] }).
--record(uploaded_file, { original_name, temp_file, size }).
+-record(uploaded_file, { original_name, temp_file, size }).
+-record(request_cache, {request, docroot="", body=""}).
View
@@ -1,4 +1,10 @@
% -*- Erlang -*-
+%% vim: ts=4 sw=4 et ft=erlang
+
+{deps, [
+ {mimetypes, ".*", {git, "git://github.com/spawngrid/mimetypes.git", {tag, "HEAD"}}}
+]}.
+
{erl_opts, [fail_on_warning, debug_info]}.
{cover_enabled, true}.
{xref_checks, [undefined_function_calls]}.
@@ -0,0 +1,162 @@
+%% vim: ts=4 sw=4 et
+% Simple Bridge Cowboy
+% Copyright (c) 2012 Jesse Gumm
+% See MIT-LICENSE for licensing information.
+
+-module (cowboy_request_bridge).
+-behaviour (simple_bridge_request).
+-include_lib ("simple_bridge.hrl").
+
+-export ([
+ init/1,
+ request_method/1, path/1, uri/1,
+ peer_ip/1, peer_port/1,
+ headers/1, cookies/1,
+ query_params/1, post_params/1, request_body/1,
+ socket/1, recv_from_socket/3
+]).
+
+-define(GET,_RequestCache=#request_cache{request=Req}=cowboy_request_server:get(ReqKey)).
+-define(PUT,cowboy_request_server:set(ReqKey,NewRequestCache)).
+
+new_key() ->
+ {cowboy_bridge,now()}.
+
+init({Req,DocRoot}) ->
+ ReqKey = new_key(),
+ NewRequestCache = #request_cache{
+ body=not_loaded,
+ request=Req,
+ docroot=DocRoot
+ },
+ ?PUT,
+ ReqKey.
+
+request_method(ReqKey) ->
+ ?GET,
+ {Method, Req} = cowboy_http_req:method(Req),
+ Method.
+
+path(ReqKey) ->
+ ?GET,
+ {Path, Req} = cowboy_http_req:path(Req),
+ case Path of
+ [] -> "/";
+ _ -> b2l(filename:join(Path))
+ end.
+
+uri(ReqKey) ->
+ ?GET,
+ {RawPath, Req} = cowboy_http_request:raw_path(Req),
+ b2l(RawPath).
+
+peer_ip(ReqKey) ->
+ ?GET,
+ {{IP, _Port, Req}} = cowboy_http_req:peer(Req),
+ IP.
+
+peer_port(ReqKey) ->
+ ?GET,
+ {{_IP, Port, Req}} = cowboy_http_req:peer(Req),
+ Port.
+
+headers(ReqKey) ->
+ ?GET,
+ {Headers,Req} = cowboy_http_req:headers(Req),
+ [{simple_bridge_util:atomize_header(Header),b2l(Val)} || {Header,Val} <- Headers].
+
+
+cookies(ReqKey) ->
+ ?GET,
+ {Cookies, NewReq} = cowboy_http_req:cookies(Req),
+ NewRequestCache = _RequestCache#request_cache{request=NewReq},
+ ?PUT,
+ [{b2l(K),b2l(V)} || {K,V} <- Cookies].
+
+query_params(ReqKey) ->
+ ?GET,
+ {QsVals, NewReq} = cowboy_http_req:qs_vals(Req),
+ NewRequestCache = _RequestCache#request_cache{request=NewReq},
+ ?PUT,
+ [{b2l(K),b2l(V)} || {K,V} <- QsVals].
+
+
+
+post_params(ReqKey) ->
+ Body = request_body(ReqKey,binary),
+ BodyQs = parse_qs(Body),
+ [{b2l(K),b2l(V)} || {K,V} <- BodyQs].
+
+request_body(ReqKey) ->
+ request_body(ReqKey,string).
+
+request_body(ReqKey,binary) ->
+ ?GET,
+ %% We cache the body here because we can't request the body twice in cowboy or it'll crash
+ {Body,NewReq} = case _RequestCache#request_cache.body of
+ not_loaded ->
+ {ok, B, R} = cowboy_http_req:body(Req),
+ {B,R};
+ B -> {B,Req}
+ end,
+ NewRequestCache = _RequestCache#request_cache {
+ body=Body,
+ request=NewReq
+ },
+ ?PUT,
+ Body;
+request_body(ReqKey,string) ->
+ b2l(request_body(ReqKey,binary)).
+
+
+socket(ReqKey) ->
+ ?GET,
+ {ok, _Transport, Socket} = cowboy_http_req:transport(Req),
+ Socket.
+
+%% TODO: Cowboy's stream_body doesn't support customizable Length and Timeout
+recv_from_socket(_Length, _Timeout, ReqKey) ->
+ ?GET,
+ %cowboy_http_req:init_stream(
+ case cowboy_http_req:stream_body(Req) of
+ {ok, Data, NewReq} ->
+ NewRequestCache = _RequestCache#request_cache{request=NewReq},
+ ?PUT,
+ Data;
+ {done, NewReq} ->
+ NewRequestCache = _RequestCache#request_cache{request=NewReq},
+ ?PUT,
+ <<"">>;
+ {error, Reason} ->
+ exit({error, Reason}) %% exit(normal) instead?
+ end.
+
+
+
+%% parse_qs, borrowed from Cowboy by Loic Hugian :)
+parse_qs(<<>>) ->
+ [];
+parse_qs(Qs) ->
+ URLDecode = fun cowboy_http:urldecode/1,
+ Tokens = binary:split(Qs, <<"&">>, [global, trim]),
+ [case binary:split(Token, <<"=">>) of
+ [Token] -> {URLDecode(Token), true};
+ [Name, Value] -> {URLDecode(Name), URLDecode(Value)}
+ end || Token <- Tokens].
+
+
+b2l(B) when is_binary(B) ->
+ binary_to_list(B);
+b2l(B) ->
+ B.
+
+l2b(L) when is_list(L) ->
+ list_to_binary(L);
+l2b(L) ->
+ L.
+
+b2a(B) when is_binary(B) ->
+ list_to_atom(binary_to_list(B)).
+
+
+
@@ -0,0 +1,79 @@
+%% vim: ts=4 sw=4 et
+%% Cowboy Request Server. Becaue cowboy returns Request object for each
+%% request, this helps to manage that object so it can be pushed back onto
+%% the pile after it's done being used.
+%%
+%% Right now, it's nasty and uses the process dict. Not exactly the smoothest approach.
+%%
+%% I keep it at set/2 and get/1 as I'd like to eventually update it to be a server that uses a dict
+%% but the concerns are for memory usage for expired requests. We can at least
+%% Clean up our requests when complete, with a delete(Key), but I'd like to also
+%% make sure that the handler cleans itself up if the request crashes, say if
+%% nitrogen crashes while doing whatever.
+%%
+%% Also, while using the process dict, the key will always be 'proc_dict_cowboy_request'
+
+-module(cowboy_request_server).
+-include_lib("simple_bridge.hrl").
+
+-export([set/2,get/1]).
+
+%-behaviour(gen_server).
+-compile({no_auto_import,[get/1]}).
+
+set(Key,RequestCache) ->
+ %error_logger:info_msg("Saving ~p~n",[Req]),
+ erlang:put(Key,RequestCache).
+
+get(Key) ->
+ %error_logger:info_msg("Getting ~p~n",[Key]),
+ _RequestCache = erlang:get(Key).
+
+
+%% -define(M,?MODULE).
+%%
+%% -export([start/0,start_link/0,set/2,get/1,delete/1]).
+%% -export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).
+%%
+%% start_link() ->
+%% gen_server:start_link({local, ?M}, ?M, [], []).
+%%
+%% start() ->
+%% gen_server:start({local, ?M}, ?M, [], []).
+%%
+%% %% Public Functions
+%% init(_) ->
+%% {ok, dict:new()}.
+%%
+%% set(Key, Req) ->
+%% ok = gen_server:call(?M,{set,Key,Req}).
+%%
+%% get(Key) ->
+%% {ok, Req} = gen_server:call(?M,{get,Key}),
+%% Req.
+%%
+%% delete(Key) ->
+%% ok = gen_server:call(?M,{delete,Key}).
+%%
+%% %% Private functions
+%% handle_call({set,Key,Req},_From,Dict) ->
+%% NewDict = dict:store(Key,Req,Dict),
+%% {reply,ok,NewDict};
+%% handle_call({get,Key},_From,Dict) ->
+%% Reply = dict:find(Key,Dict),
+%% {reply,Reply,Dict};
+%% handle_call({delete,Key},_From,Dict) ->
+%% NewDict = dict:erase(Key,Dict),
+%% {reply,ok,NewDict}.
+%%
+%% handle_info(_Info,State) ->
+%% {noreply,State}.
+%%
+%% handle_cast(_,State) ->
+%% {noreply,State}.
+%%
+%% terminate(_Reason,_State) ->
+%% ok.
+%%
+%% code_change(_OldVsn,State,_Extra) ->
+%% {ok,State}.
@@ -0,0 +1,110 @@
+%% vim: ts=4 sw=4 et
+% Simple Bridge
+% Copyright (c) 2008-2012 Rusty Klophaus
+% See MIT-LICENSE for licensing information.
+
+-module (cowboy_response_bridge).
+-behaviour (simple_bridge_response).
+-include_lib ("simple_bridge.hrl").
+-export ([build_response/2,init/1]).
+
+
+init(Request) when
+ is_tuple(Request),
+ element(1,Request)==simple_bridge_request_wrapper,
+ element(2,Request)==cowboy_request_bridge ->
+ element(3,Request); %% The third element of Request is the RequestKey from response_bridge
+init({Req,DocRoot}) ->
+ %% Since cowboy request and response information are the same, this synchronizes it
+ cowboy_request_bridge:init({Req,DocRoot}).
+
+build_response(ReqKey, Res) ->
+ RequestCache = #request_cache{request=Req,docroot=DocRoot} = cowboy_request_server:get(ReqKey),
+ % Some values...
+ Code = Res#response.statuscode,
+
+ case Res#response.data of
+ {data, Body} ->
+
+ % Assemble headers...
+ Headers = lists:flatten([
+ [{X#header.name, X#header.value} || X <- Res#response.headers]
+ ]),
+
+ % Ensure content type...
+ F = fun(Key) -> lists:keymember(Key, 1, Headers) end,
+ HasContentType = lists:any(F, ["content-type", "Content-Type", "CONTENT-TYPE"]),
+ Headers2 = case HasContentType of
+ true -> Headers;
+ false -> [{"Content-Type", "text/html"}|Headers]
+ end,
+
+ % Send the cowboy cookies
+ {ok, FinReq} = send(Code,Headers2,Res#response.cookies,Body,Req),
+
+ NewRequestCache = RequestCache#request_cache{
+ request=FinReq
+ },
+ cowboy_request_server:set(ReqKey,NewRequestCache),
+ {ok,FinReq};
+
+ {file, Path} ->
+ %% Calculate expire date far into future...
+ Seconds = calendar:datetime_to_gregorian_seconds(calendar:local_time()),
+ TenYears = 10 * 365 * 24 * 60 * 60,
+ Seconds1 = calendar:gregorian_seconds_to_datetime(Seconds + TenYears),
+ ExpireDate = httpd_util:rfc1123_date(Seconds1),
+
+ [$. | Ext] = filename:extension(Path),
+ %% We can use the mimetypes module because it's a dependency for cowboy
+ Mimetype = mimetypes:extension(Ext),
+
+ %% Create the response telling Mochiweb to serve the file...
+ Headers = [
+ {"Expires", ExpireDate},
+ {"Content-Type",Mimetype}
+ ],
+
+ io:format("Serving static file ~p~n",[Path]),
+
+ FullPath = filename:join(DocRoot,Path),
+ {ok, FinReq} = case file:read_file(FullPath) of
+ {error,enoent} ->
+ {ok, _R} = send(404,[],[],"Not Found",Req);
+ {ok,Bin} ->
+ {ok, _R} = send(200,Headers,[],Bin,Req)
+ end,
+
+ NewRequestCache = RequestCache#request_cache{
+ request=FinReq
+ },
+ cowboy_request_server:set(ReqKey,NewRequestCache),
+ {ok, FinReq}
+ end.
+
+send(Code,Headers,Cookies,Body,Req) ->
+ Req1 = prepare_cookies(Req,Cookies),
+ Req2 = prepare_headers(Req1,Headers),
+ {ok, Req3} = cowboy_http_req:set_resp_body(Body,Req2),
+ {ok, _ReqFinal} = cowboy_http_req:reply(Code, Req3).
+
+prepare_cookies(Req,Cookies) ->
+ lists:foldl(fun(C,R) ->
+ Name = iol2b(C#cookie.name),
+ Value = iol2b(C#cookie.value),
+ Path = iol2b(C#cookie.path),
+ SecsToLive = C#cookie.minutes_to_live * 60,
+ Options = [{path,Path},{max_age,SecsToLive}],
+ {ok,NewReq} = cowboy_http_req:set_resp_cookie(Name,Value,Options,R),
+ NewReq
+ end,Req,Cookies).
+
+prepare_headers(Req,Headers) ->
+ lists:foldl(fun({Header,Value},R) ->
+ {ok,NewReq} = cowboy_http_req:set_resp_header(iol2b(Header),iol2b(Value),R),
+ NewReq
+ end,Req,Headers).
+
+
+iol2b(V) when is_binary(V) -> V;
+iol2b(V) -> iolist_to_binary(V).
@@ -5,7 +5,10 @@
-module (inets_response_bridge).
-behaviour (simple_bridge_response).
-include_lib ("simple_bridge.hrl").
--export ([build_response/2]).
+-export ([build_response/2,init/1]).
+
+init(Req) ->
+ Req.
build_response(Req, Res) ->
ResponseCode = Res#response.statuscode,
@@ -5,7 +5,10 @@
-module (mochiweb_response_bridge).
-behaviour (simple_bridge_response).
-include_lib ("simple_bridge.hrl").
--export ([build_response/2]).
+-export ([build_response/2,init/1]).
+
+init({Req,DocRoot}) ->
+ {Req,DocRoot}.
build_response({Req, DocRoot}, Res) ->
% Some values...
Oops, something went wrong.

0 comments on commit 121ace4

Please sign in to comment.