From 23a32cae1ae32a510009ee40389e0f6ecfe296f8 Mon Sep 17 00:00:00 2001 From: Pawel Chmielowski Date: Tue, 17 Mar 2026 12:21:26 +0100 Subject: [PATCH] Add handling of Etag and If-Modified-Since headers to files served by mod_http_upload --- src/ejabberd_http.erl | 119 ++++++++++++++++++++++++++++++++++------ src/mod_http_upload.erl | 4 +- test/upload_tests.erl | 14 ++++- 3 files changed, 117 insertions(+), 20 deletions(-) diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 6af2e7762..bde56b1e3 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -517,7 +517,9 @@ process_request(#state{request_method = Method, make_text_output(State, Status, apply_custom_headers(Headers, CustomHeaders), Output); {Status, Headers, {file, FileName}} -> - make_file_output(State, Status, Headers, FileName); + make_file_output(State, Status, Headers, FileName, []); + {Status, Headers, {file, FileName, ReqHeaders}} -> + make_file_output(State, Status, Headers, FileName, ReqHeaders); {Status, Reason, Headers, Output} when is_binary(Output) or is_list(Output) -> make_text_output(State, Status, Reason, @@ -683,22 +685,107 @@ make_text_output(State, Status, Reason, Headers, Text) -> EncodedHdrs = make_headers(State, Status, Reason, Headers, Data2), [EncodedHdrs, Data2]. -make_file_output(State, Status, Headers, FileName) -> +parse_etags(Etags, WeakIgnore) -> + lists:filtermap( + fun(Value) -> + case string:trim(Value) of + <<"W/\"", _Rest/binary>> when WeakIgnore -> + false; + <<"W/\"", Rest/binary>> -> + case string:split(Rest, <<"\"">>, trailing) of + [Etag, _] -> {true, Etag}; + _ -> false + end; + <<"\"", Rest/binary>> -> + case string:split(Rest, <<"\"">>, trailing) of + [Etag, _] -> {true, Etag}; + _ -> false + end; + <<"*">> -> true; + _ -> false + end + end, + string:split(Etags, <<",">>, all)). + + +process_etags(Etag, RequestHeaders) -> + process_etags(Etag, RequestHeaders, if_match). + +process_etags(Etag, RequestHeaders, if_match) -> + case lists:keyfind('If-Match', 1, RequestHeaders) of + {_, Header} -> + Etags = parse_etags(Header, true), + case lists:any(fun(V) -> V == <<"*">> orelse V == Etag end, Etags) of + true -> process_etags(Etag, RequestHeaders, if_none_match); + _ -> {true, 412} + end; + _ -> + process_etags(Etag, RequestHeaders, if_none_match) + end; +process_etags(Etag, RequestHeaders, if_none_match) -> + case lists:keyfind('If-None-Match', 1, RequestHeaders) of + {_, Header} -> + Etags = parse_etags(Header, false), + case lists:any(fun(V) -> V == <<"*">> orelse V == Etag end, Etags) of + true -> {true, 304}; + _ -> false + end; + _ -> + false + end. + +process_if_modified_since(MTime, RequestHeaders) -> + case lists:keyfind('If-Modified-Since', 1, RequestHeaders) of + {_, Header} -> + case httpd_util:convert_request_date(binary_to_list(Header)) of + bad_date -> + false; + LM -> + T1 = calendar:datetime_to_gregorian_seconds( + calendar:universal_time_to_local_time(LM)), + T2 = calendar:datetime_to_gregorian_seconds(MTime), + case T1 >= T2 of + true -> + {true, 304}; + _-> false + end + end; + _ -> + false + end. + +make_file_output(State, Status, Headers, FileName, RequestHeaders) -> case file:read_file_info(FileName) of - {ok, #file_info{size = Size}} when State#state.request_method == 'HEAD' -> - make_headers(State, Status, <<"">>, Headers, Size); - {ok, #file_info{size = Size}} -> - case file:open(FileName, [raw, read]) of - {ok, Fd} -> - EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Size), - send_text(State, EncodedHdrs), - send_file(State, Fd, Size, FileName), - file:close(Fd), - none; - {error, Why} -> - Reason = file_format_error(Why), - ?ERROR_MSG("Failed to open ~ts: ~ts", [FileName, Reason]), - make_text_output(State, 404, Reason, [], <<>>) + {ok, #file_info{size = Size, mtime = MTime} = FI} -> + Etag = list_to_binary(httpd_util:create_etag(FI)), + ExtraHeaders = [{<<"Last-Modified">>, httpd_util:rfc1123_date(MTime)}, + {<<"ETag">>, Etag}], + case process_etags(Etag, RequestHeaders) of + false -> + case process_if_modified_since(MTime, RequestHeaders) of + false -> + if + State#state.request_method == 'HEAD' -> + make_headers(State, Status, <<"">>, ExtraHeaders ++ Headers, Size); + true -> + case file:open(FileName, [raw, read]) of + {ok, Fd} -> + EncodedHdrs = make_headers(State, Status, <<"">>, ExtraHeaders ++ Headers, Size), + send_text(State, EncodedHdrs), + send_file(State, Fd, Size, FileName), + file:close(Fd), + none; + {error, Why} -> + Reason = file_format_error(Why), + ?ERROR_MSG("Failed to open ~ts: ~ts", [FileName, Reason]), + make_text_output(State, 404, Reason, [], <<>>) + end + end; + {_, NewStatus} -> + make_headers(State, NewStatus, <<"">>, ExtraHeaders ++ Headers, 0) + end; + {_, NewStatus} -> + make_headers(State, NewStatus, <<"">>, ExtraHeaders ++ Headers, 0) end; {error, Why} -> Reason = file_format_error(Why), diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index 418a3aada..a5282e422 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -561,7 +561,7 @@ process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, [encode_addr(IP), Host, Error]), http_response(500) end; -process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request0) +process(_LocalPath, #request{method = Method, host = Host, ip = IP, headers = ReqHeaders} = Request0) when Method == 'GET'; Method == 'HEAD' -> Request = Request0#request{host = redecode_url(Host)}, @@ -584,7 +584,7 @@ process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request0) end, Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], Headers3 = ejabberd_http:apply_custom_headers(Headers2, CustomHeaders), - http_response(200, Headers3, {file, Path}); + http_response(200, Headers3, {file, Path, ReqHeaders}); {error, eacces} -> ?WARNING_MSG("Cannot serve ~ts to ~ts: Permission denied", [Path, encode_addr(IP)]), diff --git a/test/upload_tests.erl b/test/upload_tests.erl index 57da6d2ff..1d428929f 100644 --- a/test/upload_tests.erl +++ b/test/upload_tests.erl @@ -167,10 +167,20 @@ put_request(_Config, URL0, Data) -> get_request(_Config, URL0, Data) -> ct:comment("Getting ~B bytes from ~s", [size(Data), URL0]), URL = binary_to_list(URL0), - {ok, {{"HTTP/1.1", 200, _}, _, Body}} = + {ok, {{"HTTP/1.1", 200, _}, Headers, Body}} = httpc:request(get, {URL, []}, [], [{body_format, binary}]), ct:comment("Checking returned body"), - Body = Data. + Body = Data, + ct:comment("Request had Etag"), + Etag = ?match({_, Etag}, lists:keyfind("etag", 1, Headers), Etag), + ct:comment("Request had Last-Modified"), + LM = ?match({_, LM}, lists:keyfind("last-modified", 1, Headers), LM), + ct:comment("Request with Etag are handled correctly"), + {ok, {{"HTTP/1.1", 304, _}, _, _}} = + httpc:request(get, {URL, [{"If-None-Match", ["\"",Etag,"\""]}]}, [], [{body_format, binary}]), + ct:comment("Request with If-Modified-Since are handled correctly"), + {ok, {{"HTTP/1.1", 304, _}, _, _}} = + httpc:request(get, {URL, [{"If-Modified-Since", LM}]}, [], [{body_format, binary}]). max_size_exceed(Config, NS) -> To = upload_jid(Config),