• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1296

19 Jan 2026 11:25AM UTC coverage: 33.562% (+0.09%) from 33.468%
1296

push

github

badlop
mod_conversejs: Cosmetic change: sort paths alphabetically

0 of 4 new or added lines in 1 file covered. (0.0%)

11245 existing lines in 174 files now uncovered.

15580 of 46421 relevant lines covered (33.56%)

1074.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

6.06
/src/mod_http_fileserver.erl
1
%%%-------------------------------------------------------------------
2
%%% File    : mod_http_fileserver.erl
3
%%% Author  : Massimiliano Mirra <mmirra [at] process-one [dot] net>
4
%%% Purpose : Simple file server plugin for embedded ejabberd web server
5
%%% Created :
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2026   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 along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(mod_http_fileserver).
27

28
-author('mmirra@process-one.net').
29

30
-behaviour(gen_mod).
31
-behaviour(gen_server).
32

33
%% gen_mod callbacks
34
-export([start/2, stop/1, reload/3]).
35

36
%% gen_server callbacks
37
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
38
         terminate/2, code_change/3]).
39

40
%% request_handlers callbacks
41
-export([process/2]).
42

43
%% utility for other http modules
44
-export([content_type/3, build_list_content_types/1]).
45

46
-export([reopen_log/0, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
47

48
-export([web_menu_system/3]).
49

50
-include_lib("xmpp/include/xmpp.hrl").
51
-include("logger.hrl").
52
-include("ejabberd_http.hrl").
53
-include("ejabberd_web_admin.hrl").
54
-include_lib("kernel/include/file.hrl").
55
-include("translate.hrl").
56

57
-record(state,
58
        {host, docroot, accesslog, accesslogfd,
59
         directory_indices, custom_headers, default_content_type,
60
         content_types = [], user_access = none}).
61

62
%% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data}
63
-define(HTTP_ERR_FILE_NOT_FOUND,
64
        {-1, 404, [], <<"Not found">>}).
65

66
-define(REQUEST_AUTH_HEADERS,
67
        [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]).
68

69
-define(HTTP_ERR_FORBIDDEN,
70
        {-1, 403, [], <<"Forbidden">>}).
71
-define(HTTP_ERR_REQUEST_AUTH,
72
        {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}).
73
-define(HTTP_ERR_HOST_UNKNOWN,
74
        {-1, 410, [], <<"Host unknown">>}).
75

76
-define(DEFAULT_CONTENT_TYPES,
77
        [{<<".avi">>, <<"video/avi">>},
78
         {<<".bmp">>, <<"image/bmp">>},
79
         {<<".bz2">>, <<"application/x-bzip2">>},
80
         {<<".css">>, <<"text/css">>},
81
         {<<".gif">>, <<"image/gif">>},
82
         {<<".gz">>, <<"application/x-gzip">>},
83
         {<<".html">>, <<"text/html">>},
84
         {<<".ico">>, <<"image/vnd.microsoft.icon">>},
85
         {<<".jar">>, <<"application/java-archive">>},
86
         {<<".jpeg">>, <<"image/jpeg">>},
87
         {<<".jpg">>, <<"image/jpeg">>},
88
         {<<".js">>, <<"text/javascript">>},
89
         {<<".json">>, <<"application/json">>},
90
         {<<".m4a">>, <<"audio/mp4">>},
91
         {<<".map">>, <<"application/json">>},
92
         {<<".mp3">>, <<"audio/mpeg">>},
93
         {<<".mp4">>, <<"video/mp4">>},
94
         {<<".mpeg">>, <<"video/mpeg">>},
95
         {<<".mpg">>, <<"video/mpeg">>},
96
         {<<".ogg">>, <<"application/ogg">>},
97
         {<<".pdf">>, <<"application/pdf">>},
98
         {<<".png">>, <<"image/png">>},
99
         {<<".rtf">>, <<"application/rtf">>},
100
         {<<".svg">>, <<"image/svg+xml">>},
101
         {<<".tiff">>, <<"image/tiff">>},
102
         {<<".ttf">>, <<"font/ttf">>},
103
         {<<".txt">>, <<"text/plain">>},
104
         {<<".wav">>, <<"audio/wav">>},
105
         {<<".webp">>, <<"image/webp">>},
106
         {<<".woff">>, <<"font/woff">>},
107
         {<<".woff2">>, <<"font/woff2">>},
108
         {<<".xml">>, <<"application/xml">>},
109
         {<<".xpi">>, <<"application/x-xpinstall">>},
110
         {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>},
111
         {<<".xz">>, <<"application/x-xz">>},
112
         {<<".zip">>, <<"application/zip">>}]).
113

114
%%====================================================================
115
%% gen_mod callbacks
116
%%====================================================================
117

118
start(Host, Opts) ->
UNCOV
119
    ejabberd_hooks:add(webadmin_menu_system_post, global, ?MODULE, web_menu_system, 1000-$f),
×
UNCOV
120
    gen_mod:start_child(?MODULE, Host, Opts).
×
121

122
stop(Host) ->
UNCOV
123
    ejabberd_hooks:delete(webadmin_menu_system_post, global, ?MODULE, web_menu_system, 1000-$f),
×
UNCOV
124
    gen_mod:stop_child(?MODULE, Host).
×
125

126
reload(Host, NewOpts, OldOpts) ->
UNCOV
127
    Proc = get_proc_name(Host),
×
UNCOV
128
    gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}).
