diff --git a/src/cowboy_bridge_modules/cowboy_response_bridge.erl b/src/cowboy_bridge_modules/cowboy_response_bridge.erl index 2e7fad5..c464ed7 100644 --- a/src/cowboy_bridge_modules/cowboy_response_bridge.erl +++ b/src/cowboy_bridge_modules/cowboy_response_bridge.erl @@ -22,19 +22,13 @@ build_response(ReqKey, Res) -> % Some values... Code = Res#response.statuscode, + %% assemble headers... + Headers = lists:flatten([[{X#header.name, X#header.value} || X <- Res#response.headers]]), + 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, + Headers2 = simple_bridge_util:ensure_header(Headers,"Content-Type","text/html"), % Send the cowboy cookies {ok, FinReq} = send(Code, Headers2, Res#response.cookies, Body, Req), @@ -53,34 +47,42 @@ build_response(ReqKey, Res) -> %% 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 + + throw(wip) +%% 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(Headers), +%% +%% 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, +%% cowboy_request_server:set(ReqKey, RequestCache#request_cache{request = FinReq}), +%% generate_static_error(P), +%% {ok, FinReq} + end. - %% % Cowboy path starts with / so we need to remove it - %% Path = strip_leading_slash(P), - %% ExpireDate = simple_bridge_util:expires(years, 10), - %% [$. | Ext] = filename:extension(Path), - %% Mimetype = mimetypes:extension(Ext), - %% Headers = [{"Expires", ExpireDate}, {"Content-Type",Mimetype}], - - %% io:format("Serving static file ~p from docroot of ~p ~n",[Path, DocRoot]), - - %% 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, - %% cowboy_request_server:set(ReqKey, RequestCache#request_cache{request = FinReq}), - %% {ok, FinReq} - throw({unrouted_static_file, [ - {requested_file, P}, - {description, "Simple Bridge through Cowboy is not set up to handle static files. Static Files should be handled by Cowboy through the routing table."}, - {see_also, [ - "https://github.com/nitrogen/nitrogen/blob/master/rel/overlay/cowboy/site/src/nitrogen_sup.erl", - "https://github.com/nitrogen/nitrogen/blob/master/rel/overlay/cowboy/etc/cowboy.config" - ]} +generate_static_error(P) -> + error_logger:warning_msg("~p",[ + {unrouted_static_file, [ + {requested_file, P}, + {description, "Simple Bridge through Cowboy is not set up to handle static files. Static Files should be handled by Cowboy through the routing table."}, + {see_also, [ + "https://github.com/nitrogen/nitrogen/blob/master/rel/overlay/cowboy/site/src/nitrogen_sup.erl", + "https://github.com/nitrogen/nitrogen/blob/master/rel/overlay/cowboy/etc/cowboy.config" + ]} + ]} + ]). - ]}) - end. +get_mimetype(Path) -> + [$. | Ext] = filename:extension(Path), + mimetypes:extension(Ext). %% %% Just to strip leading slash, as cowboy tends to do this. %% %% If no leading slash, just return the path. diff --git a/src/inets_bridge_modules/inets_response_bridge.erl b/src/inets_bridge_modules/inets_response_bridge.erl index 77d6910..8d4aa20 100644 --- a/src/inets_bridge_modules/inets_response_bridge.erl +++ b/src/inets_bridge_modules/inets_response_bridge.erl @@ -49,6 +49,7 @@ to_cookie_expire(SecondsToLive) -> %% Inets wants some headers as lowercase atoms, so we %% need to do some special massage here. +%% TODO: This needs to be reworked. We shouldn't be making atoms from every request massage(Header) -> X = list_to_atom( binary_to_list( diff --git a/src/mochiweb_bridge_modules/mochiweb_response_bridge.erl b/src/mochiweb_bridge_modules/mochiweb_response_bridge.erl index ba3f225..f094aaf 100644 --- a/src/mochiweb_bridge_modules/mochiweb_response_bridge.erl +++ b/src/mochiweb_bridge_modules/mochiweb_response_bridge.erl @@ -19,27 +19,21 @@ build_response({Req, DocRoot}, Res) -> Headers = lists:flatten([ [{X#header.name, X#header.value} || X <- Res#response.headers], [create_cookie_header(X) || X <- Res#response.cookies] - ]), - - %% 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, + ]), case Res#response.data of {data, Body} -> % Send the mochiweb response... + Headers2 = simple_bridge_util:ensure_header(Headers,{"Content-Type","text/html"}), Req:respond({Code, Headers2, Body}); {file, Path} -> %% Create the response telling Mochiweb to serve the file... + Headers2 = simple_bridge_util:ensure_expires_header(Headers), Req:serve_file(tl(Path), DocRoot, Headers2) end. + + create_cookie_header(Cookie) -> SecondsToLive = Cookie#cookie.minutes_to_live * 60, Name = Cookie#cookie.name, diff --git a/src/simple_bridge_util.erl b/src/simple_bridge_util.erl index 903de24..18c0793 100644 --- a/src/simple_bridge_util.erl +++ b/src/simple_bridge_util.erl @@ -1,14 +1,26 @@ % vim: ts=4 sw=4 et -module(simple_bridge_util). -export([ - atomize_header/1, - expires/2, - b2l/1 - ]). + atomize_header/1, + expires/2, + b2l/1, + has_header/2, + has_any_header/2, + ensure_header/3, + ensure_header/2, + ensure_headers/2, + default_static_expires_header/0, + ensure_expires_header/1, + needs_expires_header/1 +]). +-type header_key() :: string() | binary(). +-type header() :: {header_key(), string()}. +-type header_list() :: [header()]. %% converts a Header to a lower-case, underscored version %% ie. "X-Forwarded-For" -> x_forwarded_for + atomize_header(Header) when is_binary(Header) -> atomize_header(binary_to_list(Header)); atomize_header(Header) when is_atom(Header) -> @@ -25,14 +37,116 @@ atomize_header(Header) when is_list(Header) -> end, list_to_atom(lists:map(LowerUnderscore,Header)). -%% TODO: Make this flexibile beyond just years -expires(years, Years) when is_integer(Years) -> - %% Calculate expire date far into future... - %% This method copied from Evan Miller's implementation - {{Y, _, _}, _} = calendar:local_time(), - ExpireDate = httpd_util:rfc1123_date(), - _FinalExpiresDate = re:replace(ExpireDate, " \\d\\d\\d\\d ", io_lib:format(" ~4.4.0w ", [Y + Years])). +%% Checks if `Header` exists as a key in `HeaderList` +%% if it doesn't, inserts it with the value `Value` +-spec ensure_header(header_list(), {header_key(), term()}) -> header_list(). +ensure_header(HeaderList,{Header,Value}) -> + ensure_header(HeaderList,Header,Value). + +-spec ensure_header(header_list(), header_key(), term()) -> header_list(). +ensure_header(HeaderList,Header,Value) -> + case has_header(HeaderList,Header) of + true -> HeaderList; + false -> [{Header,Value} | HeaderList] + end. + +-spec ensure_headers(header_list(), header_list()) -> header_list(). +ensure_headers(HeaderList,HeadersToEnsure) -> + LowerList = lower_keys(HeaderList), + lists:foldl(fun({Header,Value},NewHeaderList) -> + case has_lower_header(LowerList, Header) of + true -> NewHeaderList; + false -> [{Header,Value} | NewHeaderList] + end + end,HeaderList,HeadersToEnsure). + +-spec ensure_expires_header(header_list()) -> header_list(). +ensure_expires_header(HeaderList) -> + case needs_expires_header(HeaderList) of + true -> + Expires = default_static_expires_header(), + [Expires | HeaderList]; + false -> + HeaderList + end. + +-spec needs_expires_header(header_list()) -> boolean(). +needs_expires_header(HeaderList) -> + not(has_any_header(HeaderList,["Expires","Cache-Control"])). + +-spec has_header(header_list(), header_key()) -> boolean(). +has_header(HeaderList,Header) -> + LowerKeys = lower_keys(HeaderList), + has_lower_header(LowerKeys, Header). + +-spec has_any_header(header_list(), [header_key()]) -> boolean(). +has_any_header(HeaderList,HeadersToCheck) -> + LowerKeys = lower_keys(HeaderList), + lists:any(fun(Key) -> has_lower_header(LowerKeys,Key) end,HeadersToCheck). + +-spec has_lower_header([string()], header_key()) -> boolean(). +has_lower_header(HeaderLowerKeyList, Header) -> + HeaderLower = to_lower(Header), + lists:member(HeaderLower, HeaderLowerKeyList). + +-spec lower_keys(header_list()) -> [string()]. +lower_keys(HeaderList) -> + [to_lower(Key) || {Key,_} <- HeaderList]. + +-spec to_lower(header_key()) -> string(). +to_lower(Header) when is_binary(Header) -> + to_lower(b2l(Header)); +to_lower(Header) when is_list(Header) -> + string:to_lower(Header). + +-spec default_static_expires_header() -> header(). +default_static_expires_header() -> + case application:get_env(simple_bridge,default_expires) of + {ok, immediate} -> + {"Cache-control","no-cache"}; + + {ok, Seconds} when is_integer(Seconds) -> + Expires = expires(seconds,Seconds), + {"Expires", Expires}; + + {ok, {Unit, Value}} when Unit==years orelse + Unit==months orelse + Unit==weeks orelse + Unit==days orelse + Unit==hours orelse + Unit==minutes orelse + Unit==seconds -> + Expires = expires(Unit,Value), + {"Expires", Expires}; + _ -> + Expires = expires(years,10), + {"Expires", Expires} + end. + +-type unit_of_time() :: years|months|weeks|days|hours|minuites|seconds. +-spec expires(unit_of_time(), integer()) -> string(). +expires(years,X) when is_integer(X) -> + make_expires_from_seconds(X*31536000); +expires(months,X) when is_integer(X) -> + make_expires_from_seconds(X*2592000); +expires(weeks,X) when is_integer(X) -> + make_expires_from_seconds(X*604800); +expires(days,X) when is_integer(X) -> + make_expires_from_seconds(X*86400); +expires(hours,X) when is_integer(X) -> + make_expires_from_seconds(X*3600); +expires(minutes,X) when is_integer(X) -> + make_expires_from_seconds(X*60); +expires(seconds,X) when is_integer(X) -> + make_expires_from_seconds(X). + +-spec make_expires_from_seconds(integer()) -> string(). +make_expires_from_seconds(Seconds) -> + {NowMegaSec,NowSec,_} = now(), + ExpiresDate = calendar:now_to_datetime({NowMegaSec,NowSec+Seconds,0}), + httpd_util:rfc1123_date(ExpiresDate). +-spec b2l(binary() | string()) -> string(). b2l(B) when is_binary(B) -> binary_to_list(B); b2l(B) -> B. diff --git a/src/yaws_bridge_modules/yaws_response_bridge.erl b/src/yaws_bridge_modules/yaws_response_bridge.erl index f22a13a..dd9605b 100644 --- a/src/yaws_bridge_modules/yaws_response_bridge.erl +++ b/src/yaws_bridge_modules/yaws_response_bridge.erl @@ -44,7 +44,8 @@ build_response(_Arg, Res) -> %% completely back to Yaws, or 2) how the streamcontent return types work as define in %% yaws_server:handle_out_reply - ExpireDate = simple_bridge_util:expires(years, 10), + %% Static Content should have an expires date. If not, we're going to make one + Headers2 = ensure_expires_header(Res, Headers), %% Docroot needed to find file in Path Docroot = yaws_api:arg_docroot(_Arg), @@ -52,7 +53,7 @@ build_response(_Arg, Res) -> %% Get the content type as defined by yaws ContentType = yaws_api:mime_type(Path), - + %% Get the file content FullResponse = case file:read_file(FullPath) of {error,enoent} -> @@ -60,7 +61,7 @@ build_response(_Arg, Res) -> {ok,Bin} -> [ {status, Code}, - [{header, {"Expires", ExpireDate}} | Headers], + Headers2, {content, ContentType, Bin} ] end, @@ -72,7 +73,18 @@ assemble_headers(Res) -> [{header, {X#header.name, X#header.value}} || X <- Res#response.headers], [create_cookie(X) || X <- Res#response.cookies] ]). + +%% This is slightly different from the one in simple_bridge_util due to the +%% formatting of the yaws headers isn't just a simple proplist. +ensure_expires_header(Res,Headers) -> + case simple_bridge_util:needs_expires_header(Res#response.headers) of + true -> + Expires = simple_bridge_util:default_static_expires_header(), + [{header, Expires} | Headers]; + false -> Headers + end. + get_content_type(Res) -> coalesce([ kvl3(content_type, Res#response.headers),