~ubuntu-branches/ubuntu/trusty/ejabberd/trusty-proposed

« back to all changes in this revision

Viewing changes to src/web/mod_http_fileserver.erl

  • Committer: Bazaar Package Importer
  • Author(s): Gerfried Fuchs, Konstantin Khomoutov, Gerfried Fuchs
  • Date: 2009-12-04 18:22:49 UTC
  • mfrom: (1.1.11 upstream)
  • Revision ID: james.westby@ubuntu.com-20091204182249-6jfmdz8878h7oaos
Tags: 2.1.0-1
[ Konstantin Khomoutov ]
* New upstream release (Closes: #519858).
  This also adds support for LDAPS upstream (Closes: #526145).
* Do not depend on cdbs anymore, port debian/rules to dh+quilt,
  remove build dependency on patchutils, use erlang-depends.
* Bump debhelper version to 7, standards base to 3.8.3
* Depend on erlang R13B.
* Recommend imagemagick (for captcha support).
* Remove deprecated patches (ssl.patch patch, dynamic_compile_loglevel.patch,
  ldaps.patch, update.patch, proxy.patch, caps.patch, convert.patch,
  s2s.patch).
* Replace mod_ctlextra with mod_admin_extra.
* Use upstream inetrc file.
* Bring debian/ejabberd.cfg and ejabberdctl in sync with upstream.
* Update ejabberdctl manual page.
* Provide NEWS file.
* Rework README.Debian:
  * Group all information into sections.
  * Describe issues with epam binary (Closes: #502791).
  * Discuss how to use DBMS backends (Closes: #540915, #507144).
  * Discuss upgrading from 2.0.x series.
* Implement PID file management (Closes: #519858).
* Make logrotate process all files matching "*.log".
* Improve init script:
  * Make init script LSB-compliant.
  * Implement "live" target which allows to run ejabberd in foreground.
* Make captcha.sh use bash explicitly.
* Rework node-generation for ejabberdctl to fix ejabberd's atom table
  overflows while preserving the possibility to run several versions
  of ejabberdctl concurrently as before.
* Add webadmin patch restoring compatibility with Erlang/OTP <= R12B-4.
* Integrate upstream patch for EJAB-1106.
* Add upstream patch for EJAB-1098.
* Add upstream patch for EJAB-1045.
* Add Konstantin Khomoutov to uploaders.
* Add Japanese debconf translation (thanks to Hideki Yamane)
  (Closes: #558071).

[ Gerfried Fuchs ]
* Build-Depend on po-debconf so po2debconf can be called.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
5
5
%%% Created :
6
 
%%% Id      :
 
6
%%%
 
7
%%%
 
8
%%% ejabberd, Copyright (C) 2002-2009   ProcessOne
 
9
%%%
 
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.
 
14
%%%
 
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.
 
19
%%%
 
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
 
23
%%% 02111-1307 USA
 
24
%%%
7
25
%%%----------------------------------------------------------------------
8
26
 
9
27
-module(mod_http_fileserver).
10
28
-author('mmirra@process-one.net').
11
29
 
12
30
-behaviour(gen_mod).
13
 
 
14
 
-export([
15
 
         start/2,
16
 
         stop/1,
17
 
         process/2,
18
 
         loop/1,
19
 
         ctl_process/2
20
 
        ]).
 
31
-behaviour(gen_server).
 
32
 
 
33
%% gen_mod callbacks
 
34
-export([start/2, stop/1]).
 
35
 
 
36
%% API
 
37
-export([start_link/2]).
 
38
 
 
39
%% gen_server callbacks
 
40
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
 
41
         terminate/2, code_change/3]).
 
42
 
 
43
%% request_handlers callbacks
 
44
-export([process/2]).
 
45
 
 
46
%% ejabberd_hooks callbacks
 
47
-export([reopen_log/1]).
21
48
 
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").
27
52
 
 
53
%%-include("ejabberd_http.hrl").
 
54
%% TODO: When ejabberd-modules SVN gets the new ejabberd_http.hrl, delete this code:
 
55
-record(request, {method,
 
56
                  path,
 
57
                  q = [],
 
58
                  us,
 
59
                  auth,
 
60
                  lang = "",
 
61
                  data = "",
 
62
                  ip,
 
63
                  host, % string()
 
64
                  port, % integer()
 
65
                  tp, % transfer protocol = http | https
 
66
                  headers
 
67
                 }).
 
68
 
28
69
-ifdef(SSL39).
29
70
-define(STRING2LOWER, string).
30
71
-else.
31
72
-define(STRING2LOWER, httpd_util).
32
73
-endif.
33
74
 
34
 
%%%----------------------------------------------------------------------
35
 
%%% REQUEST HANDLERS
36
 
%%%----------------------------------------------------------------------
37
 
 
38
 
%%-----------------------------------------------------------------------
39
 
%% FUNCTION
40
 
%%
41
 
%%   process/2
42
 
%%
43
 
%% PURPOSE
44
 
%%
45
 
%%   Handle an HTTP request.
46
 
%%
47
 
%% RETURNS
48
 
%%
49
 
%%   Page to be sent back to the client and/or HTTP status code.
50
 
%%
51
 
%% ARGUMENTS
52
 
%%
53
 
%% - LocalPath: part of the requested URL path that is "local to the
54
 
%%   module".
55
 
%%
56
 
%%-----------------------------------------------------------------------
57
 
 
58
 
 
 
75
-record(state, {host, docroot, accesslog, accesslogfd, directory_indices,
 
76
                default_content_type, content_types = []}).
 
77
 
 
78
-define(PROCNAME, ejabberd_mod_http_fileserver).
 
79
 
 
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"}).
 
83
 
 
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"}]).
 
96
 
 
97
-compile(export_all).
 
98
 
 
99
%%====================================================================
 
100
%% gen_mod callbacks
 
101
%%====================================================================
 
102
 
 
103
start(Host, Opts) ->
 
104
    Proc = get_proc_name(Host),
 
105
    ChildSpec =
 
106
        {Proc,
 
107
         {?MODULE, start_link, [Host, Opts]},
 
108
         transient, % if process crashes abruptly, it gets restarted
 
109
         1000,
 
110
         worker,
 
111
         [?MODULE]},
 
112
    supervisor:start_child(ejabberd_sup, ChildSpec).
 
113
 
 
114
stop(Host) ->
 
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).
 
119
 
 
120
%%====================================================================
 
121
%% API
 
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], []).
 
130
 
 
131
%%====================================================================
 
132
%% gen_server callbacks
 
133
%%====================================================================
 
134
%%--------------------------------------------------------------------
 
135
%% Function: init(Args) -> {ok, State} |
 
136
%%                         {ok, State, Timeout} |
 
137
%%                         ignore               |
 
138
%%                         {stop, Reason}
 
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,
 
148
                        docroot = DocRoot,
 
149
                        directory_indices = DirectoryIndices,
 
150
                        default_content_type = DefaultContentType,
 
151
                        content_types = ContentTypes}}
 