×
129

130
depends(_Host, _Opts) ->
UNCOV
131
    [].
×
132

133
%%====================================================================
134
%% gen_server callbacks
135
%%====================================================================
136
%%--------------------------------------------------------------------
137
%% Function: init(Args) -> {ok, State} |
138
%%                         {ok, State, Timeout} |
139
%%                         ignore               |
140
%%                         {stop, Reason}
141
%% Description: Initiates the server
142
%%--------------------------------------------------------------------
143
init([Host|_]) ->
UNCOV
144
    Opts = gen_mod:get_module_opts(Host, ?MODULE),
×
145
    try initialize(Host, Opts) of
×
146
        State ->
UNCOV
147
            process_flag(trap_exit, true),
×
UNCOV
148
            {ok, State}
×
149
    catch
150
        throw:Reason ->
151
            {stop, Reason}
×
152
    end.
153

154
initialize(Host, Opts) ->
155
    DocRoot = mod_http_fileserver_opt:docroot(Opts),
×
156
    AccessLog = mod_http_fileserver_opt:accesslog(Opts),
×
157
    AccessLogFD = try_open_log(AccessLog, Host),
×
UNCOV
158
    DirectoryIndices = mod_http_fileserver_opt:directory_indices(Opts),
×
159
    CustomHeaders = mod_http_fileserver_opt:custom_headers(Opts),
×
UNCOV
160
    DefaultContentType = mod_http_fileserver_opt:default_content_type(Opts),
×
161
    UserAccess0 = mod_http_fileserver_opt:must_authenticate_with(Opts),
×
UNCOV
162
    UserAccess = case UserAccess0 of
×
163
                     [] -> none;
×
164
                     _ ->
165
                         maps:from_list(UserAccess0)
×
166
                 end,
UNCOV
167
    ContentTypes = build_list_content_types(
×
168
                     mod_http_fileserver_opt:content_types(Opts)),
UNCOV
169
    ?DEBUG("Known content types: ~ts",
×
UNCOV
170
           [str:join([[$*, K, " -> ", V] || {K, V} <- ContentTypes],
×
UNCOV
171
                     <<", ">>)]),
×
UNCOV
172
    #state{host = Host,
×
173
           accesslog = AccessLog,
174
           accesslogfd = AccessLogFD,
175
           docroot = DocRoot,
176
           directory_indices = DirectoryIndices,
177
           custom_headers = CustomHeaders,
178
           default_content_type = DefaultContentType,
179
           content_types = ContentTypes,
180
           user_access = UserAccess}.
181

182
build_list_content_types(AdminCTs) ->
UNCOV
183
    build_list_content_types(AdminCTs, ?DEFAULT_CONTENT_TYPES).
3✔
184

185
-spec build_list_content_types(AdminCTs::[{binary(), binary()|undefined}],
186
                               Default::[{binary(), binary()|undefined}]) ->
187
    [{string(), string()|undefined}].
188
%% where CT = {Extension::string(), Value}
189
%%       Value = string() | undefined
190
%% @doc Return a unified list without duplicates.
191
%% Elements of AdminCTs have more priority.
192
%% If a CT is declared as 'undefined', then it is not included in the result.
193
build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) ->
UNCOV
194
    AdminCTs = lists:ukeysort(1, AdminCTsUnsorted),
