1
%%%----------------------------------------------------------------------
1
%%%-------------------------------------------------------------------
2
2
%%% File : mod_http_fileserver.erl
3
3
%%% Author : Massimiliano Mirra <mmirra [at] process-one [dot] net>
4
4
%%% Purpose : Simple file server plugin for embedded ejabberd web server
8
%%% ejabberd, Copyright (C) 2002-2009 ProcessOne
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
%%% General Public License for more details.
20
%%% You should have received a copy of the GNU General Public License
21
%%% along with this program; if not, write to the Free Software
22
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
7
25
%%%----------------------------------------------------------------------
9
27
-module(mod_http_fileserver).
10
28
-author('mmirra@process-one.net').
12
30
-behaviour(gen_mod).
31
-behaviour(gen_server).
34
-export([start/2, stop/1]).
37
-export([start_link/2]).
39
%% gen_server callbacks
40
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
41
terminate/2, code_change/3]).
43
%% request_handlers callbacks
46
%% ejabberd_hooks callbacks
47
-export([reopen_log/1]).
22
49
-include("ejabberd.hrl").
23
50
-include("jlib.hrl").
24
-include("ejabberd_http.hrl").
25
-include("ejabberd_ctl.hrl").
26
51
-include_lib("kernel/include/file.hrl").
53
%%-include("ejabberd_http.hrl").
54
%% TODO: When ejabberd-modules SVN gets the new ejabberd_http.hrl, delete this code:
55
-record(request, {method,
65
tp, % transfer protocol = http | https
29
70
-define(STRING2LOWER, string).
31
72
-define(STRING2LOWER, httpd_util).
34
%%%----------------------------------------------------------------------
36
%%%----------------------------------------------------------------------
38
%%-----------------------------------------------------------------------
45
%% Handle an HTTP request.
49
%% Page to be sent back to the client and/or HTTP status code.
53
%% - LocalPath: part of the requested URL path that is "local to the
56
%%-----------------------------------------------------------------------
75
-record(state, {host, docroot, accesslog, accesslogfd, directory_indices,
76
default_content_type, content_types = []}).
78
-define(PROCNAME, ejabberd_mod_http_fileserver).
80
%% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data}
81
-define(HTTP_ERR_FILE_NOT_FOUND, {-1, 404, [], "Not found"}).
82
-define(HTTP_ERR_FORBIDDEN, {-1, 403, [], "Forbidden"}).
84
-define(DEFAULT_CONTENT_TYPE, "application/octet-stream").
85
-define(DEFAULT_CONTENT_TYPES, [{".css", "text/css"},
86
{".gif", "image/gif"},
87
{".html", "text/html"},
88
{".jar", "application/java-archive"},
89
{".jpeg", "image/jpeg"},
90
{".jpg", "image/jpeg"},
91
{".js", "text/javascript"},
92
{".png", "image/png"},
93
{".txt", "text/plain"},
94
{".xpi", "application/x-xpinstall"},
95
{".xul", "application/vnd.mozilla.xul+xml"}]).
99
%%====================================================================
101
%%====================================================================
104
Proc = get_proc_name(Host),
107
{?MODULE, start_link, [Host, Opts]},
108
transient, % if process crashes abruptly, it gets restarted
112
supervisor:start_child(ejabberd_sup, ChildSpec).
115
Proc = get_proc_name(Host),
116
gen_server:call(Proc, stop),
117
supervisor:terminate_child(ejabberd_sup, Proc),
118
supervisor:delete_child(ejabberd_sup, Proc).
120
%%====================================================================
122
%%====================================================================
123
%%--------------------------------------------------------------------
124
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
125
%% Description: Starts the server
126
%%--------------------------------------------------------------------
127
start_link(Host, Opts) ->
128
Proc = get_proc_name(Host),
129
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
131
%%====================================================================
132
%% gen_server callbacks
133
%%====================================================================
134
%%--------------------------------------------------------------------
135
%% Function: init(Args) -> {ok, State} |
136
%% {ok, State, Timeout} |
139
%% Description: Initiates the server
140
%%--------------------------------------------------------------------
141
init([Host, Opts]) ->
142
try initialize(Host, Opts) of
143
{DocRoot, AccessLog, AccessLogFD, DirectoryIndices,
144
DefaultContentType, ContentTypes} ->
145
{ok, #state{host = Host,
146
accesslog = AccessLog,
147
accesslogfd = AccessLogFD,
149
directory_indices = DirectoryIndices,
150
default_content_type = DefaultContentType,
151
content_types = ContentTypes}}
157
initialize(Host, Opts) ->
158
DocRoot = gen_mod:get_opt(docroot, Opts, undefined),
159
check_docroot_defined(DocRoot, Host),
160
DRInfo = check_docroot_exists(DocRoot),
161
check_docroot_is_dir(DRInfo, DocRoot),
162
check_docroot_is_readable(DRInfo, DocRoot),
163
AccessLog = gen_mod:get_opt(accesslog, Opts, undefined),
164
AccessLogFD = try_open_log(AccessLog, Host),
165
DirectoryIndices = gen_mod:get_opt(directory_indices, Opts, []),
166
DefaultContentType = gen_mod:get_opt(default_content_type, Opts,
167
?DEFAULT_CONTENT_TYPE),
168
ContentTypes = build_list_content_types(gen_mod:get_opt(content_types, Opts, []), ?DEFAULT_CONTENT_TYPES),
169
?INFO_MSG("initialize: ~n ~p", [ContentTypes]),%+++
170
{DocRoot, AccessLog, AccessLogFD, DirectoryIndices,
171
DefaultContentType, ContentTypes}.
173
%% @spec (AdminCTs::[CT], Default::[CT]) -> [CT]
174
%% where CT = {Extension::string(), Value}
175
%% Value = string() | undefined
176
%% @doc Return a unified list without duplicates.
177
%% Elements of AdminCTs have more priority.
178
%% If a CT is declared as 'undefined', then it is not included in the result.
179
build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) ->
180
AdminCTs = lists:ukeysort(1, AdminCTsUnsorted),
181
DefaultCTs = lists:ukeysort(1, DefaultCTsUnsorted),
182
CTsUnfiltered = lists:ukeymerge(1, AdminCTs, DefaultCTs),
183
[{Extension, Value} || {Extension, Value} <- CTsUnfiltered, Value /= undefined].
185
check_docroot_defined(DocRoot, Host) ->
187
undefined -> throw({undefined_docroot_option, Host});
191
check_docroot_exists(DocRoot) ->
192
case file:read_file_info(DocRoot) of
193
{error, Reason} -> throw({error_access_docroot, DocRoot, Reason});
197
check_docroot_is_dir(DRInfo, DocRoot) ->
198
case DRInfo#file_info.type of
200
_ -> throw({docroot_not_directory, DocRoot})
203
check_docroot_is_readable(DRInfo, DocRoot) ->
204
case DRInfo#file_info.access of
207
_ -> throw({docroot_not_readable, DocRoot})
210
try_open_log(undefined, _Host) ->
212
try_open_log(FN, Host) ->
213
FD = try open_log(FN) of
216
throw:{cannot_open_accesslog, FN, Reason} ->
217
?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]),
220
ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50),
223
%%--------------------------------------------------------------------
224
%% Function: handle_call(Request, From, State) -> {reply, Reply, State} |
225
%% {reply, Reply, State, Timeout} |
226
%% {noreply, State} |
227
%% {noreply, State, Timeout} |
228
%% {stop, Reason, Reply, State} |
229
%% {stop, Reason, State}
230
%% Description: Handling call messages
231
%%--------------------------------------------------------------------
232
handle_call({serve, LocalPath}, _From, State) ->
233
Reply = serve(LocalPath, State#state.docroot, State#state.directory_indices,
234
State#state.default_content_type, State#state.content_types),
235
{reply, Reply, State};
236
handle_call(_Request, _From, State) ->
239
%%--------------------------------------------------------------------
240
%% Function: handle_cast(Msg, State) -> {noreply, State} |
241
%% {noreply, State, Timeout} |
242
%% {stop, Reason, State}
243
%% Description: Handling cast messages
244
%%--------------------------------------------------------------------
245
handle_cast({add_to_log, FileSize, Code, Request}, State) ->
246
add_to_log(State#state.accesslogfd, FileSize, Code, Request),
248
handle_cast(reopen_log, State) ->
249
FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd),
250
{noreply, State#state{accesslogfd = FD2}};
251
handle_cast(_Msg, State) ->
254
%%--------------------------------------------------------------------
255
%% Function: handle_info(Info, State) -> {noreply, State} |
256
%% {noreply, State, Timeout} |
257
%% {stop, Reason, State}
258
%% Description: Handling all non call/cast messages
259
%%--------------------------------------------------------------------
260
handle_info(_Info, State) ->
263
%%--------------------------------------------------------------------
264
%% Function: terminate(Reason, State) -> void()
265
%% Description: This function is called by a gen_server when it is about to
266
%% terminate. It should be the opposite of Module:init/1 and do any necessary
267
%% cleaning up. When it returns, the gen_server terminates with Reason.
268
%% The return value is ignored.
269
%%--------------------------------------------------------------------
270
terminate(_Reason, State) ->
271
close_log(State#state.accesslogfd),
272
ejabberd_hooks:delete(reopen_log_hook, State#state.host, ?MODULE, reopen_log, 50),
275
%%--------------------------------------------------------------------
276
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
277
%% Description: Convert process state when code is changed
278
%%--------------------------------------------------------------------
279
code_change(_OldVsn, State, _Extra) ->
282
%%====================================================================
283
%% request_handlers callbacks
284
%%====================================================================
286
%% @spec (LocalPath, Request) -> {HTTPCode::integer(), [Header], Page::string()}
287
%% @doc Handle an HTTP request.
288
%% LocalPath is the part of the requested URL path that is "local to the module".
289
%% Returns the page to be sent back to the client and/or HTTP status code.
59
290
process(LocalPath, Request) ->
60
291
?DEBUG("Requested ~p", [LocalPath]),
62
Result = serve(LocalPath),
63
case ets:lookup(mod_http_fileserver, accessfile) of
66
[{accessfile, AccessFile}] ->
67
{Code, _, _} = Result,
68
log(AccessFile, Code, Request)
73
[{docroot, DocRoot}] = ets:lookup(mod_http_fileserver, docroot),
292
try gen_server:call(get_proc_name(Request#request.host), {serve, LocalPath}) of
293
{FileSize, Code, Headers, Contents} ->
294
add_to_log(FileSize, Code, Request),
295
{Code, Headers, Contents}
298
ejabberd_web:error(not_found)
301
serve(LocalPath, DocRoot, DirectoryIndices, DefaultContentType, ContentTypes) ->
74
302
FileName = filename:join(filename:split(DocRoot) ++ LocalPath),
75
case file:read_file(FileName) of
77
?DEBUG("Delivering content.", []),
79
[{"Server", "ejabberd"},
80
{"Last-Modified", last_modified(FileName)},
81
{"Content-Type", content_type(FileName)}],
84
?DEBUG("Delivering error: ~p", [Error]),
86
eacces -> {403, [], "Forbidden"};
87
enoent -> {404, [], "Not found"};
88
_Else -> {404, [], atom_to_list(Error)}
92
ctl_process(_Val, ["reopen-weblog"]) ->
93
mod_http_fileserver_server ! reopenlog,
95
ctl_process(Val, _Args) ->
98
%%%----------------------------------------------------------------------
100
%%%----------------------------------------------------------------------
106
join([H | T], Separator) ->
107
lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H, T).
109
log(File, Code, Request) ->
303
case file:read_file_info(FileName) of
304
{error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND;
305
{error, eacces} -> ?HTTP_ERR_FORBIDDEN;
306
{ok, #file_info{type = directory}} -> serve_index(FileName,
310
{ok, FileInfo} -> serve_file(FileInfo, FileName,
315
%% Troll through the directory indices attempting to find one which
316
%% works, if none can be found, return a 404.
317
serve_index(_FileName, [], _DefaultContentType, _ContentTypes) ->
318
?HTTP_ERR_FILE_NOT_FOUND;
319
serve_index(FileName, [Index | T], DefaultContentType, ContentTypes) ->
320
IndexFileName = filename:join([FileName] ++ [Index]),
321
case file:read_file_info(IndexFileName) of
322
{error, _Error} -> serve_index(FileName, T, DefaultContentType, ContentTypes);
323
{ok, #file_info{type = directory}} -> serve_index(FileName, T, DefaultContentType, ContentTypes);
324
{ok, FileInfo} -> serve_file(FileInfo, IndexFileName, DefaultContentType, ContentTypes)
327
%% Assume the file exists if we got this far and attempt to read it in
329
serve_file(FileInfo, FileName, DefaultContentType, ContentTypes) ->
330
?DEBUG("Delivering: ~s", [FileName]),
331
{ok, FileContents} = file:read_file(FileName),
332
ContentType = content_type(FileName, DefaultContentType, ContentTypes),
333
{FileInfo#file_info.size,
334
200, [{"Server", "ejabberd"},
335
{"Last-Modified", last_modified(FileInfo)},
336
{"Content-Type", ContentType}],
339
%%----------------------------------------------------------------------
341
%%----------------------------------------------------------------------
344
case file:open(FN, [append]) of
348
throw({cannot_open_accesslog, FN, Reason})
354
reopen_log(undefined, undefined) ->
356
reopen_log(FN, FD) ->
361
gen_server:cast(get_proc_name(Host), reopen_log).
363
add_to_log(FileSize, Code, Request) ->
364
gen_server:cast(get_proc_name(Request#request.host),
365
{add_to_log, FileSize, Code, Request}).
367
add_to_log(undefined, _FileSize, _Code, _Request) ->
369
add_to_log(File, FileSize, Code, Request) ->
110
370
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
111
IP = join(tuple_to_list(element(1, Request#request.ip)), "."),
371
IP = ip_to_string(element(1, Request#request.ip)),
112
372
Path = join(Request#request.path, "/"),
113
373
Query = case join(lists:map(fun(E) -> lists:concat([element(1, E), "=", element(2, E)]) end,
114
374
Request#request.q), "&") of
120
% combined apache like log format :
121
% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
122
% XXX TODO some fields are harcoded/missing (reply size, user agent or referer for example)
123
io:format(File, "~s - - [~p/~p/~p:~p:~p:~p] \"~s /~s~s\" ~p -1 \"-\" \"-\"~n",
124
[IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code]).
126
content_type(Filename) ->
127
case ?STRING2LOWER:to_lower(filename:extension(Filename)) of
128
".jpg" -> "image/jpeg";
129
".jpeg" -> "image/jpeg";
130
".gif" -> "image/gif";
131
".png" -> "image/png";
132
".html" -> "text/html";
133
".css" -> "text/css";
134
".txt" -> "text/plain";
135
".xul" -> "application/vnd.mozilla.xul+xml";
136
".jar" -> "application/java-archive";
137
".xpi" -> "application/x-xpinstall";
138
".js" -> "application/x-javascript";
139
_Else -> "application/octet-stream"
142
last_modified(FileName) ->
143
{ok, FileInfo} = file:read_file_info(FileName),
380
UserAgent = find_header('User-Agent', Request#request.headers, "-"),
381
Referer = find_header('Referer', Request#request.headers, "-"),
382
%% Pseudo Combined Apache log format:
383
%% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
384
%% TODO some fields are harcoded/missing:
385
%% The date/time integers should have always 2 digits. For example day "7" should be "07"
386
%% Month should be 3*letter, not integer 1..12
387
%% Missing time zone = (`+' | `-') 4*digit
388
%% Missing protocol version: HTTP/1.1
389
%% For reference: http://httpd.apache.org/docs/2.2/logs.html
390
io:format(File, "~s - - [~p/~p/~p:~p:~p:~p] \"~s /~s~s\" ~p ~p ~p ~p~n",
391
[IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code,
392
FileSize, Referer, UserAgent]).
394
find_header(Header, Headers, Default) ->
395
case lists:keysearch(Header, 1, Headers) of
396
{value, {_, Value}} -> Value;
400
%%----------------------------------------------------------------------
402
%%----------------------------------------------------------------------
404
get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
410
join([H | T], Separator) ->
411
lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H, T).
413
content_type(Filename, DefaultContentType, ContentTypes) ->
414
Extension = ?STRING2LOWER:to_lower(filename:extension(Filename)),
415
case lists:keysearch(Extension, 1, ContentTypes) of
416
{value, {_, ContentType}} -> ContentType;
417
false -> DefaultContentType
420
last_modified(FileInfo) ->
144
421
Then = FileInfo#file_info.mtime,
145
422
httpd_util:rfc1123_date(Then).
147
open_file(Filename) ->
148
case file:open(Filename, [append]) of
150
ets:insert(mod_http_fileserver, {accessfile, File}),
153
{'EXIT', {unaccessible_accessfile, ?MODULE}}
159
case ets:lookup(mod_http_fileserver, accessfile) of
162
[{accessfile, AccessFile}] ->
163
file:close(AccessFile),
164
case open_file(Filename) of
177
%%%----------------------------------------------------------------------
178
%%% BEHAVIOUR CALLBACKS
179
%%%----------------------------------------------------------------------
181
%% TODO: Improve this module to allow each virtual host to have a different
182
%% options. See http://support.process-one.net/browse/EJAB-561
183
start(_Host, Opts) ->
184
case ets:info(mod_http_fileserver, name) of
191
start2(_Host, Opts) ->
192
case gen_mod:get_opt(docroot, Opts, undefined) of
194
{'EXIT', {missing_document_root, ?MODULE}};
196
case filelib:is_dir(DocRoot) of
198
%% XXX WARNING, using a single ets table name will
199
%% not work with virtual hosts
200
ets:new(mod_http_fileserver, [named_table, public]),
201
ets:insert(mod_http_fileserver, [{docroot, DocRoot}]),
202
case gen_mod:get_opt(accesslog, Opts, undefined) of
206
%% XXX same remark as above for proc name
207
ejabberd_ctl:register_commands(
209
"reopen http fileserver log file"}],
210
?MODULE, ctl_process),
211
register(mod_http_fileserver_server,
212
spawn(?MODULE, loop, [Filename])),
216
{'EXIT', {unaccessible_document_root, ?MODULE}}
221
case ets:info(mod_http_fileserver, name) of
225
case ets:lookup(mod_http_fileserver, accessfile) of
228
[{accessfile, AccessFile}] ->
229
ejabberd_ctl:unregister_commands(
231
"reopen http fileserver log file"}],
232
?MODULE, ctl_process),
233
mod_http_fileserver_server ! stop,
234
file:close(AccessFile)
236
ets:delete(mod_http_fileserver)
424
%% Convert IP address tuple to string representation. Accepts either
425
%% IPv4 or IPv6 address tuples.
426
ip_to_string(Address) when size(Address) == 4 ->
427
join(tuple_to_list(Address), ".");
428
ip_to_string(Address) when size(Address) == 8 ->
429
Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)),
430
?STRING2LOWER:to_lower(lists:flatten(join(Parts, ":"))).