152
    catch
 
153
        throw:Reason ->
 
154
            {stop, Reason}
 
155
    end.
 
156
 
 
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}.
 
172
 
 
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].
 
184
 
 
185
check_docroot_defined(DocRoot, Host) ->
 
186
    case DocRoot of
 
187
        undefined -> throw({undefined_docroot_option, Host});
 
188
        _ -> ok
 
189
    end.
 
190
 
 
191
check_docroot_exists(DocRoot) ->
 
192
    case file:read_file_info(DocRoot) of
 
193
        {error, Reason} -> throw({error_access_docroot, DocRoot, Reason});
 
194
        {ok, FI} -> FI
 
195
    end.
 
196
 
 
197
check_docroot_is_dir(DRInfo, DocRoot) ->
 
198
    case DRInfo#file_info.type of
 
199
        directory -> ok;
 
200
        _ -> throw({docroot_not_directory, DocRoot})
 
201
    end.
 
202
 
 
203
check_docroot_is_readable(DRInfo, DocRoot) ->
 
204
    case DRInfo#file_info.access of
 
205
        read -> ok;
 
206
        read_write -> ok;
 
207
        _ -> throw({docroot_not_readable, DocRoot})
 
208
    end.
 
209
 
 
210
try_open_log(undefined, _Host) ->
 
211
    undefined;
 
212
try_open_log(FN, Host) ->
 
213
    FD = try open_log(FN) of
 
214
             FD1 -> FD1
 
215
         catch
 
216
             throw:{cannot_open_accesslog, FN, Reason} ->
 
217
                 ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]),
 
218
                 undefined
 
