1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1997-2018. 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-module(mod_alias).
22
23-export([do/1,
24	 real_name/3,
25	 real_script_name/3,
26	 default_index/2,
27	 store/2,
28	 path/3]).
29
30-include("httpd.hrl").
31-include("httpd_internal.hrl").
32-include("inets_internal.hrl").
33
34-define(VMODULE,"ALIAS").
35
36%% do
37
38do(#mod{data = Data} = Info) ->
39    case proplists:get_value(status, Data) of
40	%% A status code has been generated!
41	{_StatusCode, _PhraseArgs, _Reason} ->
42	    {proceed, Data};
43	%% No status code has been generated!
44	undefined ->
45	    case proplists:get_value(response, Data) of
46		%% No response has been generated!
47		undefined ->
48		    do_alias(Info);
49		%% A response has been generated or sent!
50		_Response ->
51		    {proceed, Data}
52	    end
53    end.
54
55do_alias(#mod{config_db   = ConfigDB,
56	      request_uri = ReqURI,
57	      socket_type = SocketType,
58	      data        = Data}) ->
59    {ShortPath, Path, AfterPath} =
60	real_name(ConfigDB, ReqURI, which_alias(ConfigDB)),
61    %% Relocate if a trailing slash is missing else proceed!
62    LastChar = lists:last(ShortPath),
63    case file:read_file_info(ShortPath) of
64	{ok, FileInfo} when ((FileInfo#file_info.type =:= directory) andalso
65			     (LastChar =/= $/)) ->
66	    ServerName = which_server_name(ConfigDB),
67	    Port = port_string(which_port(ConfigDB)),
68	    Protocol = get_protocol(SocketType),
69	    URL = Protocol ++ ServerName ++ Port ++ ReqURI ++ "/",
70	    ReasonPhrase = httpd_util:reason_phrase(301),
71	    Message = httpd_util:message(301, URL, ConfigDB),
72	    {proceed,
73	     [{response,
74	       {301, ["Location: ", URL, "\r\n"
75		      "Content-Type: text/html\r\n",
76		      "\r\n",
77		      "<HTML>\n<HEAD>\n<TITLE>",ReasonPhrase,
78		      "</TITLE>\n</HEAD>\n"
79		      "<BODY>\n<H1>",ReasonPhrase,
80		      "</H1>\n", Message,
81		      "\n</BODY>\n</HTML>\n"]}}|
82	      [{real_name, {Path, AfterPath}} | Data]]};
83	_NoFile ->
84	    {proceed, [{real_name, {Path, AfterPath}} | Data]}
85    end.
86
87port_string(80) ->
88    "";
89port_string(Port) ->
90    ":" ++ integer_to_list(Port).
91
92get_protocol(ip_comm) ->
93    "http://";
94get_protocol(_) ->
95    %% Should clean up to have only one ssl type essl vs ssl is not relevant any more
96    "https://".
97
98%% real_name
99
100real_name(ConfigDB, RequestURI, []) ->
101    {Prefix, DocumentRoot} = which_document_root(ConfigDB),
102    RealName = DocumentRoot ++ RequestURI,
103    {ShortPath, _AfterPath} = httpd_util:split_path(RealName),
104    {Path, AfterPath} =
105	httpd_util:split_path(default_index(ConfigDB, RealName)),
106    {Prefix ++ ShortPath, Prefix ++ Path, AfterPath};
107real_name(ConfigDB, RequestURI, [{MP,Replacement}| _] = Aliases)
108  when element(1, MP) =:= re_pattern ->
109    case longest_match(Aliases, RequestURI) of
110	{match, {MP, Replacement}} ->
111	    NewURI = re:replace(RequestURI, MP, Replacement, [{return,list}]),
112	    {ShortPath,_} = httpd_util:split_path(NewURI),
113	    {Path,AfterPath} =
114		httpd_util:split_path(default_index(ConfigDB, NewURI)),
115	    {ShortPath, Path, AfterPath};
116	nomatch ->
117	    real_name(ConfigDB, RequestURI, [])
118    end;
119
120real_name(ConfigDB, RequestURI,  [{_,_}|_] = Aliases) ->
121    case longest_match(Aliases, RequestURI) of
122	{match, {FakeName, RealName}} ->
123	    ActualName = re:replace(RequestURI,
124				    "^" ++ FakeName, RealName, [{return,list}]),
125 	    {ShortPath, _AfterPath} = httpd_util:split_path(ActualName),
126	    {Path, AfterPath} =
127		httpd_util:split_path(default_index(ConfigDB, ActualName)),
128	    {ShortPath, Path, AfterPath};
129	nomatch ->
130	    real_name(ConfigDB, RequestURI, [])
131    end.
132
133longest_match(Aliases, RequestURI) ->
134    longest_match(Aliases, RequestURI, _LongestNo = 0, _LongestAlias = undefined).
135
136longest_match([{FakeName, RealName} | Rest], RequestURI, LongestNo, LongestAlias) ->
137    case re:run(RequestURI, "^" ++ FakeName, [{capture, first}]) of
138	{match, [{_, Length}]} ->
139	    if
140		Length > LongestNo ->
141		    longest_match(Rest, RequestURI, Length, {FakeName, RealName});
142		true ->
143		    longest_match(Rest, RequestURI, LongestNo, LongestAlias)
144	    end;
145	nomatch ->
146	    longest_match(Rest, RequestURI, LongestNo, LongestAlias)
147    end;
148longest_match([], _RequestURI, 0, _LongestAlias) ->
149    nomatch;
150longest_match([], _RequestURI, _LongestNo, LongestAlias) ->
151    {match, LongestAlias}.
152
153%% real_script_name
154
155real_script_name(_ConfigDB, _RequestURI, []) ->
156    not_a_script;
157real_script_name(ConfigDB, RequestURI, [{FakeName,RealName} | Rest]) ->
158    case re:run(RequestURI, "^" ++ FakeName, [{capture, none}]) of
159	match ->
160	    ActualName0 =
161		re:replace(RequestURI, "^" ++ FakeName, RealName,  [{return,list}]),
162            ActualName = abs_script_path(ConfigDB, ActualName0),
163	    httpd_util:split_script_path(default_index(ConfigDB, ActualName));
164	nomatch ->
165	    real_script_name(ConfigDB, RequestURI, Rest)
166    end.
167
168%% ERL-574: relative path in script_alias property results in malformed url
169abs_script_path(ConfigDB, [$.|_] = RelPath) ->
170    Root = httpd_util:lookup(ConfigDB, server_root),
171    Root ++ "/" ++ RelPath;
172abs_script_path(_, RelPath) ->
173    RelPath.
174
175%% default_index
176
177default_index(ConfigDB, Path) ->
178    case file:read_file_info(Path) of
179	{ok, FileInfo} when FileInfo#file_info.type =:= directory ->
180	    DirectoryIndex = which_directory_index(ConfigDB),
181	    append_index(Path, DirectoryIndex);
182	_ ->
183	    Path
184    end.
185
186append_index(RealName, []) ->
187    RealName;
188append_index(RealName, [Index | Rest]) ->
189    case file:read_file_info(filename:join(RealName, Index)) of
190	{error, _Reason} ->
191	    append_index(RealName, Rest);
192	_ ->
193	    filename:join(RealName, Index)
194    end.
195
196%% path
197
198path(Data, ConfigDB, RequestURI0) ->
199    case proplists:get_value(real_name, Data) of
200	undefined ->
201            {Prefix, DocumentRoot} = which_document_root(ConfigDB),
202            RequestURI = percent_decode_path(RequestURI0),
203            {Path, _AfterPath} =
204                httpd_util:split_path(DocumentRoot ++ RequestURI),
205            Prefix ++ Path;
206	{Path, _AfterPath} ->
207	    Path
208    end.
209
210percent_decode_path(InitPath) ->
211    case uri_string:percent_decode(InitPath) of
212        {error, _} ->
213            InitPath;
214        Path0 -> %% Protect against vulnerabilities
215            case uri_string:normalize(Path0) of
216                {error, _, _} ->
217                    InitPath;
218                Path ->
219                    Path
220            end
221    end.
222%%
223%% Configuration
224%%
225store({directory_index, Value} = Conf, _) when is_list(Value) ->
226    case is_directory_index_list(Value) of
227	true ->
228	    {ok, Conf};
229	false ->
230	    {error, {wrong_type, {directory_index, Value}}}
231    end;
232store({directory_index, Value}, _) ->
233    {error, {wrong_type, {directory_index, Value}}};
234store({alias, {Fake, Real}} = Conf, _)
235  when is_list(Fake), is_list(Real) ->
236    {ok, Conf};
237store({alias, Value}, _) ->
238    {error, {wrong_type, {alias, Value}}};
239store({re_write, {Re, Replacement}} = Conf, _)
240  when is_list(Re), is_list(Replacement) ->
241    case re:compile(Re) of
242	{ok, MP} ->
243	    {ok, {alias, {MP, Replacement}}};
244	{error,_} ->
245	    {error, {re_compile, Conf}}
246    end;
247store({re_write, _} = Conf, _) ->
248    {error, {wrong_type, Conf}};
249store({script_alias, {Fake, Real}} = Conf, _)
250  when is_list(Fake), is_list(Real) ->
251    {ok, Conf};
252store({script_alias, Value}, _) ->
253    {error, {wrong_type, {script_alias, Value}}};
254store({script_re_write, {Re, Replacement}} = Conf, _)
255  when is_list(Re), is_list(Replacement) ->
256    case re:compile(Re) of
257	{ok, MP} ->
258	    {ok, {script_alias, {MP, Replacement}}};
259	{error,_} ->
260	    {error, {re_compile, Conf}}
261    end;
262store({script_re_write, _} = Conf, _) ->
263    {error, {wrong_type, Conf}}.
264
265is_directory_index_list([]) ->
266    true;
267is_directory_index_list([Head | Tail]) when is_list(Head) ->
268    is_directory_index_list(Tail);
269is_directory_index_list(_) ->
270    false.
271
272
273%% ---------------------------------------------------------------------
274
275which_alias(ConfigDB) ->
276    httpd_util:multi_lookup(ConfigDB, alias).
277
278which_server_name(ConfigDB) ->
279    httpd_util:lookup(ConfigDB, server_name).
280
281which_port(ConfigDB) ->
282    httpd_util:lookup(ConfigDB, port, 80).
283
284which_document_root(ConfigDB) ->
285    Root = httpd_util:lookup(ConfigDB, document_root, ""),
286    case string:tokens(Root, ":") of
287        [Prefix, Path] ->
288            {Prefix ++ ":", Path};
289        [Path] ->
290            {"", Path}
291    end.
292
293which_directory_index(ConfigDB) ->
294    httpd_util:lookup(ConfigDB, directory_index, []).
295