3✔
UNCOV
195
    DefaultCTs = lists:ukeysort(1, DefaultCTsUnsorted),
3✔
UNCOV
196
    CTsUnfiltered = lists:ukeymerge(1, AdminCTs,
3✔
197
                                    DefaultCTs),
UNCOV
198
    [{Extension, Value}
3✔
199
     || {Extension, Value} <- CTsUnfiltered,
3✔
200
        Value /= undefined].
108✔
201

202
try_open_log(undefined, _Host) ->
203
    undefined;
×
204
try_open_log(FN, _Host) ->
UNCOV
205
    FD = try open_log(FN) of
×
206
             FD1 -> FD1
×
207
         catch
208
             throw:{cannot_open_accesslog, FN, Reason} ->
UNCOV
209
                 ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]),
×
UNCOV
210
                 undefined
×
211
         end,
UNCOV
212
    ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50),
×
UNCOV
213
    FD.
×
214

215
%%--------------------------------------------------------------------
216
%% Function: handle_call(Request, From, State) -> {reply, Reply, State} |
217
%%                                      {reply, Reply, State, Timeout} |
218
%%                                      {noreply, State} |
219
%%                                      {noreply, State, Timeout} |
220
%%                                      {stop, Reason, Reply, State} |
221
%%                                      {stop, Reason, State}
222
%% Description: Handling call messages
223
%%--------------------------------------------------------------------
224
handle_call({serve, RawPath, LocalPath, Auth, RHeaders}, _From, State) ->
225
    IfModifiedSince = case find_header('If-Modified-Since', RHeaders, bad_date) of
×
226
                          bad_date ->
UNCOV
227
                              bad_date;
×
228
                          Val ->
UNCOV
229
                              httpd_util:convert_request_date(binary_to_list(Val))
×
230
                      end,