219
         end,
 
220
    ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50),
 
221
    FD.
 
222
 
 
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) ->
 
237
    {reply, ok, State}.
 
238
 
 
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),
 
247
    {noreply, State};
 
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) ->
 
252
    {noreply, State}.
 
253
 
 
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) ->
 
261
    {noreply, State}.
 
262
 
 
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),
 
273
    ok.
 
274
 
 
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) ->
 
280
    {ok, State}.
 
281
 
 
282
%%====================================================================
 
283
%% request_handlers callbacks
 
284
%%====================================================================
 
285
 
 
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]),
61
 
 
62
 
    Result = serve(LocalPath),
63
 
    case ets:lookup(mod_http_fileserver, accessfile) of
64
 
        [] ->
65
 
            ok;
66
 
        [{accessfile, AccessFile}] ->
67
 
            {Code, _, _} = Result,
68
 
            log(AccessFile, Code, Request)
69
 
    end,
70
 
    Result.
71
 
 
72
 
serve(LocalPath) ->
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}
 
296
    catch
 
297
        exit:{noproc, _} -> 
 
298
            ejabberd_web:error(not_found)
 
299
    end.
 
300
 
 
301
serve(LocalPath, DocRoot, DirectoryIndices, DefaultContentType, ContentTypes) ->
74
302
    FileName = filename:join(filename:split(DocRoot) ++ LocalPath),
75
 
    case file:read_file(FileName) of
76
 
        {ok, FileContents} ->
77
 
            ?DEBUG("Delivering content.", []),
78
 
            {200,
79
 
             [{"Server", "ejabberd"},
80
 
              {"Last-Modified", last_modified(FileName)},
81
 
              {"Content-Type", content_type(FileName)}],
82
 
             FileContents};
83
 
        {error, Error} ->
84
 
            ?DEBUG("Delivering error: ~p", [Error]),
85
 
            case Error of
86
 
                eacces -> {403, [], "Forbidden"};
87
 
                enoent -> {404, [], "Not found"};
88
 
                _Else -> {404, [], atom_to_list(Error)}
89
 
            end
90
 
    end.
91
 
 
92
 
ctl_process(_Val, ["reopen-weblog"]) ->
93
 
    mod_http_fileserver_server ! reopenlog,
94
 
    ?STATUS_SUCCESS;
95
 
ctl_process(Val, _Args) ->
96
 
        Val.
97
 
 
98
 
%%%----------------------------------------------------------------------
99
 
%%% UTILITIES
100
 
%%%----------------------------------------------------------------------
101
 
 
102
 
join([], _) ->
103
 
    "";
104
 
join([E], _) ->
105
 
    E;
106
 
join([H | T], Separator) ->
107
 
    lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H, T).
108
 
 
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,
 
307
                                                          DirectoryIndices,
 
308
                                                          DefaultContentType,
 
309
                                                          ContentTypes);
 
310
        {ok, FileInfo}                     -> serve_file(FileInfo, FileName,
 
311
                                                         DefaultContentType,
 
312
                                                         ContentTypes)
 
313
    end.
 
314
 
 
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)
 
325
    end.
 
326
 
 
327
%% Assume the file exists if we got this far and attempt to read it in
 
328
%% and serve it up.
 
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}],
 
337
     FileContents}.
 
338
 
 
339
%%----------------------------------------------------------------------
 
340
%% Log file
 
341
%%----------------------------------------------------------------------
 
342
 
 
343
open_log(FN) ->
 
344
    case file:open(FN, [append]) of
 
345
        {ok, FD} ->
 
346
            FD;
 
347
        {error, Reason} ->
 
348
            throw({cannot_open_accesslog, FN, Reason})
 
349
    end.
 
350
 
 
351
close_log(FD) ->
 
352
    file:close(FD).
 
353
 
 
354
reopen_log(undefined, undefined) ->
 
355
    ok;
 
356
reopen_log(FN, FD) ->
 
357
    close_log(FD),
 
358
    open_log(FN).
 
359
 
 
360
reopen_log(Host) ->
 
361
    gen_server:cast(get_proc_name(Host), reopen_log).
 
362
 
 
363
add_to_log(FileSize, Code, Request) ->
 
