src/cowboy_bridge_modules/cowboy_simple_bridge.erl

%% vim: ts=4 sw=4 et
% Simple Bridge Cowboy
% Copyright (c) 2012 Jesse Gumm
% See MIT-LICENSE for licensing information.

-module (cowboy_simple_bridge).
-behaviour (simple_bridge).
-include("simple_bridge.hrl").

%% REQUEST EXPORTS

-export([
        init/1,
        protocol/1,
        host/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,
        protocol_version/1,
        native_header_type/0
    ]).

%% RESPONSE EXPORTS
-export([
        build_response/2
    ]).

%% HELPER STREAMING EXPORTS
-export([
	 stream_body/1
	]).

native_header_type() ->
    map.

new_key() ->
    {cowboy_bridge, erlang:make_ref()}.

get_key(ReqKey) ->
    try
        RequestCache = #request_cache{request = Req} = cowboy_request_server:get(ReqKey),
        {RequestCache, Req}
    catch E:T:S  ->
        error_logger:info_msg("~p:~p~n~p", [E, T, S])
    end.

put_key(ReqKey, NewRequestCache) ->
    cowboy_request_server:set(ReqKey, NewRequestCache).


init({Req, DocRoot}) ->
    ReqKey = new_key(),
    put_key(ReqKey, #request_cache{body = not_loaded, docroot=DocRoot, request = Req}),
    ReqKey.

protocol(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    case cowboy_req:scheme(Req) of
        <<"http">> -> http;
        <<"https">> -> https
    end.

host(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    % @TODO: Could x-forwarded-for be the better value here if it exists?
    Host = cowboy_req:host(Req),
    simple_bridge_util:to_list(Host).

request_method(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    Method = cowboy_req:method(Req),
    list_to_atom(simple_bridge_util:to_list(Method)).

path(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    Path = cowboy_req:path(Req),
    simple_bridge_util:to_list(Path).

uri(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    URL = cowboy_req:uri(Req),
    case re:run(URL, "^https?://[^/]*(/.*)$", [{capture, all_but_first, list}]) of
	{match, [Uri]} -> Uri;
	_ -> ""
    end.

peer_ip(ReqKey) ->
    {RequestCache, Req} = get_key(ReqKey),
    {IP, _Port} = cowboy_req:peer(Req),
    put_key(ReqKey, RequestCache#request_cache{request = Req}),
    IP.

peer_port(ReqKey) ->
    {RequestCache, Req} = get_key(ReqKey),
    {_IP, Port} = cowboy_req:peer(Req),
    put_key(ReqKey, RequestCache#request_cache{request = Req}),
    Port.

headers(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    Headers = cowboy_req:headers(Req),
    % When running in TLS mode cowboy doesn't provide a Host
    % header in the request's headers, but rather it provides
    % a `host` param in the request itself.
    case Headers of
        #{<<"host">> := _}      -> Headers;
        #{}                     -> maps:put(<<"host">>, cowboy_req:host(Req), Headers);
        _ when is_list(Headers) -> Headers ++ [{<<"host">>, cowboy_req:host(Req)}]
    end.

cookies(ReqKey) ->
    {RequestCache, Req} = get_key(ReqKey),
    Cookies = cowboy_req:parse_cookies(Req),
    put_key(ReqKey, RequestCache#request_cache{request = Req}),
    Cookies.

query_params(ReqKey) ->
    {RequestCache, Req} = get_key(ReqKey),
    QsVals = cowboy_req:parse_qs(Req),
    put_key(ReqKey, RequestCache#request_cache{request = Req}),
    QsVals.

post_params(ReqKey) ->
    Body = request_body(ReqKey),
    cow_qs:parse_qs(Body).

request_body(ReqKey) ->
    {RequestCache, Req} = get_key(ReqKey),
    {Body, NewReq} = case RequestCache#request_cache.body of
        not_loaded ->
            %% We start with 2MB here, as headers and form fields will almost
            %% certainly be in the first 2mb of a request, and give the client
            %% 120 seconds to send the chunk.
            %% TODO, Make the period a configuration option for simple_bridge
            case cowboy_req:read_body(Req,#{length => 2000000,period =>120000}) of 
                {ok, B, R} -> {B, R};
                {more, B, R} -> {B, R}
            end;
        B ->
            {B, Req}
    end,
    put_key(ReqKey, RequestCache#request_cache{body = Body, request = NewReq}),
    Body.

socket(_ReqKey) ->
    undefined.

recv_from_socket(_Length, _Timeout, ReqKey) ->
    {RequestCache, Req} = get_key(ReqKey),
    case cowboy_req:read_body(Req,#{length => 8000000}) of
        {ok, Data, NewReq} ->
            put_key(ReqKey, RequestCache#request_cache{request = NewReq}),
            Data;
        {more, Data, NewReq} ->
            put_key(ReqKey, RequestCache#request_cache{request = NewReq}),
            Data;
        {error, Reason} ->
            exit({error, Reason}) %% exit(normal) instead?
    end.


protocol_version(ReqKey) ->
    {_RequestCache, Req} = get_key(ReqKey),
    Version = cowboy_req:version(Req),
    case Version of
        'HTTP/1.1' -> {1, 1};
        'HTTP/1.0' -> {1, 0};
        {H, L} -> {H, L}
    end.

%% RESPONSE

build_response(ReqKey, Res) ->
    RequestCache = #request_cache{request = Req, docroot=DocRoot} = cowboy_request_server:get(ReqKey),
    % Some values...
    Code = Res#response.status_code,

    %% assemble headers...
    Headers = lists:flatten([[{X#header.name, X#header.value} || X <- Res#response.headers]]),

    case Res#response.data of
        {data, Body} ->
            % Assemble headers...
            Headers2 = simple_bridge_util:ensure_header(Headers,<<"content-type">>,<<"text/html">>),

            % Send the cowboy cookies
            FinReq = send(Code, Headers2, Res#response.cookies, Body, Req),
            cowboy_request_server:set(ReqKey, RequestCache#request_cache{request = FinReq}),
            {ok, FinReq};

        {file, P} ->
            %% Note: that this entire {file, Path} section should be avoided
            %% as much as possible. The cowboy static handler is designed for
            %% this task, but this is put here to help the programmer in the
            %% case of a misconfiguration.
            %% 
            %% You want to make sure that cowboy.config is properly set
            %% up with paths so that the requests for static files are
            %% properly handled by cowboy directly.
            %% 
            %% See https://github.com/nitrogen/nitrogen/blob/master/rel/overlay/cowboy/etc/cowboy.config
            %% and
            %% https://github.com/nitrogen/nitrogen/blob/master/rel/overlay/cowboy/site/src/nitrogen_sup.erl
 
            Path = strip_leading_slash(P),
            Mimetype = get_mimetype(Path),
            Headers2 = simple_bridge_util:ensure_header(Headers,{<<"content-type">>, Mimetype}),
            Headers3 = simple_bridge_util:ensure_expires_header(Headers2),
            FullPath = filename:join(DocRoot, Path),
            FinReq = case filelib:is_regular(FullPath) of
			 false ->
			     send(404, [], [], "Not Found", Req);
			 true -> 
			     Body = stream_body(FullPath),
			     send(200, Headers3, [], Body, Req)
		     end,
            cowboy_request_server:set(ReqKey, RequestCache#request_cache{request = FinReq}),
            {ok, FinReq}
    end.

stream_body(FullPath) ->
    Size = filelib:file_size(FullPath),
    {sendfile, 0, Size, FullPath}.

get_mimetype(Path) ->
    {Mime1, Mime2, _} = cow_mimetypes:all(list_to_binary(Path)),
    binary_to_list(Mime1) ++ "/" ++ binary_to_list(Mime2).

%% Just to strip leading slash, as cowboy tends to do this.
%% If no leading slash, just return the path.
strip_leading_slash([$/ | Path]) -> Path;
strip_leading_slash(Path) -> Path.

send(Code, Headers, Cookies, Body, Req) ->
    Req1 = prepare_cookies(Req, Cookies),
    Req2 = prepare_headers(Req1, Headers),
    Req3 = case Body of
	       {sendfile, _, _Size, _FullPath} -> %%{stream, _Size, Fun} -> 
		   cowboy_req:set_resp_body(Body, Req2);
	       {stream, Fun} ->
		   cowboy_req:set_resp_body(Fun, Req2);
	       {chunked, Fun} ->
		   cowboy_req:set_resp_body(Fun, Req2);
	       _ ->
		   cowboy_req:set_resp_body(Body, Req2)
	   end,
    cowboy_req:reply(Code, Req3).


prepare_cookies(Req, Cookies) ->
    lists:foldl(fun(C, R) ->
			%% In case cookie name or value was set to an atom, we need to make
			%% sure it's something usable, so let's just use binary
			Name = simple_bridge_util:to_binary(C#cookie.name),
			Value = simple_bridge_util:to_binary(C#cookie.value),
			Options = #{
			  domain => C#cookie.domain,
			  path => C#cookie.path,
			  max_age => C#cookie.max_age,
			  secure => C#cookie.secure,
			  http_only => C#cookie.http_only,
              same_site => simple_bridge_util:to_existing_atom(C#cookie.same_site)
			 },
			%% cowlib 1.0.0 (which is the dependency for cowboy 1.0.4) has a bug
			%% that freaks out with {secure, false} or {http_only, false} so this
			%% is a workaround.

			Pred = fun(K,V) ->
				       case K of
					   secure when V  =/= true -> false;
					   http_only when V =/= true -> false;
					   _ when V==undefined -> false;
					   _ -> true
				       end
			       end,
			FilteredOptions = ?MAPS_FILTER(Pred,Options),
			cowboy_req:set_resp_cookie(Name, Value, R, FilteredOptions)
		end, Req, Cookies).


prepare_headers(Req, Headers) ->
    lists:foldl(fun({Header, Value}, R) -> cowboy_req:set_resp_header(Header, Value, R) end, Req, Headers).