UNCOV
231
    DocRootBased = pick_docroot_based(RawPath, State#state.docroot),
×
232
    Reply = serve(LocalPath, Auth, DocRootBased, State#state.directory_indices,
×
233
                  State#state.custom_headers,
234
                  State#state.default_content_type, State#state.content_types,
235
                  State#state.user_access, IfModifiedSince),
236
    {reply, Reply, State};
×
237
handle_call(Request, From, State) ->
UNCOV
238
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
×
UNCOV
239
    {noreply, State}.
×
240

241
pick_docroot_based(RawPath, DocRootList) when is_list(DocRootList) ->
242
    [{_, PathDir} | _] = lists:dropwhile(fun({Dr, _PathDir}) ->
×
UNCOV
243
                                     nomatch == binary:match(RawPath, Dr)
×
244
                             end,
245
                             DocRootList),
UNCOV
246
    PathDir;
×
247
pick_docroot_based(_RawPath, DocRoot) ->
UNCOV
248
    DocRoot.
×
249

250
%%--------------------------------------------------------------------
251
%% Function: handle_cast(Msg, State) -> {noreply, State} |
252
%%                                      {noreply, State, Timeout} |
253
%%                                      {stop, Reason, State}
254
%% Description: Handling cast messages
255
%%--------------------------------------------------------------------
256
handle_cast({add_to_log, FileSize, Code, Request}, State) ->
257
    add_to_log(State#state.accesslogfd, FileSize, Code, Request),
×
UNCOV
258
    {noreply, State};
×
259
handle_cast(reopen_log, State) ->
260
    FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd),
×
UNCOV
261
    {noreply, State#state{accesslogfd = FD2}};
×
262
handle_cast({reload, Host, NewOpts, _OldOpts}, OldState) ->
UNCOV
263
    try initialize(Host, NewOpts) of
×
264
        NewState ->
265
            FD = reopen_log(NewState#state.accesslog, OldState#state.accesslogfd),
×
266
            {noreply, NewState#state{accesslogfd = FD}}
×
267
    catch throw:_ ->
UNCOV
268
            {noreply, OldState}
×
269
    end;
270
handle_cast(Msg, State) ->
UNCOV
271
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
×
UNCOV
272
    {noreply, State}.
×
273

274
%%--------------------------------------------------------------------
275
%% Function: handle_info(Info, State) -> {noreply, State} |
276
%%                                       {noreply, State, Timeout} |
277
%%                                       {stop, Reason, State}
278
%% Description: Handling all non call/cast messages
279
%%--------------------------------------------------------------------
280
handle_info(Info, State) ->
UNCOV
281
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
UNCOV
282
    {noreply, State}.
×
283

284
%%--------------------------------------------------------------------
285
%% Function: terminate(Reason, State) -> void()
286
%% Description: This function is called by a gen_server when it is about to
287
%% terminate. It should be the opposite of Module:init/1 and do any necessary
288
%% cleaning up. When it returns, the gen_server terminates with Reason.
289
%% The return value is ignored.
290
%%--------------------------------------------------------------------
291
terminate(_Reason, #state{host = Host} = State) ->
UNCOV
292
    close_log(State#state.accesslogfd),
×
UNCOV
293
    case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
×
294
        false ->
UNCOV
295
            ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50);
×
296
        true ->
UNCOV
297
            ok
×
298
    end.
299

300
%%--------------------------------------------------------------------
301
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
302
%% Description: Convert process state when code is changed
303
%%--------------------------------------------------------------------
304
code_change(_OldVsn, State, _Extra) ->
UNCOV
305
    {ok, State}.
×
306

307
%%====================================================================
308
%% request_handlers callbacks
309
%%====================================================================
310

311
-spec process(LocalPath::[binary()], #request{}) ->
312
    {HTTPCode::integer(), [{binary(), binary()}], Page::string()}.
313
%% @doc Handle an HTTP request.
314
%% LocalPath is the part of the requested URL path that is "local to the module".
315
%% Returns the page to be sent back to the client and/or HTTP status code.
316
process(LocalPath, #request{host = Host, auth = Auth, headers = RHeaders, raw_path = RawPath} = Request) ->
317
    ?DEBUG("Requested ~p", [LocalPath]),
×
318
    try
×
UNCOV
319
        VHost = ejabberd_router:host_of_route(Host),
×
320
        {FileSize, Code, Headers, Contents} =
×
321
            gen_server:call(get_proc_name(VHost),
322
                            {serve, RawPath, LocalPath, Auth, RHeaders}),
323
        add_to_log(FileSize, Code, Request#request{host = VHost}),
×
324
        {Code, Headers, Contents}
×
325
    catch _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route ->
UNCOV
326
            ?DEBUG("Received an HTTP request with Host: ~ts, "
×
327
                   "but couldn't find the related "
UNCOV
328
                   "ejabberd virtual host", [Host]),
×
UNCOV
329
            {FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN,
×
330
            add_to_log(FileSize1, Code1, Request#request{host = ejabberd_config:get_myname()}),
×
331
            {Code1, Headers1, Contents1}
×
332
    end.
333

334
serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType,
335
    ContentTypes, UserAccess, IfModifiedSince) ->
UNCOV
336
    CanProceed = case {UserAccess, Auth} of
×
UNCOV
337
                     {none, _} -> true;
×
338
                     {_, {User, Pass}} ->
UNCOV
339
                         case maps:find(User, UserAccess) of
×
340
                             {ok, Pass} -> true;
×
UNCOV
341
                             _ -> false
×
342
                         end;
343
                     _ ->
344
                         false
×
345
                 end,
UNCOV
346
    case CanProceed of
×
347
        false ->
UNCOV
348
            ?HTTP_ERR_REQUEST_AUTH;
×
349
        true ->
UNCOV
350
            FileName = filename:join(filename:split(DocRoot) ++ LocalPath),
×
351
            case file:read_file_info(FileName) of
×
352
                {error, enoent} ->
UNCOV
353
                    ?HTTP_ERR_FILE_NOT_FOUND;
×
354
                {error, enotdir} ->
UNCOV
355
                    ?HTTP_ERR_FILE_NOT_FOUND;
×
356
                {error, eacces} ->
UNCOV
357
                    ?HTTP_ERR_FORBIDDEN;
×
358
                {ok, #file_info{type = directory}} -> serve_index(FileName,
×
359
                                                                  DirectoryIndices,
360
                                                                  CustomHeaders,
361
                                                                  DefaultContentType,
362
                                                                  ContentTypes);
363
                {ok, #file_info{mtime = MTime} = FileInfo} ->
UNCOV
364
                    case calendar:local_time_to_universal_time_dst(MTime) of
×
365
                        [IfModifiedSince | _] ->
UNCOV
366
                            serve_not_modified(FileInfo, FileName,
×
367
                                               CustomHeaders);
368
                        _ ->
UNCOV
369
                            serve_file(FileInfo, FileName,
×
370
                                       CustomHeaders,
371
                                       DefaultContentType,
372
                                       ContentTypes)
373
                    end
374
            end
375
    end.
376

377
%% Troll through the directory indices attempting to find one which
378
%% works, if none can be found, return a 404.
379
serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) ->
380
    ?HTTP_ERR_FILE_NOT_FOUND;
×
381
serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) ->
UNCOV
382
    IndexFileName = filename:join([FileName] ++ [Index]),
×
UNCOV
383
    case file:read_file_info(IndexFileName) of
×
384
        {error, _Error}                    -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes);
×
385
        {ok, #file_info{type = directory}} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes);
×
UNCOV
386
        {ok, FileInfo}                     -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes)
×
387
    end.
388

389
serve_not_modified(FileInfo, FileName, CustomHeaders) ->
UNCOV
390
    ?DEBUG("Delivering not modified: ~ts", [FileName]),
×
UNCOV
391
    {0, 304,
×
392
     ejabberd_http:apply_custom_headers(
393
         [{<<"Server">>, <<"ejabberd">>},
394
          {<<"Last-Modified">>, last_modified(FileInfo)}],
395
         CustomHeaders), <<>>}.
396

397
%% Assume the file exists if we got this far and attempt to read it in
398
%% and serve it up.
399
serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) ->
UNCOV
400
    ?DEBUG("Delivering: ~ts", [FileName]),
×
UNCOV
401
    ContentType = content_type(FileName, DefaultContentType,
×
402
                               ContentTypes),
UNCOV
403
    {FileInfo#file_info.size, 200,
×
404
     ejabberd_http:apply_custom_headers(
405
         [{<<"Server">>, <<"ejabberd">>},
406
          {<<"Last-Modified">>, last_modified(FileInfo)},
407
          {<<"Content-Type">>, ContentType}],
408
         CustomHeaders),
409
     {file, FileName}}.
410

411
%%----------------------------------------------------------------------
412
%% Log file
413
%%----------------------------------------------------------------------
414

415
open_log(FN) ->
UNCOV
416
    case file:open(FN, [append]) of
×
417
        {ok, FD} ->
418
            FD;
×
419
        {error, Reason} ->
UNCOV
420
            throw({cannot_open_accesslog, FN, Reason})
×
421
    end.
422

423
close_log(FD) ->
424
    file:close(FD).
×
425

426
reopen_log(undefined, undefined) ->
427
    ok;
×
428
reopen_log(FN, FD) ->
429
    close_log(FD),
×
UNCOV
430
    open_log(FN).
×
431

432
reopen_log() ->
433
    lists:foreach(
×
434
      fun(Host) ->
UNCOV
435
              gen_server:cast(get_proc_name(Host), reopen_log)
×
436
      end, ejabberd_option:hosts()).
437

438
add_to_log(FileSize, Code, Request) ->
439
    gen_server:cast(get_proc_name(Request#request.host),
×
440
                    {add_to_log, FileSize, Code, Request}).
441

442
add_to_log(undefined, _FileSize, _Code, _Request) ->
UNCOV
443
    ok;
×
444
add_to_log(File, FileSize, Code, Request) ->
UNCOV
445
    {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
×
446
    IP = ip_to_string(element(1, Request#request.ip)),
×
UNCOV
447
    Path = join(Request#request.path, "/"),
×
448
    Query = case stringify_query(Request#request.q) of
×
449
                <<"">> ->
UNCOV
450
                    "";
×
451
                String ->
UNCOV
452
                    [$? | String]
×
453
            end,
UNCOV
454
    UserAgent = find_header('User-Agent', Request#request.headers, "-"),
×
UNCOV
455
    Referer = find_header('Referer', Request#request.headers, "-"),
×
456
    %% Pseudo Combined Apache log format:
457
    %% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung"
458
    %% TODO some fields are hardcoded/missing:
459
    %%   The date/time integers should have always 2 digits. For example day "7" should be "07"
460
    %%   Month should be 3*letter, not integer 1..12
461
    %%   Missing time zone = (`+' | `-') 4*digit
462
    %%   Missing protocol version: HTTP/1.1
463
    %% For reference: http://httpd.apache.org/docs/2.2/logs.html
UNCOV
464
    io:format(File, "~ts - - [~p/~p/~p:~p:~p:~p] \"~ts /~ts~ts\" ~p ~p ~p ~p~n",
×
465
              [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code,
466
               FileSize, Referer, UserAgent]).
467

468
stringify_query(Q) ->
469
    stringify_query(Q, []).
×
470
stringify_query([], Res) ->
UNCOV
471
    join(lists:reverse(Res), "&");
×
472
stringify_query([{nokey, _B} | Q], Res) ->
473
    stringify_query(Q, Res);
×
474
stringify_query([{A, B} | Q], Res) ->
UNCOV
475
    stringify_query(Q, [join([A,B], "=") | Res]).
×
476

477
find_header(Header, Headers, Default) ->
UNCOV
478
    case lists:keysearch(Header, 1, Headers) of
×
UNCOV
479
      {value, {_, Value}} -> Value;
×
UNCOV
480
      false -> Default
×
481
    end.
482

483
%%----------------------------------------------------------------------
484
%% Utilities
485
%%----------------------------------------------------------------------
486

UNCOV
487
get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE).
×
488

489
join([], _) ->
490
    <<"">>;
×
491
join([E], _) ->
UNCOV
492
    E;
×
493
join([H | T], Separator) ->
UNCOV
494
    [H2 | T2] = case is_binary(H) of true -> [binary_to_list(I)||I<-[H|T]]; false -> [H | T] end,
×
UNCOV
495
    Res=lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H2, T2),
×
496
    case is_binary(H) of true -> list_to_binary(Res); false -> Res end.
×
497

498
content_type(Filename, DefaultContentType, ContentTypes) ->
UNCOV
499
    Extension = str:to_lower(filename:extension(Filename)),
3✔
500
    case lists:keysearch(Extension, 1, ContentTypes) of
3✔
501
      {value, {_, ContentType}} -> ContentType;
3✔
UNCOV
502
      false -> DefaultContentType
×
503
    end.
504

505
last_modified(FileInfo) ->
506
    Then = FileInfo#file_info.mtime,
×
UNCOV
507
    httpd_util:rfc1123_date(Then).
×
508

509
%% Convert IP address tuple to string representation. Accepts either
510
%% IPv4 or IPv6 address tuples.
511
ip_to_string(Address) when size(Address) == 4 ->
UNCOV
512
    join(tuple_to_list(Address), ".");
×
513
ip_to_string(Address) when size(Address) == 8 ->
UNCOV
514
    Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)),
×
UNCOV
515
    string:to_lower(lists:flatten(join(Parts, ":"))).
×
516

517
%%----------------------------------------------------------------------
518
%% WebAdmin
519
%%----------------------------------------------------------------------
520

521
web_menu_system(Result, _Request, _Level) ->
522
    Els = ejabberd_web_admin:make_menu_system(?MODULE, "📁", "Fileserver: {URLPATH}", ""),
×
UNCOV
523
    Els ++ Result.
×
524

525
%%----------------------------------------------------------------------
526

527
mod_opt_type(accesslog) ->
528
    econf:file(write);
×
529
mod_opt_type(content_types) ->
530
    econf:map(econf:binary(), econf:binary());
×
531
mod_opt_type(custom_headers) ->
532
    econf:map(econf:binary(), econf:binary());
×
533
mod_opt_type(default_content_type) ->
UNCOV
534
    econf:binary();
×
535
mod_opt_type(directory_indices) ->
UNCOV
536
    econf:list(econf:binary());
×
537
mod_opt_type(docroot) ->
UNCOV
538
    econf:either(
×
539
      econf:directory(write),
540
      econf:map(econf:binary(), econf:binary())
541
    );
542
mod_opt_type(must_authenticate_with) ->
UNCOV
543
    econf:list(
×
544
      econf:and_then(
545
        econf:and_then(
546
          econf:binary("^[^:]+:[^:]+$"),
547
          econf:binary_sep(":")),
UNCOV
548
        fun([K, V]) -> {K, V} end)).
×
549

550
-spec mod_options(binary()) -> [{must_authenticate_with, [{binary(), binary()}]} |
551
                                {atom(), any()}].
552
mod_options(_) ->
UNCOV
553
    [{accesslog, undefined},
×
554
     {content_types, []},
555
     {default_content_type, <<"application/octet-stream">>},
556
     {custom_headers, []},
557
     {directory_indices, []},
558
     {must_authenticate_with, []},
559
     %% Required option
560
     docroot].
561

562
mod_doc() ->
UNCOV
563
    #{desc =>
×
564
          ?T("This simple module serves files from the local disk over HTTP."),
565
      note => "improved 'docroot' in 26.xx",
566
      opts =>
567
          [{accesslog,
568
            #{value => ?T("Path"),
569
              desc =>
570
                  ?T("File to log accesses using an Apache-like format. "
571
                     "No log will be recorded if this option is not specified.")}},
572
           {docroot,
573
            #{value => ?T("PathDir | {PathURL, PathDir}"),
574
              note => "improved in 26.xx",
575
              desc =>
576
                  ?T("Directory to serve the files from, "
577
                     "or a map with several URL path "
578
                     "(as specified in _`listen-options.md#request_handlers|request_handlers`_) "
579
                     "and their corresponding directory. "
580
                     "This is a mandatory option."),
581
              example =>
582
                   ["listen:",
583
                   "  -",
584
                   "    port: 5280",
585
                   "    module: ejabberd_http",
586
                   "    request_handlers:",
587
                   "      /pub/content: mod_http_fileserver",
588
                   "      /share: mod_http_fileserver",
589
                   "      /: mod_http_fileserver",
590
                   "modules:",
591
                   "  mod_http_fileserver:",
592
                   "    docroot:",
593
                   "      /pub/content: /var/service/www",
594
                   "      /share: /usr/share/javascript",
595
                   "      /: /var/www"]}},
596
           {content_types,
597
            #{value => "{Extension: Type}",
598
              desc =>
599
                  ?T("Specify mappings of extension to content type. "
600
                     "There are several content types already defined. "
601
                     "With this option you can add new definitions "
602
                     "or modify existing ones. The default values are:"),
603
              example =>
604
                  ["content_types:"|
UNCOV
605
                     ["  " ++ binary_to_list(E) ++ ": " ++ binary_to_list(T)
×
UNCOV
606
                      || {E, T} <- ?DEFAULT_CONTENT_TYPES]]}},
×
607
           {default_content_type,
608
            #{value => ?T("Type"),
609
              desc =>
610
                  ?T("Specify the content type to use for unknown extensions. "
611
                     "The default value is 'application/octet-stream'.")}},
612
           {custom_headers,
613
            #{value => "{Name: Value}",
614
              desc =>
615
                  ?T("Indicate custom HTTP headers to be included in all responses. "
616
                     "There are no custom headers by default.")}},
617
           {directory_indices,
618
            #{value => "[Index, ...]",
619
              desc =>
620
                  ?T("Indicate one or more directory index files, "
621
                     "similarly to Apache's 'DirectoryIndex' variable. "
622
                     "When an HTTP request hits a directory instead of a "
623
                     "regular file, those directory indices are looked in order, "
624
                     "and the first one found is returned. "
625
                     "The default value is an empty list.")}},
626
           {must_authenticate_with,
627
            #{value => ?T("[{Username, Hostname}, ...]"),
628
              desc =>
629
                  ?T("List of accounts that are allowed to use this service. "
630
                     "Default value: '[]'.")}}],
631
      example =>
632
          [{?T("This example configuration will serve the files from the "
633
               "local directory '/var/www' in the address "
634
               "'http://example.org:5280/pub/content/'. In this example a new "
635
               "content type 'ogg' is defined, 'png' is redefined, and 'jpg' "
636
               "definition is deleted:"),
637
           ["listen:",
638
           "  -",
639
           "    port: 5280",
640
           "    module: ejabberd_http",
641
           "    request_handlers:",
642
           "      /pub/content: mod_http_fileserver",
643
           "",
644
           "modules:",
645
           "  mod_http_fileserver:",
646
           "    docroot: /var/www",
647
           "    accesslog: /var/log/ejabberd/access.log",
648
           "    directory_indices:",
649
           "      - index.html",
650
           "      - main.htm",
651
           "    custom_headers:",
652
           "      X-Powered-By: Erlang/OTP",
653
           "      X-Fry: \"It's a widely-believed fact!\"",
654
           "    content_types:",
655
           "      .ogg: audio/ogg",
656
           "      .png: image/png",
657
           "    default_content_type: text/html"]}]}.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc