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

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

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

52.16
/src/ejabberd_http.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : ejabberd_http.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose :
5
%%% Created : 27 Feb 2004 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2025   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(ejabberd_http).
27
-behaviour(ejabberd_listener).
28

29
-author('alexey@process-one.net').
30

31
%% External exports
32
-export([start/3, start_link/3,
33
         accept/1, receive_headers/1, recv_file/2,
34
         listen_opt_type/1, listen_options/0,
35
         apply_custom_headers/2]).
36
-export([get_url/4, get_auto_url/2, get_auto_urls/2, find_handler_port_path/2]).
37
-export([init/3]).
38

39
-deprecate({get_auto_url, 2}).
40

41
-include("logger.hrl").
42
-include_lib("xmpp/include/xmpp.hrl").
43
-include("ejabberd_http.hrl").
44

45
-include_lib("kernel/include/file.hrl").
46

47
-record(state, {sockmod,
48
                socket,
49
                request_method,
50
                request_version,
51
                request_path,
52
                request_auth,
53
                request_keepalive,
54
                request_content_length = 0,
55
                request_lang = <<"en">>,
56
                %% XXX bard: request handlers are configured in
57
                %% ejabberd.cfg under the HTTP service.         For example,
58
                %% to have the module test_web handle requests with
59
                %% paths starting with "/test/module":
60
                %%
61
                %%   {5280, ejabberd_http,    [http_bind, web_admin,
62
                %%                               {request_handlers, [{["test", "module"], mod_test_web}]}]}
63
                %%
64
                request_handlers = [],
65
                request_host,
66
                request_port,
67
                request_tp,
68
                request_headers = [],
69
                end_of_request = false,
70
                options = [],
71
                custom_headers,
72
                trail = <<>>,
73
                allow_unencrypted_sasl2,
74
                addr_re,
75
                sock_peer_name = none
76
               }).
77

78
-define(XHTML_DOCTYPE,
79
        <<"<?xml version='1.0'?>\n<!DOCTYPE html "
80
          "PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//"
81
          "EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1"
82
          "-transitional.dtd\">\n">>).
83

84
-define(HTML_DOCTYPE,
85
        <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD "
86
          "XHTML 1.0 Transitional//EN\" \"http://www.w3."
87
          "org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
88
          "">>).
89

90
-define(RECV_BUF, 65536).
91
-define(SEND_BUF, 65536).
92
-define(MAX_POST_SIZE, 20971520). %% 20Mb
93

94
start(SockMod, Socket, Opts) ->
95
    {ok,
×
96
     proc_lib:spawn(ejabberd_http, init,
97
                    [SockMod, Socket, Opts])}.
98

99
start_link(SockMod, Socket, Opts) ->
100
    {ok,
10✔
101
     proc_lib:spawn_link(ejabberd_http, init,
102
                         [SockMod, Socket, Opts])}.
103

104
init(SockMod, Socket, Opts) ->
105
    TLSEnabled = proplists:get_bool(tls, Opts),
10✔
106
    TLSOpts1 = lists:filter(fun ({ciphers, _}) -> true;
10✔
107
                                ({dhfile, _}) -> true;
×
108
                                ({cafile, _}) -> true;
×
109
                                ({protocol_options, _}) -> true;
×
110
                                (_) -> false
10✔
111
                            end,
112
                            Opts),
113
    TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of
10✔
114
                   false -> [compression_none | TLSOpts1];
10✔
115
                   true -> TLSOpts1
×
116
               end,
117
    TLSOpts3 = case ejabberd_pkix:get_certfile(
10✔
118
                      ejabberd_config:get_myname()) of
119
                   error -> TLSOpts2;
×
120
                   {ok, CertFile} -> [{certfile, CertFile}|TLSOpts2]
10✔
121
               end,
122
    TLSOpts = [verify_none | TLSOpts3],
10✔
123
    {SockMod1, Socket1} = if TLSEnabled ->
10✔
124
                                 inet:setopts(Socket, [{recbuf, ?RECV_BUF}]),
×
125
                                 {ok, TLSSocket} = fast_tls:tcp_to_tls(Socket,
×
126
                                                                  TLSOpts),
127
                                 {fast_tls, TLSSocket};
×
128
                             true -> {SockMod, Socket}
10✔
129
                          end,
130
    SockPeer =  proplists:get_value(sock_peer_name, Opts, none),
10✔
131
    RequestHandlers0 = proplists:get_value(request_handlers, Opts, []),
10✔
132
    RequestHandlers = ejabberd_hooks:run_fold(http_request_handlers_init,
10✔
133
                                              RequestHandlers0,
134
                                              [Opts]),
135
    ?DEBUG("S: ~p~n", [RequestHandlers]),
10✔
136

137
    {ok, RE} = re:compile(<<"^(?:\\[(.*?)\\]|(.*?))(?::(\\d+))?$">>),
10✔
138

139
    CustomHeaders = proplists:get_value(custom_headers, Opts, []),
10✔
140

141
    AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts),
10✔
142
    State = #state{sockmod = SockMod1,
10✔
143
                   socket = Socket1,
144
                   custom_headers = CustomHeaders,
145
                   options = Opts,
146
                   allow_unencrypted_sasl2 = AllowUnencryptedSasl2,
147
                   request_handlers = RequestHandlers,
148
                   sock_peer_name = SockPeer,
149
                   addr_re = RE},
150
    try receive_headers(State) of
10✔
151
        V -> V
×
152
    catch
153
        {error, _} -> State
×
154
    end.
155

156
accept(_Pid) ->
157
    ok.
10✔
158

159
send_text(_State, none) ->
160
    ok;
3✔
161
send_text(State, Text) ->
162
    case (State#state.sockmod):send(State#state.socket, Text) of
83✔
163
        ok -> ok;
83✔
164
        {error, timeout} ->
165
            ?INFO_MSG("Timeout on ~p:send", [State#state.sockmod]),
×
166
            exit(normal);
×
167
        Error ->
168
            ?DEBUG("Error in ~p:send: ~p",
×
169
                   [State#state.sockmod, Error]),
×
170
            exit(normal)
×
171
    end.
172

173
send_file(State, Fd, Size, FileName) ->
174
    try
3✔
175
        case State#state.sockmod of
3✔
176
            gen_tcp ->
177
                {ok, _} = file:sendfile(Fd, State#state.socket, 0, Size, []),
3✔
178
                ok;
3✔
179
            _ ->
180
                case file:read(Fd, ?SEND_BUF) of
×
181
                    {ok, Data} ->
182
                        send_text(State, Data),
×
183
                        send_file(State, Fd, Size, FileName);
×
184
                    eof ->
185
                        ok
×
186
                end
187
        end
188
    catch _:{case_clause, {error, Why}} ->
189
            if Why /= closed ->
×
190
                    ?WARNING_MSG("Failed to read ~ts: ~ts",
×
191
                                 [FileName, file_format_error(Why)]),
×
192
                    exit(normal);
×
193
               true ->
194
                    ok
×
195
            end
196
    end.
197

198
receive_headers(#state{trail = Trail} = State) ->
199
    SockMod = State#state.sockmod,
93✔
200
    Socket = State#state.socket,
93✔
201
    Data = SockMod:recv(Socket, 0, 300000),
93✔
202
    case Data of
83✔
203
        {error, closed} when State#state.request_method == undefined ->
204
            % socket closed without receiving anything in it
205
            ok;
×
206
        {error, Error} ->
207
            ?DEBUG("Error when retrieving http headers ~p: ~p",
×
208
                   [State#state.sockmod, Error]),
×
209
            ok;
×
210
        {ok, D} ->
211
            parse_headers(State#state{trail = <<Trail/binary, D/binary>>})
83✔
212
    end.
213

214
parse_headers(#state{trail = <<>>} = State) ->
215
    receive_headers(State);
83✔
216
parse_headers(#state{request_method = Method,
217
                     trail = Data} =
218
                  State) ->
219
    PktType = case Method of
520✔
220
                undefined -> http_bin;
83✔
221
                _ -> httph_bin
437✔
222
              end,
223
    case erlang:decode_packet(PktType, Data, []) of
520✔
224
        {ok, Pkt, Rest} ->
225
            NewState = process_header(State#state{trail = Rest}, {ok, Pkt}),
520✔
226
            case NewState#state.end_of_request of
520✔
227
                true -> ok;
×
228
                _ -> parse_headers(NewState)
520✔
229
            end;
230
        {more, _} ->
231
            receive_headers(State#state{trail = Data});
×
232
        _ ->
233
            ok
×
234
    end.
235

236
process_header(State, Data) ->
237
    SockMod = State#state.sockmod,
520✔
238
    Socket = State#state.socket,
520✔
239
    case Data of
520✔
240
      {ok, {http_request, Method, Uri, Version}} ->
241
          KeepAlive = case Version of
83✔
242
                        {1, 1} -> true;
83✔
243
                        _ -> false
×
244
                      end,
245
          Path = case Uri of
83✔
246
                   {absoluteURI, _Scheme, _Host, _Port, P} ->
247
                       {abs_path, P};
×
248
                   {abs_path, P} ->
249
                       {abs_path, P};
83✔
250
                   _ -> Uri
×
251
                 end,
252
          State#state{request_method = Method,
83✔
253
                      request_version = Version, request_path = Path,
254
                      request_keepalive = KeepAlive};
255
      {ok, {http_header, _, 'Connection' = Name, _, Conn}} ->
256
          KeepAlive1 = case misc:tolower(Conn) of
83✔
257
                         <<"keep-alive">> -> true;
83✔
258
                         <<"close">> -> false;
×
259
                         _ -> State#state.request_keepalive
×
260
                       end,
261
          State#state{request_keepalive = KeepAlive1,
83✔
262
                      request_headers = add_header(Name, Conn, State)};
263
      {ok,
264
       {http_header, _, 'Authorization' = Name, _, Auth}} ->
265
          State#state{request_auth = parse_auth(Auth),
40✔
266
                      request_headers = add_header(Name, Auth, State)};
267
      {ok,
268
       {http_header, _, 'Content-Length' = Name, _, SLen}} ->
269
          case catch binary_to_integer(SLen) of
83✔
270
            Len when is_integer(Len) ->
271
                State#state{request_content_length = Len,
83✔
272
                            request_headers = add_header(Name, SLen, State)};
273
            _ -> State
×
274
          end;
275
      {ok,
276
       {http_header, _, 'Accept-Language' = Name, _, Langs}} ->
277
          State#state{request_lang = parse_lang(Langs),
×
278
                      request_headers = add_header(Name, Langs, State)};
279
      {ok, {http_header, _, 'Host' = Name, _, Value}} ->
280
          {Host, Port, TP} = get_transfer_protocol(State#state.addr_re, SockMod, Value),
83✔
281
          State#state{request_host = ejabberd_config:resolve_host_alias(Host),
83✔
282
                      request_port = Port,
283
                      request_tp = TP,
284
                      request_headers = add_header(Name, Value, State)};
285
      {ok, {http_header, _, Name, _, Value}} when is_binary(Name) ->
286
          State#state{request_headers =
9✔
287
                          add_header(normalize_header_name(Name), Value, State)};
288
      {ok, {http_header, _, Name, _, Value}} ->
289
          State#state{request_headers =
56✔
290
                          add_header(Name, Value, State)};
291
      {ok, http_eoh} when State#state.request_host == undefined;
292
                          State#state.request_host == error ->
293
            {State1, Out} = process_request(State),
×
294
            send_text(State1, Out),
×
295
            process_header(State, {ok, {http_error, <<>>}});
×
296
      {ok, http_eoh} ->
297
            ?DEBUG("(~w) http query: ~w ~p~n",
83✔
298
                   [State#state.socket, State#state.request_method,
299
                    element(2, State#state.request_path)]),
83✔
300
            {State3, Out} = process_request(State),
83✔
301
            send_text(State3, Out),
83✔
302
            case State3#state.request_keepalive of
83✔
303
                true ->
304
                    #state{sockmod = SockMod, socket = Socket,
83✔
305
                           trail = State3#state.trail,
306
                           options = State#state.options,
307
                           custom_headers = State#state.custom_headers,
308
                           request_handlers = State#state.request_handlers,
309
                           addr_re = State#state.addr_re};
310
                _ ->
311
                    #state{end_of_request = true,
×
312
                           trail = State3#state.trail,
313
                           options = State#state.options,
314
                           custom_headers = State#state.custom_headers,
315
                           request_handlers = State#state.request_handlers,
316
                           addr_re = State#state.addr_re}
317
            end;
318
      _ ->
319
          #state{end_of_request = true,
×
320
                 options = State#state.options,
321
                 custom_headers = State#state.custom_headers,
322
                 request_handlers = State#state.request_handlers,
323
                 addr_re = State#state.addr_re}
324
    end.
325

326
add_header(Name, Value, State)->
327
    [{Name, Value} | State#state.request_headers].
354✔
328

329
get_transfer_protocol(RE, SockMod, HostPort) ->
330
    {Proto, DefPort} = case SockMod of
83✔
331
                           gen_tcp -> {http, 80};
83✔
332
                           fast_tls -> {https, 443}
×
333
                       end,
334
    {Host, Port} = case re:run(HostPort, RE, [{capture,[1,2,3],binary}]) of
83✔
335
                       nomatch ->
336
                           {error, DefPort};
×
337
                       {match, [<<>>, H, <<>>]} ->
338
                           {jid:nameprep(H), DefPort};
×
339
                       {match, [H, <<>>, <<>>]} ->
340
                           {jid:nameprep(H), DefPort};
×
341
                       {match, [<<>>, H, PortStr]} ->
342
                           {jid:nameprep(H), binary_to_integer(PortStr)};
83✔
343
                       {match, [H, <<>>, PortStr]} ->
344
                           {jid:nameprep(H), binary_to_integer(PortStr)}
×
345
                   end,
346

347
    {Host, Port, Proto}.
83✔
348

349
%% XXX bard: search through request handlers looking for one that
350
%% matches the requested URL path, and pass control to it.  If none is
351
%% found, answer with HTTP 404.
352

353
process([], _) -> ejabberd_web:error(not_found);
×
354
process(Handlers, Request) ->
355
    {HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} =
124✔
356
        case Handlers of
357
            [{Pfx, Mod} | Tail] ->
358
                {Pfx, Mod, [], Tail};
124✔
359
            [{Pfx, Mod, Opts} | Tail] ->
360
                {Pfx, Mod, Opts, Tail}
×
361
        end,
362

363
    case (lists:prefix(HandlerPathPrefix, Request#request.path) or
124✔
364
         (HandlerPathPrefix==Request#request.path)) of
365
        true ->
366
            ?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]),
83✔
367
            %% LocalPath is the path "local to the handler", i.e. if
368
            %% the handler was registered to handle "/test/" and the
369
            %% requested path is "/test/foo/bar", the local path is
370
            %% ["foo", "bar"]
371
            LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path),
83✔
372
            R = case erlang:function_exported(HandlerModule, socket_handoff, 3) of
83✔
373
                    true ->
374
                        HandlerModule:socket_handoff(
×
375
                          LocalPath, Request, HandlerOpts);
376
                    false ->
377
                        try
83✔
378
                            HandlerModule:process(LocalPath, Request)
83✔
379
                        catch
380
                            Class:Reason:Stack ->
381
                                ?ERROR_MSG(
×
382
                                  "HTTP handler crashed: ~s",
383
                                  [misc:format_exception(2, Class, Reason, Stack)]),
×
384
                                erlang:raise(Class, Reason, Stack)
×
385
                        end
386
                end,
387
            ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]),
83✔
388
            R;
83✔
389
        false ->
390
            process(HandlersLeft, Request)
41✔
391
    end.
392

393
extract_path_query(#state{request_method = Method,
394
                          request_path = {abs_path, Path}} = State)
395
    when Method =:= 'GET' orelse
396
           Method =:= 'HEAD' orelse
397
             Method =:= 'DELETE' orelse Method =:= 'OPTIONS' ->
398
    case catch url_decode_q_split_normalize(Path) of
27✔
399
        {'EXIT', Error} ->
400
            ?DEBUG("Error decoding URL '~p': ~p", [Path, Error]),
×
401
            {State, false};
×
402
        {LPath, Query} ->
403
            LQuery = case catch parse_urlencoded(Query) of
27✔
404
                         {'EXIT', _Reason} -> [];
×
405
                         LQ -> LQ
27✔
406
                     end,
407
            {State, {LPath, LQuery, <<"">>, Path}}
27✔
408
    end;
409
extract_path_query(#state{request_method = Method,
410
                          request_path = {abs_path, Path},
411
                          request_content_length = Len,
412
                          trail = Trail,
413
                          sockmod = _SockMod,
414
                          socket = _Socket} = State)
415
  when (Method =:= 'POST' orelse Method =:= 'PUT') andalso Len>0 ->
416
    case catch url_decode_q_split_normalize(Path) of
56✔
417
        {'EXIT', Error} ->
418
            ?DEBUG("Error decoding URL '~p': ~p", [Path, Error]),
×
419
            {State, false};
×
420
        {LPath, _Query} ->
421
            case Method of
56✔
422
                'PUT' ->
423
                    {State, {LPath, [], Trail, Path}};
3✔
424
                'POST' ->
425
                    case recv_data(State) of
53✔
426
                        {ok, Data} ->
427
                            LQuery = case catch parse_urlencoded(Data) of
53✔
428
                                         {'EXIT', _Reason} -> [];
9✔
429
                                         LQ -> LQ
44✔
430
                                     end,
431
                            {State, {LPath, LQuery, Data, Path}};
53✔
432
                        error ->
433
                            {State, false}
×
434
                    end
435
            end
436
    end;
437
extract_path_query(State) ->
438
    {State, false}.
×
439

440
process_request(#state{request_host = undefined,
441
                       custom_headers = CustomHeaders} = State) ->
442
    {State, make_text_output(State, 400, CustomHeaders,
×
443
                             <<"Missing Host header">>)};
444
process_request(#state{request_host = error,
445
                       custom_headers = CustomHeaders} = State) ->
446
    {State, make_text_output(State, 400, CustomHeaders,
×
447
                             <<"Malformed Host header">>)};
448
process_request(#state{request_method = Method,
449
                       request_auth = Auth,
450
                       request_lang = Lang,
451
                       request_version = Version,
452
                       sockmod = SockMod,
453
                       socket = Socket,
454
                       sock_peer_name = SockPeer,
455
                       options = Options,
456
                       request_host = Host,
457
                       request_port = Port,
458
                       request_tp = TP,
459
                       request_content_length = Length,
460
                       request_headers = RequestHeaders,
461
                       request_handlers = RequestHandlers,
462
                       custom_headers = CustomHeaders} = State) ->
463
    case proplists:get_value(<<"Expect">>, RequestHeaders, <<>>) of
83✔
464
        <<"100-", _/binary>> when Version == {1, 1} ->
465
            send_text(State, <<"HTTP/1.1 100 Continue\r\n\r\n">>);
×
466
        _ ->
467
            ok
83✔
468
    end,
469
    case extract_path_query(State) of
83✔
470
        {State2, false} ->
471
            {State2, make_bad_request(State)};
×
472
        {State2, {LPath, LQuery, Data, RawPath}} ->
473
            PeerName = case SockPeer of
83✔
474
                           none ->
475
                               case SockMod of
83✔
476
                                   gen_tcp ->
477
                                       inet:peername(Socket);
83✔
478
                                   _ ->
479
                                       SockMod:peername(Socket)
×
480
                               end;
481
                           {_, Peer} ->
482
                               {ok, Peer}
×
483
                       end,
484
            IPHere = case PeerName of
83✔
485
                         {ok, V} -> V;
83✔
486
                         {error, _} = E -> throw(E)
×
487
                     end,
488
            XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []),
83✔
489
            IP = analyze_ip_xff(IPHere, XFF),
83✔
490
            Request = #request{method = Method,
83✔
491
                               path = LPath,
492
                               raw_path = RawPath,
493
                               q = LQuery,
494
                               auth = Auth,
495
                               length = Length,
496
                               sockmod = SockMod,
497
                               socket = Socket,
498
                               data = Data,
499
                               lang = Lang,
500
                               host = Host,
501
                               port = Port,
502
                               tp = TP,
503
                               opts = Options,
504
                               headers = RequestHeaders,
505
                               ip = IP},
506
            RequestHandlers1 = ejabberd_hooks:run_fold(
83✔
507
                                http_request_handlers, RequestHandlers, [Host, Request]),
508
            Res = case process(RequestHandlers1, Request) of
83✔
509
                      El when is_record(El, xmlel) ->
510
                          make_xhtml_output(State, 200, CustomHeaders, El);
×
511
                      {Status, Headers, El}
512
                        when is_record(El, xmlel) ->
513
                          make_xhtml_output(State, Status,
48✔
514
                                            apply_custom_headers(Headers, CustomHeaders), El);
515
                      Output when is_binary(Output) or is_list(Output) ->
516
                          make_text_output(State, 200, CustomHeaders, Output);
×
517
                      {Status, Headers, Output}
518
                        when is_binary(Output) or is_list(Output) ->
519
                          make_text_output(State, Status,
32✔
520
                                           apply_custom_headers(Headers, CustomHeaders), Output);
521
                      {Status, Headers, {file, FileName}} ->
522
                          make_file_output(State, Status, Headers, FileName);
3✔
523
                      {Status, Reason, Headers, Output}
524
                        when is_binary(Output) or is_list(Output) ->
525
                          make_text_output(State, Status, Reason,
×
526
                                           apply_custom_headers(Headers, CustomHeaders), Output);
527
                      _ ->
528
                          none
×
529
                  end,
530
            {State2#state{trail = <<>>}, Res}
83✔
531
    end.
532

533
make_bad_request(State) ->
534
    make_xhtml_output(State, 400, State#state.custom_headers,
×
535
                      ejabberd_web:make_xhtml([#xmlel{name = <<"h1">>,
536
                                                      attrs = [],
537
                                                      children =
538
                                                          [{xmlcdata,
539
                                                            <<"400 Bad Request">>}]}])).
540

541
analyze_ip_xff(IP, []) -> IP;
83✔
542
analyze_ip_xff({IPLast, Port}, XFF) ->
543
    [ClientIP | ProxiesIPs] = str:tokens(XFF, <<", ">>) ++
×
544
                                [misc:ip_to_list(IPLast)],
545
    TrustedProxies = ejabberd_option:trusted_proxies(),
×
546
    IPClient = case is_ipchain_trusted(ProxiesIPs,
×
547
                                       TrustedProxies)
548
                   of
549
                 true ->
550
                     case inet_parse:address(binary_to_list(ClientIP)) of
×
551
                         {ok, IPFirst} ->
552
                             ?DEBUG("The IP ~w was replaced with ~w due to "
×
553
                                    "header X-Forwarded-For: ~ts",
554
                                    [IPLast, IPFirst, XFF]),