364
    gen_server:cast(get_proc_name(Request#request.host),
 
365
                    {add_to_log, FileSize, Code, Request}).
 
366
 
 
367
add_to_log(undefined, _FileSize, _Code, _Request) ->
 
368
    ok;
 
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
115
 
                []�->
 
375
                [] ->
116
376
                    "";
117
377
                String ->
118
378
                    [$? | String]
119
379
            end,
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]).
125
 
 
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"
140
 
    end.
141
 
 
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]).
 
393
 
 
394
find_header(Header, Headers, Default) ->
 
395
    case lists:keysearch(Header, 1, Headers) of
 
396
        {value, {_, Value}} -> Value;
 
397
        false               -> Default
 
398
    end.
 
399
 
 
400
%%----------------------------------------------------------------------
 
401
%% Utilities
 
402
%%----------------------------------------------------------------------
 
403
 
 
404
get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
 
405
 
 
406
join([], _) ->
 
407
    "";
 
408
join([E], _) ->
 
409
    E;
 
410
join([H | T], Separator) ->
 
411
    lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H, T).
 
412
 
 
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
 
418
    end.
 
419
 
 
420
last_modified(FileInfo) ->
144
421
    Then = FileInfo#file_info.mtime,
145
422
    httpd_util:rfc1123_date(Then).
146
423
 
147
 
open_file(Filename) ->
148
 
    case file:open(Filename, [append]) of
149
 
        {ok, File} ->
150
 
            ets:insert(mod_http_fileserver, {accessfile, File}),
151
 
            ok;
152
 
        {error, _Reason} ->
153
 
            {'EXIT', {unaccessible_accessfile, ?MODULE}}
154
 
    end.
155
 
 
156
 
loop(Filename) ->
157
 
    receive
158
 
        reopenlog ->
159
 
            case ets:lookup(mod_http_fileserver, accessfile) of
160
 
                [] ->
161
 
                    ok;
162
 
                [{accessfile, AccessFile}] ->
163
 
                    file:close(AccessFile),
164
 
                    case open_file(Filename) of
165
 
                        ok ->
166
 
                            ok;
167
 
                        _ ->
168
 
                            error
169
 
                    end
170
 
            end,
171
 
            loop(Filename);
172
 
        stop ->
173
 
            ok
174
 
    end.
175
 
 
176
 
 
177
 
%%%----------------------------------------------------------------------
178
 
%%% BEHAVIOUR CALLBACKS
179
 
%%%----------------------------------------------------------------------
180
 
 
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
185
 
        undefined ->
186
 
            start2(_Host, Opts);
187
 
        _ ->
188
 
            ok
189
 
    end.
190
 
 
191
 
start2(_Host, Opts) ->
192
 
    case gen_mod:get_opt(docroot, Opts, undefined) of
193
 
        undefined ->
194
 
            {'EXIT', {missing_document_root, ?MODULE}};
195
 
        DocRoot ->
196
 
            case filelib:is_dir(DocRoot) of
197
 
                true ->
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
203
 
                        undefined ->
204
 
                            ok;
205
 
                        Filename ->
206
 
                            %% XXX same remark as above for proc name
207
 
                            ejabberd_ctl:register_commands(
208
 
                              [{"reopen-weblog",
209
 
                                "reopen http fileserver log file"}],
210
 
                              ?MODULE, ctl_process),
211
 
                            register(mod_http_fileserver_server,
212
 
                                     spawn(?MODULE, loop, [Filename])),
213
 
                            open_file(Filename)
214
 
                    end;
215
 
                _Else ->
216
 
                    {'EXIT', {unaccessible_document_root, ?MODULE}}
217
 
            end
218
 
    end.
219
 
 
220
 
stop(_Host) ->
221
 
    case ets:info(mod_http_fileserver, name) of
222
 
        undefined ->
223
 
            ok;
224
 
        _ ->
225
 
            case ets:lookup(mod_http_fileserver, accessfile) of
226
 
                [] ->
227
 
                    ok;
228
 
                [{accessfile, AccessFile}] ->
229
 
                    ejabberd_ctl:unregister_commands(
230
 
                      [{"reopen-weblog",
231
 
                        "reopen http fileserver log file"}],
232
 
                      ?MODULE, ctl_process),
233
 
                    mod_http_fileserver_server ! stop,
234
 
                    file:close(AccessFile)
235
 
            end,
236
 
            ets:delete(mod_http_fileserver)
237
 
    end,
238
 
    ok.
 
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, ":"))).