- Implemented a bash script to verify all references to version 3.2.x have been updated to 3.3.0. - The script checks binary version, core source files, configuration files, documentation, ejabberd module, and network resilience documentation. - Provides a summary of verification results and confirms successful updates across all components.
347 lines
14 KiB
Erlang
347 lines
14 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : mod_http_upload_hmac_network_resilient.erl
|
|
%%% Author : HMAC File Server Team
|
|
%%% Purpose : Network-Resilient XEP-0363 HTTP File Upload with HMAC Integration
|
|
%%% Version : 3.3.0 Network Resilience Edition
|
|
%%% Created : 26 Aug 2025
|
|
%%%----------------------------------------------------------------------
|
|
|
|
-module(mod_http_upload_hmac_network_resilient).
|
|
-behaviour(gen_mod).
|
|
|
|
-export([start/2, stop/1, reload/3, mod_options/1, mod_doc/0]).
|
|
-export([process_iq/1, get_url/3, get_slot/4, refresh_token/3]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("logger.hrl").
|
|
-include("xmpp.hrl").
|
|
|
|
-define(NS_HTTP_UPLOAD, <<"urn:xmpp:http:upload:0">>).
|
|
-define(DEFAULT_MAX_SIZE, 104857600). % 100MB
|
|
-define(DEFAULT_TOKEN_EXPIRY, 14400). % 4 hours for network resilience
|
|
-define(DEFAULT_EXTENDED_EXPIRY, 86400). % 24 hours for mobile scenarios
|
|
-define(DEFAULT_GRACE_PERIOD, 7200). % 2 hours grace period
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% gen_mod callbacks
|
|
%%%----------------------------------------------------------------------
|
|
|
|
start(Host, Opts) ->
|
|
?INFO_MSG("Starting mod_http_upload_hmac_network_resilient for ~s", [Host]),
|
|
IQDisc = gen_mod:get_opt(iqdisc, Opts),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_HTTP_UPLOAD,
|
|
?MODULE, process_iq, IQDisc),
|
|
ok.
|
|
|
|
stop(Host) ->
|
|
?INFO_MSG("Stopping mod_http_upload_hmac_network_resilient for ~s", [Host]),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_HTTP_UPLOAD),
|
|
ok.
|
|
|
|
reload(Host, NewOpts, OldOpts) ->
|
|
?INFO_MSG("Reloading mod_http_upload_hmac_network_resilient for ~s", [Host]),
|
|
ok.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% IQ Processing with Network Resilience
|
|
%%%----------------------------------------------------------------------
|
|
|
|
process_iq(#iq{type = get, from = From, to = To,
|
|
sub_els = [#upload_request{filename = Filename,
|
|
size = Size,
|
|
'content-type' = ContentType}]} = IQ) ->
|
|
User = jid:user(From),
|
|
Server = jid:server(From),
|
|
Host = jid:server(To),
|
|
|
|
?INFO_MSG("Upload request from ~s@~s: ~s (~B bytes)", [User, Server, Filename, Size]),
|
|
|
|
case check_upload_permission(User, Server, Host, Size) of
|
|
ok ->
|
|
case generate_resilient_upload_slot(User, Server, Host, Filename, Size, ContentType) of
|
|
{ok, PutURL, GetURL, Headers} ->
|
|
Slot = #upload_slot{get = GetURL, put = PutURL, headers = Headers},
|
|
?INFO_MSG("Upload slot created for ~s@~s: resilient token with extended expiry", [User, Server]),
|
|
IQ#iq{type = result, sub_els = [Slot]};
|
|
{error, Reason} ->
|
|
?WARNING_MSG("Upload slot generation failed: ~p", [Reason]),
|
|
xmpp:make_error(IQ, xmpp:err_internal_server_error())
|
|
end;
|
|
{error, quota_exceeded} ->
|
|
?INFO_MSG("Upload denied for ~s@~s: quota exceeded", [User, Server]),
|
|
xmpp:make_error(IQ, xmpp:err_resource_constraint());
|
|
{error, file_too_large} ->
|
|
?INFO_MSG("Upload denied for ~s@~s: file too large (~B bytes)", [User, Server, Size]),
|
|
xmpp:make_error(IQ, xmpp:err_not_acceptable());
|
|
{error, forbidden_extension} ->
|
|
?INFO_MSG("Upload denied for ~s@~s: forbidden file extension", [User, Server]),
|
|
xmpp:make_error(IQ, xmpp:err_not_acceptable());
|
|
{error, Reason} ->
|
|
?WARNING_MSG("Upload permission check failed: ~p", [Reason]),
|
|
xmpp:make_error(IQ, xmpp:err_forbidden())
|
|
end;
|
|
|
|
process_iq(#iq{type = get} = IQ) ->
|
|
xmpp:make_error(IQ, xmpp:err_bad_request());
|
|
|
|
process_iq(#iq{type = set} = IQ) ->
|
|
xmpp:make_error(IQ, xmpp:err_not_allowed()).
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Permission Checking (Enhanced for Mobile)
|
|
%%%----------------------------------------------------------------------
|
|
|
|
check_upload_permission(User, Server, Host, Size) ->
|
|
MaxSize = get_max_size(Host),
|
|
if Size > MaxSize ->
|
|
{error, file_too_large};
|
|
true ->
|
|
case check_user_quota(User, Server, Host, Size) of
|
|
ok ->
|
|
check_extension_allowed(Host, "");
|
|
Error ->
|
|
Error
|
|
end
|
|
end.
|
|
|
|
check_user_quota(User, Server, Host, Size) ->
|
|
MaxQuota = get_user_quota(Host),
|
|
case get_user_usage(User, Server, Host) of
|
|
{ok, CurrentUsage} when CurrentUsage + Size =< MaxQuota ->
|
|
ok;
|
|
{ok, _} ->
|
|
{error, quota_exceeded};
|
|
{error, _} ->
|
|
ok % If we can't check usage, allow upload
|
|
end.
|
|
|
|
check_extension_allowed(_Host, _Extension) ->
|
|
% TODO: Implement extension filtering
|
|
ok.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Network-Resilient Upload Slot Generation
|
|
%%%----------------------------------------------------------------------
|
|
|
|
generate_resilient_upload_slot(User, Server, Host, Filename, Size, ContentType) ->
|
|
UUID = generate_uuid(),
|
|
Timestamp = unix_timestamp(),
|
|
|
|
% Determine expiry based on mobile optimization settings
|
|
BaseExpiry = get_token_expiry(Host),
|
|
ExtendedExpiry = case get_mobile_optimizations(Host) of
|
|
true ->
|
|
% For mobile clients: much longer token validity
|
|
Timestamp + get_extended_expiry(Host);
|
|
false ->
|
|
% Standard expiry
|
|
Timestamp + BaseExpiry
|
|
end,
|
|
|
|
% Generate primary token
|
|
case generate_resilient_upload_token(User, Server, Filename, Size, Timestamp, Host, ExtendedExpiry) of
|
|
{ok, Token} ->
|
|
BaseURL = get_hmac_server_url(Host),
|
|
|
|
% Create resilient URLs with session recovery parameters
|
|
SessionId = generate_session_id(),
|
|
PutURL = iolist_to_binary([BaseURL, "/upload/", UUID, "/",
|
|
http_uri:encode(binary_to_list(Filename)),
|
|
"?token=", Token,
|
|
"&user=", User, "@", Server,
|
|
"&expiry=", integer_to_binary(ExtendedExpiry),
|
|
"&session_id=", SessionId,
|
|
"&network_resilience=true",
|
|
"&resume_allowed=true"]),
|
|
|
|
GetURL = iolist_to_binary([BaseURL, "/download/", UUID, "/",
|
|
http_uri:encode(binary_to_list(Filename))]),
|
|
|
|
% Enhanced headers for network resilience
|
|
Headers = [
|
|
#upload_header{name = <<"Authorization">>,
|
|
value = <<"Bearer ", Token/binary>>},
|
|
#upload_header{name = <<"Content-Type">>,
|
|
value = ContentType},
|
|
#upload_header{name = <<"X-Upload-Session-ID">>,
|
|
value = list_to_binary(SessionId)},
|
|
#upload_header{name = <<"X-Network-Resilience">>,
|
|
value = <<"enabled">>},
|
|
#upload_header{name = <<"X-Token-Refresh-URL">>,
|
|
value = iolist_to_binary([BaseURL, "/auth/refresh"])},
|
|
#upload_header{name = <<"X-Extended-Timeout">>,
|
|
value = integer_to_binary(ExtendedExpiry)}
|
|
],
|
|
|
|
?INFO_MSG("Generated resilient upload slot: session=~s, expiry=~B", [SessionId, ExtendedExpiry]),
|
|
{ok, PutURL, GetURL, Headers};
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
generate_resilient_upload_token(User, Server, Filename, Size, Timestamp, Host, Expiry) ->
|
|
Secret = get_hmac_secret(Host),
|
|
UserJID = iolist_to_binary([User, "@", Server]),
|
|
|
|
% Enhanced payload for network resilience with extended context
|
|
Payload = iolist_to_binary([
|
|
UserJID, "\0",
|
|
Filename, "\0",
|
|
integer_to_binary(Size), "\0",
|
|
integer_to_binary(Timestamp), "\0",
|
|
integer_to_binary(Expiry), "\0",
|
|
<<"network_resilient">>
|
|
]),
|
|
|
|
case crypto:mac(hmac, sha256, Secret, Payload) of
|
|
Mac when is_binary(Mac) ->
|
|
Token = base64:encode(Mac),
|
|
?DEBUG_MSG("Generated resilient token for ~s: length=~B, expiry=~B",
|
|
[UserJID, byte_size(Token), Expiry]),
|
|
{ok, Token};
|
|
_ ->
|
|
{error, token_generation_failed}
|
|
end.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Token Refresh for Network Changes
|
|
%%%----------------------------------------------------------------------
|
|
|
|
refresh_token(User, Server, Host) ->
|
|
% Generate a new token when client detects network change
|
|
Timestamp = unix_timestamp(),
|
|
Expiry = Timestamp + get_extended_expiry(Host),
|
|
|
|
case generate_resilient_upload_token(User, Server, <<"refresh">>, 0, Timestamp, Host, Expiry) of
|
|
{ok, Token} ->
|
|
?INFO_MSG("Token refreshed for ~s@~s due to network change", [User, Server]),
|
|
{ok, Token, Expiry};
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Helper Functions (Enhanced for Mobile)
|
|
%%%----------------------------------------------------------------------
|
|
|
|
generate_uuid() ->
|
|
% Enhanced UUID generation with timestamp component
|
|
{MegaSecs, Secs, MicroSecs} = os:timestamp(),
|
|
Random = crypto:strong_rand_bytes(4),
|
|
RandomHex = binary_to_list(binary:encode_hex(Random)),
|
|
lists:flatten(io_lib:format("~8.16.0b-~8.16.0b-~8.16.0b-~s",
|
|
[MegaSecs, Secs, MicroSecs, RandomHex])).
|
|
|
|
generate_session_id() ->
|
|
% Generate unique session ID for tracking across network changes
|
|
{MegaSecs, Secs, MicroSecs} = os:timestamp(),
|
|
Hash = crypto:hash(sha256, term_to_binary({MegaSecs, Secs, MicroSecs, make_ref()})),
|
|
binary_to_list(binary:encode_hex(binary:part(Hash, 0, 8))).
|
|
|
|
unix_timestamp() ->
|
|
{MegaSecs, Secs, _MicroSecs} = os:timestamp(),
|
|
MegaSecs * 1000000 + Secs.
|
|
|
|
get_url(Host, UUID, Filename) ->
|
|
BaseURL = get_hmac_server_url(Host),
|
|
iolist_to_binary([BaseURL, "/download/", UUID, "/",
|
|
http_uri:encode(binary_to_list(Filename))]).
|
|
|
|
get_slot(User, Server, Host, Filename) ->
|
|
% External API for getting upload slots
|
|
Size = 0, % Size will be determined during upload
|
|
ContentType = <<"application/octet-stream">>,
|
|
generate_resilient_upload_slot(User, Server, Host, Filename, Size, ContentType).
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Configuration Helpers (Enhanced for Network Resilience)
|
|
%%%----------------------------------------------------------------------
|
|
|
|
get_hmac_server_url(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, hmac_server_url,
|
|
<<"http://localhost:8080">>).
|
|
|
|
get_hmac_secret(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, hmac_shared_secret,
|
|
<<"default-secret-change-me">>).
|
|
|
|
get_max_size(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, max_size, ?DEFAULT_MAX_SIZE).
|
|
|
|
get_user_quota(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, quota_per_user, 1073741824). % 1GB
|
|
|
|
get_token_expiry(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, token_expiry, ?DEFAULT_TOKEN_EXPIRY).
|
|
|
|
get_extended_expiry(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, extended_token_expiry, ?DEFAULT_EXTENDED_EXPIRY).
|
|
|
|
get_mobile_optimizations(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, mobile_optimizations, true).
|
|
|
|
get_grace_period(Host) ->
|
|
gen_mod:get_module_opt(Host, ?MODULE, grace_period, ?DEFAULT_GRACE_PERIOD).
|
|
|
|
get_user_usage(User, Server, Host) ->
|
|
% TODO: Implement user quota tracking
|
|
{ok, 0}.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Module Options (Enhanced for Network Resilience)
|
|
%%%----------------------------------------------------------------------
|
|
|
|
mod_options(Host) ->
|
|
[{hmac_server_url, <<"http://localhost:8080">>},
|
|
{hmac_shared_secret, <<"default-secret-change-me">>},
|
|
{max_size, ?DEFAULT_MAX_SIZE},
|
|
{quota_per_user, 1073741824}, % 1GB
|
|
{token_expiry, ?DEFAULT_TOKEN_EXPIRY}, % 4 hours standard
|
|
{extended_token_expiry, ?DEFAULT_EXTENDED_EXPIRY}, % 24 hours for mobile
|
|
{grace_period, ?DEFAULT_GRACE_PERIOD}, % 2 hours grace period
|
|
{mobile_optimizations, true}, % Enable mobile-friendly features
|
|
{network_resilience, true}, % Enable network change handling
|
|
{session_recovery, true}, % Enable session recovery
|
|
{allowed_extensions, []},
|
|
{iqdisc, one_queue}].
|
|
|
|
mod_doc() ->
|
|
#{desc =>
|
|
?T("This module implements XEP-0363 HTTP File Upload "
|
|
"with HMAC File Server integration and network resilience. "
|
|
"It provides seamless authentication using XMPP credentials "
|
|
"and handles WiFi/LTE network switching gracefully."),
|
|
opts =>
|
|
[{hmac_server_url,
|
|
#{value => ?T("URL"),
|
|
desc => ?T("Base URL of the HMAC File Server")}},
|
|
{hmac_shared_secret,
|
|
#{value => ?T("Secret"),
|
|
desc => ?T("Shared secret for HMAC token generation")}},
|
|
{max_size,
|
|
#{value => ?T("Size"),
|
|
desc => ?T("Maximum file size in bytes")}},
|
|
{token_expiry,
|
|
#{value => ?T("Seconds"),
|
|
desc => ?T("Standard upload token expiry time")}},
|
|
{extended_token_expiry,
|
|
#{value => ?T("Seconds"),
|
|
desc => ?T("Extended token expiry for mobile scenarios")}},
|
|
{mobile_optimizations,
|
|
#{value => ?T("Boolean"),
|
|
desc => ?T("Enable mobile network optimizations")}},
|
|
{network_resilience,
|
|
#{value => ?T("Boolean"),
|
|
desc => ?T("Enable network change resilience")}},
|
|
{iqdisc,
|
|
#{value => ?T("Discipline"),
|
|
desc => ?T("IQ processing discipline")}}],
|
|
example =>
|
|
[?T("modules:"), ?T(" mod_http_upload_hmac_network_resilient:"),
|
|
?T(" hmac_server_url: \"http://localhost:8080\""),
|
|
?T(" hmac_shared_secret: \"your-secure-secret\""),
|
|
?T(" token_expiry: 14400 # 4 hours"),
|
|
?T(" extended_token_expiry: 86400 # 24 hours for mobile"),
|
|
?T(" mobile_optimizations: true"),
|
|
?T(" network_resilience: true")]}.
|