×
555
                             IPFirst;
×
556
                         E -> throw(E)
×
557
                     end;
558
                 false -> IPLast
×
559
               end,
560
    {IPClient, Port}.
×
561

562
is_ipchain_trusted([], _) -> false;
×
563
is_ipchain_trusted(_UserIPs, all) -> true;
×
564
is_ipchain_trusted(UserIPs, Masks) ->
565
    lists:all(
×
566
        fun(IP) ->
567
            case inet:parse_address(binary_to_list(IP)) of
×
568
                {ok, IP2} ->
569
                    lists:any(
×
570
                        fun({Mask, MaskLen}) ->
571
                                misc:match_ip_mask(IP2, Mask, MaskLen)
×
572
                        end, Masks);
573
                _ ->
574
                    false
×
575
            end
576
        end, UserIPs).
577

578
recv_data(#state{request_content_length = Len}) when Len >= ?MAX_POST_SIZE ->
579
    error;
×
580
recv_data(#state{request_content_length = Len, trail = Trail,
581
                 sockmod = SockMod, socket = Socket}) ->
582
    NewLen = Len - byte_size(Trail),
53✔
583
    if NewLen > 0 ->
53✔
584
            case SockMod:recv(Socket, NewLen, 60000) of
×
585
                {ok, Data} -> {ok, <<Trail/binary, Data/binary>>};
×
586
                {error, _} -> error
×
587
            end;
588
       true ->
589
            {ok, Trail}
53✔
590
    end.
591

592
recv_file(#request{length = Len, data = Trail,
593
                   sockmod = SockMod, socket = Socket}, Path) ->
594
    case file:open(Path, [write, exclusive, raw]) of
3✔
595
        {ok, Fd} ->
596
            Res = case file:write(Fd, Trail) of
3✔
597
                      ok ->
598
                          NewLen = max(0, Len - byte_size(Trail)),
3✔
599
                          do_recv_file(NewLen, SockMod, Socket, Fd);
3✔
600
                      {error, _} = Err ->
601
                          Err
×
602
                  end,
603
            file:close(Fd),
3✔
604
            case Res of
3✔
605
                ok -> ok;
3✔
606
                {error, _} -> file:delete(Path)
×
607
            end,
608
            Res;
3✔
609
        {error, _} = Err ->
610
            Err
×
611
    end.
612

613
do_recv_file(0, _SockMod, _Socket, _Fd) ->
614
    ok;
3✔
615
do_recv_file(Len, SockMod, Socket, Fd) ->
616
    ChunkLen = min(Len, ?RECV_BUF),
×
617
    case SockMod:recv(Socket, ChunkLen, timer:seconds(30)) of
×
618
        {ok, Data} ->
619
            case file:write(Fd, Data) of
×
620
                ok ->
621
                    do_recv_file(Len-size(Data), SockMod, Socket, Fd);
×
622
                {error, _} = Err ->
623
                    Err
×
624
            end;
625
        {error, _} ->
626
            {error, closed}
×
627
    end.
628

629
make_headers(State, Status, Reason, Headers, Data) ->
630
    Len = if is_integer(Data) -> Data;
83✔
631
             true -> iolist_size(Data)
80✔
632
          end,
633
    Headers1 = [{<<"Content-Length">>, integer_to_binary(Len)} | Headers],
83✔
634
    Headers2 = case lists:keyfind(<<"Content-Type">>, 1, Headers) of
83✔
635
                   {_, _} ->
636
                       Headers1;
35✔
637
                   false ->
638
                       [{<<"Content-Type">>, <<"text/html; charset=utf-8">>}
48✔
639
                        | Headers1]
640
               end,
641
    HeadersOut = case {State#state.request_version,
83✔
642
                       State#state.request_keepalive} of
643
                     {{1, 1}, true} -> Headers2;
83✔
644
                     {_, true} ->
645
                         [{<<"Connection">>, <<"keep-alive">>} | Headers2];
×
646
                     {_, false} ->
647
                         [{<<"Connection">>, <<"close">>} | Headers2]
×
648
                 end,
649
    Version = case State#state.request_version of
83✔
650
                  {1, 1} -> <<"HTTP/1.1 ">>;
83✔
651
                  _ -> <<"HTTP/1.0 ">>
×
652
              end,
653
    H = [[Attr, <<": ">>, Val, <<"\r\n">>] || {Attr, Val} <- HeadersOut],
83✔
654
    NewReason = case Reason of
83✔
655
                  <<"">> -> code_to_phrase(Status);
83✔
656
                  _ -> Reason
×
657
                end,
658
    SL = [Version,
83✔
659
          integer_to_binary(Status), <<" ">>,
660
          NewReason, <<"\r\n">>],
661
    [SL, H, <<"\r\n">>].
83✔
662

663
make_xhtml_output(State, Status, Headers, XHTML) ->
664
    Data = case State#state.request_method of
48✔
665
               'HEAD' -> <<"">>;
×
666
               _ ->
667
                   DocType = case lists:member(html, Headers) of
48✔
668
                                 true -> ?HTML_DOCTYPE;
40✔
669
                                 false -> ?XHTML_DOCTYPE
8✔
670
                             end,
671
                   iolist_to_binary([DocType, fxml:element_to_binary(XHTML)])
48✔
672
           end,
673
    EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Data),
