1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2001-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21
22-module(mod_responsecontrol).
23-export([do/1]).
24
25-include("httpd.hrl").
26-include("httpd_internal.hrl").
27
28do(Info) ->
29    case proplists:get_value(status, Info#mod.data) of
30	%% A status code has been generated!
31	{_StatusCode, _PhraseArgs, _Reason} ->
32	    {proceed, Info#mod.data};
33	%% No status code has been generated!
34	undefined ->
35	    case proplists:get_value(response, Info#mod.data) of
36		%% No response has been generated!
37		undefined ->
38		    case do_responsecontrol(Info) of
39			continue ->
40			    {proceed, Info#mod.data};
41			Response ->
42			    {proceed,[Response | Info#mod.data]}
43		    end;
44		%% A response has been generated or sent!
45		_Response ->
46 		    {proceed, Info#mod.data}
47	    end
48    end.
49
50%%----------------------------------------------------------------------
51%%Control that the request header did not contians any limitations
52%%wheather a response shall be createed or not
53%%----------------------------------------------------------------------
54do_responsecontrol(Info) ->
55    Path = mod_alias:path(Info#mod.data, Info#mod.config_db,
56			  Info#mod.request_uri),
57    case file:read_file_info(Path) of
58	{ok, FileInfo} ->
59	    control(Path, Info, FileInfo);
60	_ ->
61	    %% The requested asset is not a plain file and then it must
62	    %% be generated everytime its requested
63	    continue
64    end.
65
66%%----------------------------------------------------------------------
67%%Control the If-Match, If-None-Match,  and If-Modified-Since
68%%----------------------------------------------------------------------
69
70
71%% If a client sends more then one of the if-XXXX fields in a request
72%% The standard says it does not specify the behaviour so I specified it :-)
73%% The priority between the fields is
74%% 1.If-modified
75%% 2.If-Unmodified
76%% 3.If-Match
77%% 4.If-Nomatch
78
79%% This means if more than one of the fields are in the request the
80%% field with highest priority will be used
81
82%%If the request is a range request the If-Range field will be the winner.
83
84control(Path, Info, FileInfo) ->
85    case control_range(Path, Info, FileInfo) of
86	undefined ->
87	    case control_Etag(Path, Info, FileInfo) of
88		undefined ->
89		    case control_modification(Path, Info, FileInfo) of
90			continue ->
91			    continue;
92			ReturnValue ->
93			    send_return_value(ReturnValue, FileInfo)
94		    end;
95		continue ->
96		    continue;
97		ReturnValue ->
98		    send_return_value(ReturnValue, FileInfo)
99	    end;
100	Response->
101	    Response
102    end.
103
104%%----------------------------------------------------------------------
105%%If there are both a range and an if-range field control if
106%%----------------------------------------------------------------------
107control_range(Path,Info,FileInfo) ->
108    case proplists:get_value("range", Info#mod.parsed_header) of
109	undefined->
110	    undefined;
111	_Range ->
112	    case proplists:get_value("if-range", Info#mod.parsed_header) of
113		undefined ->
114		    undefined;
115		EtagOrDate ->
116		    control_if_range(Path,Info,FileInfo,EtagOrDate)
117	    end
118    end.
119
120control_if_range(_Path, Info, FileInfo, EtagOrDate) ->
121    case httpd_util:convert_request_date(strip_date(EtagOrDate)) of
122	bad_date ->
123	    FileEtag=httpd_util:create_etag(FileInfo),
124	    case FileEtag of
125		EtagOrDate ->
126		    continue;
127		_ ->
128		    {if_range,send_file}
129	    end;
130	_ErlDate ->
131	    %%We got the date in the request if it is
132	    case control_modification_data(Info, FileInfo#file_info.mtime,
133					   "if-range") of
134		modified ->
135		    {if_range,send_file};
136		_UnmodifiedOrUndefined->
137		    continue
138	    end
139    end.
140
141%%----------------------------------------------------------------------
142%%Controls the values of the If-Match and I-None-Mtch
143%%----------------------------------------------------------------------
144control_Etag(Path, Info, FileInfo)->
145    FileEtag = httpd_util:create_etag(FileInfo),
146    %%Control if the E-Tag for the resource  matches one of the Etags in
147    %%the -if-match header field
148    case control_match(Info, FileInfo, "if-match", FileEtag) of
149	nomatch ->
150	    %%None of the Etags in the if-match field matched the current
151	    %%Etag for the resource return a 304
152	    {412, Info, Path};
153	match ->
154	    continue;
155	undefined ->
156	    case control_match(Info, FileInfo, "if-none-match",  FileEtag) of
157		nomatch ->
158		    continue;
159		match ->
160		    case  Info#mod.method of
161			"GET" ->
162			    {304, Info, Path};
163			"HEAD" ->
164			    {304, Info, Path};
165			_OtherrequestMethod ->
166			    {412, Info, Path}
167		    end;
168		undefined ->
169		    undefined
170	    end
171    end.
172
173%%----------------------------------------------------------------------
174%%Control if there are any Etags for HeaderField in the request if so
175%%Control if they match the Etag for the requested file
176%%----------------------------------------------------------------------
177control_match(Info, _FileInfo, HeaderField, FileEtag)->
178    case split_etags(proplists:get_value(HeaderField,
179					 Info#mod.parsed_header)) of
180 	undefined->
181	    undefined;
182        Etags->
183	    %%Control that the match any star not is availible
184	    case lists:member("*",Etags) of
185		true->
186		    match;
187		false->
188		    compare_etags(FileEtag, Etags)
189	    end
190    end.
191
192%%----------------------------------------------------------------------
193%%Split the etags from the request
194%%----------------------------------------------------------------------
195split_etags(undefined)->
196    undefined;
197split_etags(Tags) ->
198    string:tokens(Tags,", ").
199
200%%----------------------------------------------------------------------
201%%Control if the etag for the file is in the list
202%%----------------------------------------------------------------------
203compare_etags(Tag,Etags) ->
204    case lists:member(Tag,Etags) of
205	true ->
206	    match;
207	_ ->
208	    nomatch
209    end.
210
211%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
212%%                                                                  %%
213%% Control if the file is modified                                  %%
214%%                                                                  %%
215%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
216
217%%----------------------------------------------------------------------
218%% Control the If-Modified-Since and If-Not-Modified-Since header fields
219%%----------------------------------------------------------------------
220control_modification(Path,Info,FileInfo)->
221    case control_modification_data(Info,
222				   FileInfo#file_info.mtime,
223				   "if-modified-since") of
224	modified->
225	    continue;
226	unmodified->
227	    {304, Info, Path};
228	{bad_date, _} = BadDate->
229	    {400, Info, BadDate};
230	undefined ->
231	    case control_modification_data(Info,
232					   FileInfo#file_info.mtime,
233					   "if-unmodified-since") of
234		modified  ->
235		    {412, Info, Path};
236		_ContinueUndefined ->
237		    continue
238	    end
239    end.
240
241%%----------------------------------------------------------------------
242%%Controls the date from the http-request if-modified-since and
243%%if-not-modified-since against the modification data of the
244%%File
245%%----------------------------------------------------------------------
246%%Info is the record about the request
247%%ModificationTime is the time the file was edited last
248%%Header Field is the name of the field  to control
249
250control_modification_data(Info, ModificationTime, HeaderField)->
251    case strip_date(proplists:get_value(HeaderField,
252					Info#mod.parsed_header)) of
253 	undefined->
254	    undefined;
255	LastModified0 ->
256	    case httpd_util:convert_request_date(LastModified0) of
257	    	bad_date ->
258	    	    {bad_date, LastModified0};
259	    	ConvertedReqDate ->
260                    LastModified =  calendar:universal_time_to_local_time(ConvertedReqDate),
261                     FileTime =
262			calendar:datetime_to_gregorian_seconds(ModificationTime),
263		    FieldTime =
264			calendar:datetime_to_gregorian_seconds(LastModified),
265		    if
266			FileTime =< FieldTime ->
267			    unmodified;
268			FileTime >= FieldTime ->
269			    modified
270		    end
271
272	    end
273    end.
274
275%% IE4 & NS4 sends an extra '; length=xxxx' string at the end of the If-Modified-Since
276%% header, we detect this and ignore it (the RFCs does not mention this).
277strip_date(undefined) ->
278    undefined;
279strip_date([]) ->
280    [];
281strip_date([$;,$ | _]) ->
282    [];
283strip_date([C | Rest]) ->
284    [C | strip_date(Rest)].
285
286send_return_value({412,_,_}, _FileInfo)->
287    {status,{412,none,"Precondition Failed"}};
288
289send_return_value({400,_, {bad_date, BadDate}}, _FileInfo)->
290    {status, {400, none, "Bad date: " ++ BadDate}};
291
292send_return_value({304,Info,Path}, FileInfo)->
293    Suffix = httpd_util:strip_extension_dot(Path),
294    MimeType = httpd_util:lookup_mime_default(Info#mod.config_db,Suffix,
295					      "text/plain"),
296    LastModified =
297	case (catch httpd_util:rfc1123_date(FileInfo#file_info.mtime)) of
298	    Date when is_list(Date) ->
299		[{last_modified, Date}];
300	    _ -> %% This will rarly happen, but could happen
301		 %% if a computer is wrongly configured.
302		[]
303	end,
304
305    Header = [{code,304},
306	      {etag, httpd_util:create_etag(FileInfo)},
307	      {content_length,"0"}, {mime_type, MimeType} | LastModified],
308    {response, {response, Header, nobody}}.
309