Permalink
Browse files

add control for "Expires" and "Cache-Control" response headers (capflam)

Allow an expires list in the server configuration and add "Expires"
and "Cache-Control" headers in responses if the mime type of the
document matches an element of this list. Like the Apache mod_expires
module, the "expires" configuration directive controls the setting of
the Expires HTTP header and the max-age directive of the Cache-Control
HTTP header in server responses. The expiration date can be relative
to either the time the source file was last modified or to the time of
the client access. It is possible to have multiple expires directives
for a virtual server.

Here is an example:

  <server www.yakaz.com >
    ...
    expires = <image/gif, access+2592000> <image/png, access+2592000>
    expires = <image/jpeg, access+2592000> <text/css, access+2592000>
    expires = <application/javascript, modify+2592000>
    ...
  </server>
  • Loading branch information...
1 parent 2b603fc commit c60fc5fcdd7d50201a9e5c5bafa3d7193a75c9fa @capflam capflam committed with vinoski May 3, 2011
Showing with 138 additions and 1 deletion.
  1. +22 −0 doc/yaws.tex
  2. +2 −0 include/yaws.hrl
  3. +20 −0 man/yaws.conf.5
  4. +61 −0 src/yaws.erl
  5. +33 −1 src/yaws_config.erl
View
@@ -2347,6 +2347,28 @@ \section{Server Part}
The function can and must do the same things
that a normal \verb+out/1+ does.
+\item \verb+expires = ListOfExpires+ -
+ Controls the setting of the \verb+Expires+ HTTP header and the
+ \verb+max-age+ directive of the \verb+Cache-Control+ HTTP header
+ in server responses for specific mime types. The expiration date
+ can set to be relative to either the time the source file was last
+ modified, or to the time of the client
+ access. \verb+ListOfExpires+ is defined as follows:
+\begin{verbatim}
+ expires = <MimeType1, access+Seconds> <MimeType2, modify+Seconds> ...
+\end{verbatim}
+ These HTTP headers are an instruction to the client about the
+ document's validity and persistence. If cached, the document may
+ be fetched from the cache rather than from the source until this
+ time has passed. After that, the cache copy is considered
+ "expired" and invalid, and a new copy must be obtained from the
+ source.
+ Here is an example:
+\begin{verbatim}
+ expires = <image/gif, access+2592000> <image/png, access+2592000>
+ expires = <image/jpeg, access+2592000> <text/css, access+2592000>
+\end{verbatim}
+
\item \verb+errormod_crash = Module+ -
It is possible to set a special module that
handles the HTML generation of server crash
View
@@ -216,6 +216,7 @@
%% 2 . A two tuple {Path, Mod}
%% 3 A three tuple {Path, Mod, [ExcludeDir ....]}
+ expires = [],
errormod_401 = yaws_outmod, %% the default 401 error module
errormod_404 = yaws_outmod, %% the default 404 error module
errormod_crash = yaws_outmod, %% use the same module for crashes
@@ -296,6 +297,7 @@
server,
location,
cache_control,
+ expires,
date,
allow,
last_modified,
View
@@ -525,6 +525,26 @@ The function must return, \fI{content,MimeType,Cont}\fR or
\fI{html, Str}\fR or \fI{ehtml, Term}\fR. That data will be shipped
to the client.
+.TP
+\fBexpires = ListOfExpires\fR
+Controls the setting of the \fIExpires\fR HTTP header and the \fImax-age\fR
+directive of the \fICache-Control\fR HTTP header in server responses for
+specific mime types. The expiration date can set to be relative to either the
+time the source file was last modified, or to the time of the client
+access. ListOfExpires is defined as follows:
+
+ expires = <MimeType1, access+Seconds> <MimeType2, modify+Seconds> ...
+
+These HTTP headers are an instruction to the client about the document's
+validity and persistence. If cached, the document may be fetched from the cache
+rather than from the source until this time has passed. After that, the cache
+copy is considered "expired" and invalid, and a new copy must be obtained from
+the source.
+Here is an example:
+
+ expires = <image/gif, access+2592000> <image/png, access+2592000>
+ expires = <image/jpeg, access+2592000> <text/css, access+2592000>
+
.TP
\fBarg_rewrite_mod = Module\fR
It is possible to install a module that rewrites all the
View
@@ -1162,6 +1162,8 @@ outh_set_static_headers(Req, UT, Headers, Range) ->
date = make_date_header(),
server = make_server_header(),
last_modified = make_last_modified_header(UT#urltype.finfo),
+ expires = make_expires_header(UT#urltype.mime, UT#urltype.finfo),
+ cache_control = make_cache_control_header(UT#urltype.mime, UT#urltype.finfo),
etag = make_etag_header(UT#urltype.finfo),
content_range = make_content_range_header(Range),
content_length = make_content_length_header(Length),
@@ -1183,6 +1185,8 @@ outh_set_304_headers(Req, UT, Headers) ->
date = make_date_header(),
server = make_server_header(),
last_modified = make_last_modified_header(UT#urltype.finfo),
+ expires = make_expires_header(UT#urltype.mime, UT#urltype.finfo),
+ cache_control = make_cache_control_header(UT#urltype.mime, UT#urltype.finfo),
etag = make_etag_header(UT#urltype.finfo),
content_length = make_content_length_header(0),
connection = make_connection_close_header(DoClose),
@@ -1200,6 +1204,8 @@ outh_set_dyn_headers(Req, Headers, UT) ->
server = make_server_header(),
connection = make_connection_close_header(DoClose),
content_type = make_content_type_header(UT#urltype.mime),
+ expires = make_expires_header(UT#urltype.mime, UT#urltype.finfo),
+ cache_control = make_cache_control_header(UT#urltype.mime, UT#urltype.finfo),
doclose = DoClose,
chunked = Chunked,
transfer_encoding =
@@ -1349,6 +1355,60 @@ do_make_last_modified(FI) ->
["Last-Modified: ", local_time_as_gmt_string(Then), "\r\n"].
+make_expires_header(MimeType, FI) ->
+ N = element(2, now()),
+ case get({expire, MimeType}) of
+ {Str, Secs} when N < (Secs+10) ->
+ Str;
+ _ ->
+ SC = get(sc),
+ case lists:keysearch(MimeType, 1, SC#sconf.expires) of
+ {value, {MimeType, Type, TTL}} ->
+ S1 = make_expires_header(Type, TTL, FI),
+ S2 = make_cache_control_header(TTL),
+ put({expire, MimeType}, {S1, N}),
+ put({cache_control, MimeType}, {S2, N}),
+ S1;
+ false ->
+ undefined
+ end
+ end.
+
+make_expires_header(access, TTL, _FI) ->
+ DateTime = erlang:localtime(),
+ Secs = calendar:datetime_to_gregorian_seconds(DateTime) + TTL,
+ ExpireTime = calendar:gregorian_seconds_to_datetime(Secs),
+ ["Expires: ", local_time_as_gmt_string(ExpireTime), "\r\n"];
+make_expires_header(modify, TTL, FI) ->
+ DateTime = FI#file_info.mtime,
+ Secs = calendar:datetime_to_gregorian_seconds(DateTime) + TTL,
+ ExpireTime = calendar:gregorian_seconds_to_datetime(Secs),
+ ["Expires: ", local_time_as_gmt_string(ExpireTime), "\r\n"].
+
+
+make_cache_control_header(MimeType, FI) ->
+ N = element(2, now()),
+ case get({cache_control, MimeType}) of
+ {Str, Secs} when N < (Secs+10) ->
+ Str;
+ _ ->
+ SC = get(sc),
+ case lists:keysearch(MimeType, 1, SC#sconf.expires) of
+ {value, {MimeType, Type, TTL}} ->
+ S1 = make_cache_control_header(TTL),
+ S2 = make_expires_header(Type, TTL, FI),
+ put({cache_control, MimeType}, {S1, N}),
+ put({expire, MimeType}, {S2, N}),
+ S1;
+ false ->
+ undefined
+ end
+ end.
+
+
+make_cache_control_header(TTL) ->
+ ["Cache-Control: ", "max-age=", integer_to_list(TTL), "\r\n"].
+
make_location_header(Where) ->
["Location: ", Where, "\r\n"].
@@ -1509,6 +1569,7 @@ outh_serialize() ->
noundef(H#outh.date),
noundef(H#outh.allow),
noundef(H#outh.last_modified),
+ noundef(H#outh.expires),
noundef(H#outh.etag),
noundef(H#outh.content_range),
noundef(H#outh.content_length),
View
@@ -1041,6 +1041,14 @@ fload(FD, server, GC, C, Cs, Lno, Chars) ->
{error, Str} ->
{error, ?F("~s at line ~w", [Str, Lno])}
end;
+ ["expires", '=' | Expires] ->
+ case parse_expires(Expires, []) of
+ {ok, L} ->
+ C2 = C#sconf{expires = L ++ C#sconf.expires},
+ fload(FD, server, GC, C2, Cs, Lno+1, Next);
+ {error, Str} ->
+ {error, ?F("~s at line ~w", [Str, Lno])}
+ end;
["errormod_404", '=' , Module] ->
C2 = C#sconf{errormod_404 = list_to_atom(Module)},
fload(FD, server, GC, C2, Cs, Lno+1, Next);
@@ -1649,7 +1657,7 @@ is_string_char([C|T]) ->
%% FIXME check that [C, hd(T)] really is a char ?? how
utf8;
true ->
- lists:member(C, [$., $/, $:, $_, $-, $~])
+ lists:member(C, [$., $/, $:, $_, $-, $+, $~])
end.
is_special(C) ->
@@ -1755,6 +1763,30 @@ parse_appmods([], Ack) ->
{ok, Ack}.
+parse_expires(['<', MimeType, ',' , Expire, '>' | Tail], Ack) ->
+ {Type, Value} =
+ case string:tokens(Expire, "+") of
+ [Secs] ->
+ {access, (catch list_to_integer(Secs))};
+ ["access", Secs] ->
+ {access, (catch list_to_integer(Secs))};
+ ["modify", Secs] ->
+ {modify, (catch list_to_integer(Secs))};
+ _ ->
+ {error, "Bad expires syntax"}
+ end,
+ if
+ Type =:= error ->
+ {Type, Value};
+ not is_integer(Value) ->
+ {error, "Bad expires syntax"};
+ true ->
+ E = {MimeType, Type, Value},
+ parse_expires(Tail, [E |Ack])
+ end;
+parse_expires([], Ack)->
+ {ok, Ack}.
+
ssl_start() ->
case catch ssl:start() of

0 comments on commit c60fc5f

Please sign in to comment.