48✔
674
    [EncodedHdrs, Data].
48✔
675

676
make_text_output(State, Status, Headers, Text) ->
677
    make_text_output(State, Status, <<"">>, Headers, Text).
32✔
678

679
make_text_output(State, Status, Reason, Headers, Text) ->
680
    Data = iolist_to_binary(Text),
32✔
681
    Data2 = case State#state.request_method of
32✔
682
                'HEAD' -> <<"">>;
×
683
                _ -> Data
32✔
684
            end,
685
    EncodedHdrs = make_headers(State, Status, Reason, Headers, Data2),
32✔
686
    [EncodedHdrs, Data2].
32✔
687

688
make_file_output(State, Status, Headers, FileName) ->
689
    case file:read_file_info(FileName) of
3✔
690
        {ok, #file_info{size = Size}} when State#state.request_method == 'HEAD' ->
691
            make_headers(State, Status, <<"">>, Headers, Size);
×
692
        {ok, #file_info{size = Size}} ->
693
            case file:open(FileName, [raw, read]) of
3✔
694
                {ok, Fd} ->
695
                    EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Size),
3✔
696
                    send_text(State, EncodedHdrs),
3✔
697
                    send_file(State, Fd, Size, FileName),
3✔
698
                    file:close(Fd),
3✔
699
                    none;
3✔
700
                {error, Why} ->
701
                    Reason = file_format_error(Why),
×
702
                    ?ERROR_MSG("Failed to open ~ts: ~ts", [FileName, Reason]),
×
703
                    make_text_output(State, 404, Reason, [], <<>>)
×
704
            end;
705
        {error, Why} ->
706
            Reason = file_format_error(Why),
×
707
            ?ERROR_MSG("Failed to read info of ~ts: ~ts", [FileName, Reason]),
×
708
            make_text_output(State, 404, Reason, [], <<>>)
×
709
    end.
710

711
parse_lang(Langs) ->
712
    case str:tokens(Langs, <<",; ">>) of
×
713
      [First | _] -> First;
×
714
      [] -> <<"en">>
×
715
    end.
716

717
file_format_error(Reason) ->
718
    case file:format_error(Reason) of
×
719
        "unknown POSIX error" -> atom_to_list(Reason);
×
720
        Text -> Text
×
721
    end.
722

723
url_decode_q_split_normalize(Path) ->
724
    {NPath, Query} = url_decode_q_split(Path),
83✔
725
    LPath = normalize_path([NPE
83✔
726
                    || NPE <- str:tokens(uri_string:percent_decode(NPath), <<"/">>)]),
83✔
727
    {LPath, Query}.
83✔
728

729
% Code below is taken (with some modifications) from the yaws webserver, which
730
% is distributed under the following license:
731
%
732
% This software (the yaws webserver) is free software.
733
% Parts of this software is Copyright (c) Claes Wikstrom <klacke@hyber.org>
734
% Any use or misuse of the source code is hereby freely allowed.
735
%
736
% 1. Redistributions of source code must retain the above copyright
737
%    notice as well as this list of conditions.
738
%
739
% 2. Redistributions in binary form must reproduce the above copyright
740
%    notice as well as this list of conditions.
741

742
%% @doc Split the URL and return {Path, QueryPart}
743
url_decode_q_split(Path) ->
744
    url_decode_q_split(Path, <<>>).
83✔
745

746
url_decode_q_split(<<$?, T/binary>>, Acc) ->
747
    %% Don't decode the query string here, that is parsed separately.
748
    {path_norm_reverse(Acc), T};
×
749
url_decode_q_split(<<H, T/binary>>, Acc) when H /= 0 ->
750
    url_decode_q_split(T, <<H, Acc/binary>>);
4,003✔
751
url_decode_q_split(<<>>, Ack) ->
752
    {path_norm_reverse(Ack), <<>>}.
83✔
753

754
path_norm_reverse(<<"/", T/binary>>) -> start_dir(0, <<"/">>, T);
48✔
755
path_norm_reverse(T) -> start_dir(0, <<"">>, T).
35✔
756

757
start_dir(N, Path, <<"..">>) -> rest_dir(N, Path, <<"">>);
×
758
start_dir(N, Path, <<"/", T/binary>>) -> start_dir(N, Path, T);
×
759
start_dir(N, Path, <<"./", T/binary>>) -> start_dir(N, Path, T);
×
760
start_dir(N, Path, <<"../", T/binary>>) ->
761
    start_dir(N + 1, Path, T);
×
762
start_dir(N, Path, T) -> rest_dir(N, Path, T).
333✔
763

764
rest_dir(_N, Path, <<>>) ->
765
    case Path of
83✔
766
      <<>> -> <<"/">>;
×
767
      _ -> Path
83✔
768
    end;
769
rest_dir(0, Path, <<$/, T/binary>>) ->
770
    start_dir(0, <<$/, Path/binary>>, T);
250✔
771
rest_dir(N, Path, <<$/, T/binary>>) ->
772
    start_dir(N - 1, Path, T);
×
773
rest_dir(0, Path, <<H, T/binary>>) ->
774
    rest_dir(0, <<H, Path/binary>>, T);
3,705✔
775
rest_dir(N, Path, <<_H, T/binary>>) -> rest_dir(N, Path, T).
×
776

777
code_to_phrase(100) -> <<"Continue">>;
×
778
code_to_phrase(101) -> <<"Switching Protocols ">>;
×
779
code_to_phrase(200) -> <<"OK">>;
72✔
780
code_to_phrase(201) -> <<"Created">>;
3✔
781
code_to_phrase(202) -> <<"Accepted">>;
×
782
code_to_phrase(203) ->
783
    <<"Non-Authoritative Information">>;
×
784
code_to_phrase(204) -> <<"No Content">>;
×
785
code_to_phrase(205) -> <<"Reset Content">>;
×
786
code_to_phrase(206) -> <<"Partial Content">>;
×
787
code_to_phrase(300) -> <<"Multiple Choices">>;
×
788
code_to_phrase(301) -> <<"Moved Permanently">>;
×
789
code_to_phrase(302) -> <<"Found">>;
×
790
code_to_phrase(303) -> <<"See Other">>;
×
791
code_to_phrase(304) -> <<"Not Modified">>;
×
792
code_to_phrase(305) -> <<"Use Proxy">>;
×
793
code_to_phrase(306) -> <<"(Unused)">>;
×
794
code_to_phrase(307) -> <<"Temporary Redirect">>;
×
795
code_to_phrase(400) -> <<"Bad Request">>;
×
796
code_to_phrase(401) -> <<"Unauthorized">>;
8✔
797
code_to_phrase(402) -> <<"Payment Required">>;
×
798
code_to_phrase(403) -> <<"Forbidden">>;
×
799
code_to_phrase(404) -> <<"Not Found">>;
×
800
code_to_phrase(405) -> <<"Method Not Allowed">>;
×
801
code_to_phrase(406) -> <<"Not Acceptable">>;
×
802
code_to_phrase(407) ->
803
    <<"Proxy Authentication Required">>;
×
804
code_to_phrase(408) -> <<"Request Timeout">>;
×
805
code_to_phrase(409) -> <<"Conflict">>;
×
806
code_to_phrase(410) -> <<"Gone">>;
×
807
code_to_phrase(411) -> <<"Length Required">>;
×
808
code_to_phrase(412) -> <<"Precondition Failed">>;
×
809
code_to_phrase(413) -> <<"Request Entity Too Large">>;
×
810
code_to_phrase(414) -> <<"Request-URI Too Long">>;
×
811
code_to_phrase(415) -> <<"Unsupported Media Type">>;
×
812
code_to_phrase(416) ->
813
    <<"Requested Range Not Satisfiable">>;
×
814
code_to_phrase(417) -> <<"Expectation Failed">>;
×
815
code_to_phrase(500) -> <<"Internal Server Error">>;
×
816
code_to_phrase(501) -> <<"Not Implemented">>;
×
817
code_to_phrase(502) -> <<"Bad Gateway">>;
×
818
code_to_phrase(503) -> <<"Service Unavailable">>;
×
819
code_to_phrase(504) -> <<"Gateway Timeout">>;
×
820
code_to_phrase(505) -> <<"HTTP Version Not Supported">>.
×
821

822
-spec parse_auth(binary()) -> {binary(), binary()} | {oauth, binary(), []} | invalid.
823
parse_auth(<<"Basic ", Auth64/binary>>) ->
824
    try base64:decode(Auth64) of
40✔
825
        Auth ->
826
            case binary:split(Auth, <<":">>) of
40✔
827
                [User, Pass] ->
828
                    PassUtf8 = unicode:characters_to_binary(Pass, utf8),
40✔
829
                    {User, PassUtf8};
40✔
830
                _ ->
831
                    invalid
×
832
            end
833
    catch _:_ ->
834
        invalid
×
835
    end;
836
parse_auth(<<"Bearer ", SToken/binary>>) ->
837
    Token = str:strip(SToken),
×
838
    {oauth, Token, []};
×
839
parse_auth(<<_/binary>>) ->
840
    invalid.
×
841

842
parse_urlencoded(S) ->
843
    parse_urlencoded(S, nokey, <<>>, key).
80✔
844

845
parse_urlencoded(<<$%, Hi, Lo, Tail/binary>>, Last, Cur,
846
                 State) ->
847
    Hex = list_to_integer([Hi, Lo], 16),
561✔
848
    parse_urlencoded(Tail, Last, <<Cur/binary, Hex>>, State);
552✔
849
parse_urlencoded(<<$&, Tail/binary>>, _Last, Cur, key) ->
850
    [{Cur, <<"">>} | parse_urlencoded(Tail,
8✔
851
                                      nokey, <<>>,
852
                                      key)];  %% cont keymode
853
parse_urlencoded(<<$&, Tail/binary>>, Last, Cur, value) ->
854
    V = {Last, Cur},
24✔
855
    [V | parse_urlencoded(Tail, nokey, <<>>, key)];
24✔
856
parse_urlencoded(<<$+, Tail/binary>>, Last, Cur, State) ->
857
    parse_urlencoded(Tail, Last, <<Cur/binary, $\s>>, State);
8✔
858
parse_urlencoded(<<$=, Tail/binary>>, _Last, Cur, key) ->
859
    parse_urlencoded(Tail, Cur, <<>>,
48✔
860
                     value); %% change mode
861
parse_urlencoded(<<H, Tail/binary>>, Last, Cur, State) ->
862
    parse_urlencoded(Tail, Last, <<Cur/binary, H>>, State);
2,772✔
863
parse_urlencoded(<<>>, Last, Cur, _State) ->
864
    [{Last, Cur}];
71✔
865
parse_urlencoded(undefined, _, _, _) -> [].
×
866

867
apply_custom_headers(Headers, CustomHeaders) ->
868
    {Doctype, Headers2} = case Headers -- [html] of
83✔
869
        Headers -> {[], Headers};
43✔
870
        Other -> {[html], Other}
40✔
871
    end,
872
    M = maps:merge(maps:from_list(Headers2),
83✔
873
                   maps:from_list(CustomHeaders)),
874
    Doctype ++ maps:to_list(M).
83✔
875

876
%%%--------------------------------
877
%%%-export([get_url/4, get_auto_url/2]).
878

879
get_url(M, bosh, Tls, Host) ->
880
    get_url(M, Tls, Host, bosh_service_url, mod_bosh);
×
881
get_url(M, websocket, Tls, Host) ->
882
    get_url(M, Tls, Host, websocket_url, ejabberd_http_ws);
×
883
get_url(M, Option, Tls, Host) ->
884
    get_url(M, Tls, Host, Option, M).
×
885

886
get_url(M, Tls, Host, Option, Handler) ->
887
    case get_url_preliminar(M, Tls, Host, Option, Handler) of
×
888
        undefined -> undefined;
×
889
        Url -> misc:expand_keyword(<<"@HOST@">>, Url, Host)
×
890
    end.
891

892
get_url_preliminar(M, Tls, Host, Option, Handler) ->
893
    case gen_mod:get_module_opt(Host, M, Option) of
×
894
        undefined -> undefined;
×
895
        auto -> get_auto_url(Tls, Handler);
×
896
        <<"auto">> -> get_auto_url(Tls, Handler);
×
897
        U when is_binary(U) -> U
×
898
    end.
899

900
get_auto_url(Tls, Handler) ->
901
    [{_ThisTls, Url} | _] = get_auto_urls(Tls, Handler),
×
902
    Url.
×
903

904
-spec get_auto_urls(boolean() | any, atom()) -> [{boolean(), binary()}].
905

906
get_auto_urls(Tls, Handler) ->
907
    Paths = find_handler_port_path(Tls, Handler),
40✔
908
    [prepare_url(Path, Handler) || Path <- Paths].
40✔
909

910
prepare_url({ThisTls, Port, Path}, Handler) ->
911
            Protocol = case {ThisTls, Handler} of
×
912
                           {false, ejabberd_http_ws} -> <<"ws">>;
×
913
                           {true, ejabberd_http_ws} -> <<"wss">>;
×
914
                           {false, _} -> <<"http">>;
×
915
                           {true, _} -> <<"https">>
×
916
                       end,
917
            {ThisTls,
×
918
             <<Protocol/binary,
919
              "://@HOST@:",
920
              (integer_to_binary(Port))/binary,
921
              "/",
922
              (str:join(Path, <<"/">>))/binary,
923
              "/">>}.
924

925
find_handler_port_path(Tls, Handler) ->
926
    lists:filtermap(
40✔
927
      fun({{Port, _, _},
928
           ejabberd_http,
929
           #{tls := ThisTls, request_handlers := Handlers}})
930
            when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) ->
931
              case lists:keyfind(Handler, 2, Handlers) of
40✔
932
                  false -> false;
40✔
933
                  {Path, Handler} -> {true, {ThisTls, Port, Path}}
×
934
              end;
935
         (_) -> false
160✔
936
      end, ets:tab2list(ejabberd_listener)).
937

938

939
%%%--------------------------------
940

941
% The following code is mostly taken from yaws_ssl.erl
942

943
toupper(C) when C >= $a andalso C =< $z -> C - 32;
×
944
toupper(C) -> C.
18✔
945

946
tolower(C) when C >= $A andalso C =< $Z -> C + 32;
×
947
tolower(C) -> C.
36✔
948

949
normalize_header_name(Name) ->
950
    normalize_header_name(Name, [], true).
9✔
951

952
normalize_header_name(<<"">>, Acc, _) ->
953
    iolist_to_binary(Acc);
9✔
954
normalize_header_name(<<"-", Rest/binary>>, Acc, _) ->
955
    normalize_header_name(Rest, [Acc, "-"], true);
9✔
956
normalize_header_name(<<C:8, Rest/binary>>, Acc, true) ->
957
    normalize_header_name(Rest, [Acc, toupper(C)], false);
18✔
958
normalize_header_name(<<C:8, Rest/binary>>, Acc, false) ->
959
    normalize_header_name(Rest, [Acc, tolower(C)], false).
36✔
960

961
normalize_path(Path) ->
962
    normalize_path(Path, []).
83✔
963

964
normalize_path([], Norm) -> lists:reverse(Norm);
83✔
965
normalize_path([<<"..">>|Path], Norm) ->
966
    normalize_path(Path, Norm);
×
967
normalize_path([_Parent, <<"..">>|Path], Norm) ->
968
    normalize_path(Path, Norm);
×
969
normalize_path([Part | Path], Norm) ->
970
    normalize_path(Path, [Part|Norm]).
250✔
971

972
listen_opt_type(tag) ->
973
    econf:binary();
13✔
974
listen_opt_type(allow_unencrypted_sasl2) ->
975
    econf:bool();
13✔
976
listen_opt_type(request_handlers) ->
977
    econf:map(
13✔
978
      econf:and_then(
979
        econf:binary(),
980
        fun(Path) -> str:tokens(Path, <<"/">>) end),
52✔
981
      econf:beam([[{socket_handoff, 3}, {process, 2}]]));
982
listen_opt_type(custom_headers) ->
983
    econf:map(
13✔
984
      econf:binary(),
985
      econf:binary()).
986

987
listen_options() ->
988
    [{ciphers, undefined},
37✔
989
     {dhfile, undefined},
990
     {cafile, undefined},
991
     {protocol_options, undefined},
992
     {tls, false},
993
     {tls_compression, false},
994
     {allow_unencrypted_sasl2, false},
995
     {request_handlers, []},
996
     {tag, <<>>},
997
     {custom_headers, []}].
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