%%%---------------------------------------------------------------------- %%% File : mod_matrix_gw.erl %%% Author : Alexey Shchepin %%% Purpose : Matrix gateway %%% Created : 23 Apr 2022 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2024 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(mod_matrix_gw). -ifndef(OTP_BELOW_24). -author('alexey@process-one.net'). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, start_link/1, procname/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). -export([parse_auth/1, encode_canonical_json/1, get_id_domain_exn/1, base64_decode/1, base64_encode/1, prune_event/2, get_event_id/2, content_hash/1, sign_event/3, sign_pruned_event/2, sign_json/2, send_request/8, s2s_out_bounce_packet/2, user_receive_packet/1, route/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("translate.hrl"). -include("ejabberd_web_admin.hrl"). -include("mod_matrix_gw.hrl"). -define(MAX_REQUEST_SIZE, 1000000). process([<<"key">>, <<"v2">>, <<"server">> | _], #request{method = 'GET', host = _Host} = _Request) -> Host = ejabberd_config:get_myname(), KeyName = mod_matrix_gw_opt:key_name(Host), KeyID = <<"ed25519:", KeyName/binary>>, ServerName = mod_matrix_gw_opt:matrix_domain(Host), TS = erlang:system_time(millisecond) + timer:hours(24 * 7), {PubKey, _PrivKey} = mod_matrix_gw_opt:key(Host), JSON = #{<<"old_verify_keys">> => #{}, <<"server_name">> => ServerName, <<"valid_until_ts">> => TS, <<"verify_keys">> => #{ KeyID => #{ <<"key">> => base64_encode(PubKey) } }}, SJSON = sign_json(Host, JSON), {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(SJSON)}; process([<<"federation">>, <<"v1">>, <<"version">>], #request{method = 'GET', host = _Host} = _Request) -> JSON = #{<<"server">> => #{<<"name">> => <<"ejabberd/mod_matrix_gw">>, <<"version">> => <<"0.1">>}}, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(JSON)}; process([<<"federation">>, <<"v1">>, <<"query">>, <<"profile">>], #request{method = 'GET', host = _Host} = Request) -> case proplists:get_value(<<"user_id">>, Request#request.q) of UserID when is_binary(UserID) -> Field = case proplists:get_value(<<"field">>, Request#request.q) of <<"displayname">> -> displayname; <<"avatar_url">> -> avatar_url; undefined -> all; _ -> error end, case Field of error -> {400, [], <<"400 Bad Request: bad 'field' parameter">>}; _ -> case preprocess_federation_request(Request) of {ok, _JSON, _Origin} -> {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], <<"{}">>}; {result, HTTPResult} -> HTTPResult end end; undefined -> {400, [], <<"400 Bad Request: missing 'user_id' parameter">>} end; process([<<"federation">>, <<"v1">>, <<"user">>, <<"devices">>, UserID], #request{method = 'GET', host = _Host} = Request) -> case preprocess_federation_request(Request) of {ok, _JSON, _Origin} -> Res = #{<<"devices">> => [#{<<"device_id">> => <<"ejabberd/mod_matrix_gw">>, <<"keys">> => []}], <<"stream_id">> => 1, <<"user_id">> => UserID}, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v1">>, <<"user">>, <<"keys">>, <<"query">>], #request{method = 'POST', host = _Host} = Request) -> case preprocess_federation_request(Request, false) of {ok, #{<<"device_keys">> := DeviceKeys}, _Origin} -> DeviceKeys2 = maps:map(fun(_Key, _) -> #{} end, DeviceKeys), Res = #{<<"device_keys">> => DeviceKeys2}, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v2">>, <<"invite">>, RoomID, EventID], #request{method = 'PUT', host = _Host} = Request) -> case preprocess_federation_request(Request) of {ok, #{<<"event">> := #{%<<"origin">> := Origin, <<"room_id">> := RoomID, <<"sender">> := Sender, <<"state_key">> := UserID} = Event, <<"room_version">> := RoomVer}, Origin} -> case mod_matrix_gw_room:binary_to_room_version(RoomVer) of #room_version{} = RoomVersion -> %% TODO: check type and userid Host = ejabberd_config:get_myname(), PrunedEvent = prune_event(Event, RoomVersion), ?DEBUG("invite ~p~n", [{RoomID, EventID, Event, RoomVer, catch mod_matrix_gw_s2s:check_signature(Host, PrunedEvent), get_pruned_event_id(PrunedEvent)}]), case mod_matrix_gw_s2s:check_signature(Host, PrunedEvent) of true -> case get_pruned_event_id(PrunedEvent) of EventID -> SEvent = sign_pruned_event(Host, PrunedEvent), ?DEBUG("sign event ~p~n", [SEvent]), ResJSON = #{<<"event">> => SEvent}, mod_matrix_gw_room:join(Host, Origin, RoomID, Sender, UserID), ?DEBUG("res ~s~n", [jiffy:encode(ResJSON)]), {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(ResJSON)}; _ -> {400, [], <<"400 Bad Request: bad event id">>} end; false -> {400, [], <<"400 Bad Request: signature check failed">>} end; false -> {400, [], <<"400 Bad Request: unsupported room version">>} end; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v1">>, <<"send">>, _TxnID], #request{method = 'PUT', host = _Host} = Request) -> case preprocess_federation_request(Request, false) of {ok, #{<<"origin">> := Origin, <<"pdus">> := PDUs} = JSON, Origin} -> ?DEBUG("send request ~p~n", [JSON]), Host = ejabberd_config:get_myname(), Res = lists:map( fun(PDU) -> case mod_matrix_gw_room:process_pdu(Host, Origin, PDU) of {ok, EventID} -> {EventID, #{}}; {error, Error} -> {get_event_id(PDU, mod_matrix_gw_room:binary_to_room_version(<<"9">>)), #{<<"error">> => Error}} end end, PDUs), ?DEBUG("send res ~p~n", [Res]), {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(maps:from_list(Res))}; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v1">>, <<"get_missing_events">>, RoomID], #request{method = 'POST', host = _Host} = Request) -> case preprocess_federation_request(Request, false) of {ok, #{<<"earliest_events">> := EarliestEvents, <<"latest_events">> := LatestEvents} = JSON, Origin} -> ?DEBUG("get_missing_events request ~p~n", [JSON]), Limit = maps:get(<<"limit">>, JSON, 10), MinDepth = maps:get(<<"min_depth">>, JSON, 0), Host = ejabberd_config:get_myname(), PDUs = mod_matrix_gw_room:get_missing_events( Host, Origin, RoomID, EarliestEvents, LatestEvents, Limit, MinDepth), ?DEBUG("get_missing_events res ~p~n", [PDUs]), Res = #{<<"events">> => PDUs}, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v1">>, <<"backfill">>, RoomID], #request{method = 'GET', host = _Host} = Request) -> case catch binary_to_integer(proplists:get_value(<<"limit">>, Request#request.q)) of Limit when is_integer(Limit) -> case preprocess_federation_request(Request, false) of {ok, _JSON, Origin} -> LatestEvents = proplists:get_all_values(<<"v">>, Request#request.q), ?DEBUG("backfill request ~p~n", [{Limit, LatestEvents}]), Host = ejabberd_config:get_myname(), PDUs1 = mod_matrix_gw_room:get_missing_events( Host, Origin, RoomID, [], LatestEvents, Limit, 0), PDUs2 = lists:flatmap( fun(EventID) -> case mod_matrix_gw_room:get_event(Host, RoomID, EventID) of {ok, PDU} -> [PDU]; _ -> [] end end, LatestEvents), PDUs = PDUs2 ++ PDUs1, ?DEBUG("backfill res ~p~n", [PDUs]), MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), Res = #{<<"origin">> => MatrixServer, <<"origin_server_ts">> => erlang:system_time(millisecond), <<"pdus">> => PDUs}, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {result, HTTPResult} -> HTTPResult end; _ -> {400, [], <<"400 Bad Request: bad 'limit' parameter">>} end; process([<<"federation">>, <<"v1">>, <<"state_ids">>, RoomID], #request{method = 'GET', host = _Host} = Request) -> case proplists:get_value(<<"event_id">>, Request#request.q) of EventID when is_binary(EventID) -> case preprocess_federation_request(Request) of {ok, _JSON, Origin} -> Host = ejabberd_config:get_myname(), case mod_matrix_gw_room:get_state_ids(Host, Origin, RoomID, EventID) of {ok, AuthChain, PDUs} -> Res = #{<<"auth_chain_ids">> => AuthChain, <<"pdu_ids">> => PDUs}, ?DEBUG("get_state_ids res ~p~n", [Res]), {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {error, room_not_found} -> {400, [], <<"400 Bad Request: room not found">>}; {error, not_allowed} -> {403, [], <<"403 Forbidden: origin not in room">>}; {error, event_not_found} -> {400, [], <<"400 Bad Request: 'event_id' not found">>} end; {result, HTTPResult} -> HTTPResult end; undefined -> {400, [], <<"400 Bad Request: missing 'event_id' parameter">>} end; process([<<"federation">>, <<"v1">>, <<"event">>, EventID], #request{method = 'GET', host = _Host} = Request) -> case preprocess_federation_request(Request) of {ok, _JSON, _Origin} -> Host = ejabberd_config:get_myname(), %% TODO: very inefficient, replace with an SQL call PDU = lists:foldl( fun(RoomID, undefined) -> case mod_matrix_gw_room:get_event(Host, RoomID, EventID) of {ok, PDU} -> PDU; _ -> undefined end; (_, Acc) -> Acc end, undefined, mod_matrix_gw_room:get_rooms_list()), ?DEBUG("get_event res ~p~n", [PDU]), case PDU of undefined -> {400, [], <<"400 Bad Request: event not found">>}; _ -> MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), Res = #{<<"origin">> => MatrixServer, <<"origin_server_ts">> => erlang:system_time(millisecond), <<"pdus">> => [PDU]}, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)} end; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v1">>, <<"make_join">>, RoomID, UserID], #request{method = 'GET', host = _Host, q = Params} = Request) -> case preprocess_federation_request(Request) of {ok, _JSON, Origin} -> Host = ejabberd_config:get_myname(), case get_id_domain_exn(UserID) of Origin -> case mod_matrix_gw_room:make_join(Host, RoomID, UserID, Params) of {error, room_not_found} -> Res = #{<<"errcode">> => <<"M_NOT_FOUND">>, <<"error">> => <<"Unknown room">>}, {404, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {error, not_invited} -> Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, <<"error">> => <<"You are not invited to this room">>}, {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {error, {incompatible_version, Ver}} -> Res = #{<<"errcode">> => <<"M_INCOMPATIBLE_ROOM_VERSION">>, <<"error">> => <<"Your homeserver does not support the features required to join this room">>, <<"room_version">> => Ver}, {400, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {ok, Res} -> {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)} end; _ -> Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, <<"error">> => <<"User not from origin">>}, {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)} end; {result, HTTPResult} -> HTTPResult end; process([<<"federation">>, <<"v2">>, <<"send_join">>, RoomID, EventID], #request{method = 'PUT', host = _Host} = Request) -> case preprocess_federation_request(Request) of {ok, #{<<"content">> := #{<<"membership">> := <<"join">>}, %<<"origin">> := Origin, <<"room_id">> := RoomID, <<"sender">> := Sender, <<"state_key">> := Sender, <<"type">> := <<"m.room.member">>} = JSON, Origin} -> Host = ejabberd_config:get_myname(), case get_id_domain_exn(Sender) of Origin -> case mod_matrix_gw_room:send_join(Host, Origin, RoomID, EventID, JSON) of {error, room_not_found} -> Res = #{<<"errcode">> => <<"M_NOT_FOUND">>, <<"error">> => <<"Unknown room">>}, {404, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {error, not_invited} -> Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, <<"error">> => <<"You are not invited to this room">>}, {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {error, Error} when is_binary(Error) -> Res = #{<<"errcode">> => <<"M_BAD_REQUEST">>, <<"error">> => Error}, {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {ok, Res} -> ?DEBUG("send_join res: ~p~n", [Res]), {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)} end; _ -> Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, <<"error">> => <<"User not from origin">>}, {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)} end; {ok, _JSON, _Origin} -> Res = #{<<"errcode">> => <<"M_BAD_REQUEST">>, <<"error">> => <<"Invalid event format">>}, {400, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], jiffy:encode(Res)}; {result, HTTPResult} -> HTTPResult end; process(_Path, _Request) -> ?DEBUG("matrix 404: ~p~n~p~n", [_Path, _Request]), ejabberd_web:error(not_found). preprocess_federation_request(Request) -> preprocess_federation_request(Request, true). preprocess_federation_request(Request, DoSignCheck) -> ?DEBUG("matrix federation: ~p~n", [Request]), case proplists:get_value('Authorization', Request#request.headers) of Auth when is_binary(Auth) -> case parse_auth(Auth) of #{<<"origin">> := MatrixServer, <<"key">> := _, <<"sig">> := _} = AuthParams -> ?DEBUG("auth ~p~n", [AuthParams]), if Request#request.length =< ?MAX_REQUEST_SIZE -> Request2 = recv_data(Request), JSON = if Request#request.length > 0 -> try jiffy:decode(Request2#request.data, [return_maps]) catch _:_ -> error end; true -> none end, ?DEBUG("json ~p~n", [JSON]), case JSON of error -> {result, {400, [], <<"400 Bad Request: invalid JSON">>}}; JSON when not DoSignCheck -> {ok, JSON, MatrixServer}; JSON -> Host = ejabberd_config:get_myname(), case mod_matrix_gw_s2s:check_auth( Host, MatrixServer, AuthParams, JSON, Request2) of true -> ?DEBUG("auth ok~n", []), {ok, JSON, MatrixServer}; false -> ?DEBUG("auth failed~n", []), {result, {401, [], <<"401 Unauthorized">>}} end end; true -> {result, {400, [], <<"400 Bad Request: size limit">>}} end; _ -> {result, {400, [], <<"400 Bad Request: bad 'Authorization' header">>}} end; undefined -> {result, {400, [], <<"400 Bad Request: no 'Authorization' header">>}} end. recv_data(#request{length = Len, data = Trail, sockmod = SockMod, socket = Socket} = Request) -> NewLen = Len - byte_size(Trail), if NewLen > 0 -> case SockMod:recv(Socket, NewLen, 60000) of {ok, Data} -> Request#request{data = <>}; {error, _} -> Request end; true -> Request end. -record(state, {host :: binary(), server_host :: binary()}). -type state() :: #state{}. start(Host, _Opts) -> case mod_matrix_gw_sup:start(Host) of {ok, _} -> {ok, [{hook, s2s_out_bounce_packet, s2s_out_bounce_packet, 50}, {hook, user_receive_packet, user_receive_packet, 50}]}; Err -> Err end. stop(Host) -> Proc = mod_matrix_gw_sup:procname(Host), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). reload(_Host, _NewOpts, _OldOpts) -> ok. start_link(Host) -> Proc = procname(Host), ?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host], ejabberd_config:fsm_limit_opts([])). -spec init(list()) -> {ok, state()}. init([Host]) -> process_flag(trap_exit, true), mod_matrix_gw_s2s:create_db(), mod_matrix_gw_room:create_db(), Opts = gen_mod:get_module_opts(Host, ?MODULE), MyHost = gen_mod:get_opt(host, Opts), register_routes(Host, [MyHost]), {ok, #state{server_host = Host, host = MyHost}}. -spec handle_call(term(), {pid(), term()}, state()) -> {reply, ok | {ok, pid()} | {error, any()}, state()} | {stop, normal, ok, state()}. handle_call(stop, _From, State) -> {stop, normal, ok, State}. -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(term(), state()) -> any(). terminate(_Reason, #state{host = Host}) -> unregister_routes([Host]). -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec register_routes(binary(), [binary()]) -> ok. register_routes(ServerHost, Hosts) -> lists:foreach( fun(Host) -> ejabberd_router:register_route( Host, ServerHost, {apply, ?MODULE, route}) end, Hosts). unregister_routes(Hosts) -> lists:foreach( fun(Host) -> ejabberd_router:unregister_route(Host) end, Hosts). procname(Host) -> binary_to_atom( <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary>>, utf8). parse_auth(<<"X-Matrix ", S/binary>>) -> parse_auth1(S, <<>>, []); parse_auth(_) -> error. parse_auth1(<<$=, Cs/binary>>, S, Ts) -> parse_auth2(Cs, S, <<>>, Ts); parse_auth1(<<$,, Cs/binary>>, <<>>, Ts) -> parse_auth1(Cs, [], Ts); parse_auth1(<<$\s, Cs/binary>>, <<>>, Ts) -> parse_auth1(Cs, [], Ts); parse_auth1(<>, S, Ts) -> parse_auth1(Cs, <>, Ts); parse_auth1(<<>>, <<>>, T) -> maps:from_list(T); parse_auth1(<<>>, _S, _T) -> error. parse_auth2(<<$", Cs/binary>>, Key, Val, Ts) -> parse_auth3(Cs, Key, Val, Ts); parse_auth2(<>, Key, Val, Ts) -> parse_auth4(Cs, Key, <>, Ts); parse_auth2(<<>>, _, _, _) -> error. parse_auth3(<<$", Cs/binary>>, Key, Val, Ts) -> parse_auth4(Cs, Key, Val, Ts); parse_auth3(<<$\\, C, Cs/binary>>, Key, Val, Ts) -> parse_auth3(Cs, Key, <>, Ts); parse_auth3(<>, Key, Val, Ts) -> parse_auth3(Cs, Key, <>, Ts); parse_auth3(<<>>, _, _, _) -> error. parse_auth4(<<$,, Cs/binary>>, Key, Val, Ts) -> parse_auth1(Cs, <<>>, [{Key, Val} | Ts]); parse_auth4(<<$\s, Cs/binary>>, Key, Val, Ts) -> parse_auth4(Cs, Key, Val, Ts); parse_auth4(<>, Key, Val, Ts) -> parse_auth4(Cs, Key, <>, Ts); parse_auth4(<<>>, Key, Val, Ts) -> parse_auth1(<<>>, <<>>, [{Key, Val} | Ts]). prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, RoomVersion) -> Event2 = case RoomVersion#room_version.updated_redaction_rules of false -> maps:with( [<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>, <<"state_key">>, <<"content">>, <<"hashes">>, <<"signatures">>, <<"depth">>, <<"prev_events">>, <<"prev_state">>, <<"auth_events">>, <<"origin">>, <<"origin_server_ts">>, <<"membership">>], Event); true -> maps:with( [<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>, <<"state_key">>, <<"content">>, <<"hashes">>, <<"signatures">>, <<"depth">>, <<"prev_events">>, <<"auth_events">>, <<"origin_server_ts">>], Event) end, Content2 = case Type of <<"m.room.member">> -> C3 = maps:with([<<"membership">>, <<"join_authorised_via_users_server">>], Content), case RoomVersion#room_version.updated_redaction_rules of false -> C3; true -> case Content of #{<<"third_party_invite">> := #{<<"signed">> := InvSign}} -> C3#{<<"third_party_invite">> => #{<<"signed">> => InvSign}}; _ -> C3 end end; <<"m.room.create">> -> case RoomVersion#room_version.updated_redaction_rules of false -> maps:with([<<"creator">>], Content); true -> Content end; <<"m.room.join_rules">> -> maps:with([<<"join_rule">>, <<"allow">>], Content); <<"m.room.power_levels">> -> case RoomVersion#room_version.updated_redaction_rules of false -> maps:with( [<<"ban">>, <<"events">>, <<"events_default">>, <<"kick">>, <<"redact">>, <<"state_default">>, <<"users">>, <<"users_default">>], Content); true -> maps:with( [<<"ban">>, <<"events">>, <<"events_default">>, <<"invite">>, <<"kick">>, <<"redact">>, <<"state_default">>, <<"users">>, <<"users_default">>], Content) end; <<"m.room.history_visibility">> -> maps:with([<<"history_visibility">>], Content); <<"m.room.redaction">> -> case RoomVersion#room_version.updated_redaction_rules of false -> #{}; true -> maps:with([<<"redacts">>], Content) end; _ -> #{} end, Event2#{<<"content">> := Content2}. reference_hash(PrunedEvent) -> Event2 = maps:without([<<"signatures">>, <<"age_ts">>, <<"unsigned">>], PrunedEvent), S = encode_canonical_json(Event2), crypto:hash(sha256, S). content_hash(Event) -> Event2 = maps:without([<<"signatures">>, <<"age_ts">>, <<"unsigned">>, <<"hashes">>, <<"outlier">>, <<"destinations">>], Event), S = encode_canonical_json(Event2), crypto:hash(sha256, S). get_event_id(Event, RoomVersion) -> PrunedEvent = prune_event(Event, RoomVersion), get_pruned_event_id(PrunedEvent). get_pruned_event_id(PrunedEvent) -> B = base64url_encode(reference_hash(PrunedEvent)), <<$$, B/binary>>. encode_canonical_json(JSON) -> JSON2 = sort_json(JSON), jiffy:encode(JSON2). sort_json(#{} = Map) -> Map2 = maps:map(fun(_K, V) -> sort_json(V) end, Map), {lists:sort(maps:to_list(Map2))}; sort_json(List) when is_list(List) -> lists:map(fun sort_json/1, List); sort_json(JSON) -> JSON. base64_decode(B) -> Fixed = case size(B) rem 4 of 0 -> B; %1 -> <>; 2 -> <>; 3 -> <>, <<"-">>, [global]), binary:replace(D1, <<"/">>, <<"_">>, [global]). sign_event(Host, Event, RoomVersion) -> PrunedEvent = prune_event(Event, RoomVersion), case sign_pruned_event(Host, PrunedEvent) of #{<<"signatures">> := Signatures} -> Event#{<<"signatures">> => Signatures} end. sign_pruned_event(Host, PrunedEvent) -> Event2 = maps:without([<<"age_ts">>, <<"unsigned">>], PrunedEvent), sign_json(Host, Event2). sign_json(Host, JSON) -> Signatures = maps:get(<<"signatures">>, JSON, #{}), JSON2 = maps:without([<<"signatures">>, <<"unsigned">>], JSON), Msg = encode_canonical_json(JSON2), SignatureName = mod_matrix_gw_opt:matrix_domain(Host), KeyName = mod_matrix_gw_opt:key_name(Host), {PubKey, PrivKey} = mod_matrix_gw_opt:key(Host), KeyID = <<"ed25519:", KeyName/binary>>, Sig = public_key:sign(Msg, ignored, {ed_pri, ed25519, PubKey, PrivKey}), Sig64 = base64_encode(Sig), Signatures2 = Signatures#{SignatureName => #{KeyID => Sig64}}, JSON#{<<"signatures">> => Signatures2}. send_request(Host, Method, MatrixServer, Path, Query, JSON, HTTPOptions, Options) -> URI1 = iolist_to_binary( lists:map(fun(P) -> [$/, uri_string:quote(P)] end, Path)), URI = case Query of [] -> URI1; _ -> URI2 = str:join( lists:map( fun({K, V}) -> [uri_string:quote(K), $=, uri_string:quote(V)] end, Query), $&), <> end, % TODO {MHost, MPort} = mod_matrix_gw_s2s:get_matrix_host_port(Host, MatrixServer), %{MHost, MPort} = {MatrixServer, 8008}, URL = <<"https://", MHost/binary, ":", (integer_to_binary(MPort))/binary, URI/binary>>, SMethod = case Method of get -> <<"GET">>; put -> <<"PUT">>; post -> <<"POST">> end, Auth = make_auth_header(Host, MatrixServer, SMethod, URI, JSON), Headers = [{"Authorization", binary_to_list(Auth)}], Content = case JSON of none -> <<>>; _ -> jiffy:encode(JSON) end, Request = case Method of get -> {URL, Headers}; _ -> {URL, Headers, "application/json;charset=UTF-8", Content} end, httpc:request(Method, Request, HTTPOptions, Options). make_auth_header(Host, MatrixServer, Method, URI, Content) -> Origin = mod_matrix_gw_opt:matrix_domain(Host), JSON = #{<<"method">> => Method, <<"uri">> => URI, <<"origin">> => Origin, <<"destination">> => MatrixServer }, JSON2 = case Content of none -> JSON; _ -> JSON#{<<"content">> => Content} end, JSON3 = sign_json(Host, JSON2), #{<<"signatures">> := #{Origin := #{} = KeySig}} = JSON3, {KeyID, Sig, _} = maps:next(maps:iterator(KeySig)), <<"X-Matrix origin=", Origin/binary, ",key=\"", KeyID/binary, "\",sig=\"", Sig/binary, "\",", "destination=\"", MatrixServer/binary, "\"">>. get_id_domain_exn(B) -> case binary:split(B, <<":">>) of [_, Tail] -> Tail; _ -> error({invalid_id, B}) end. s2s_out_bounce_packet(S2SState, Pkt) -> #{server_host := Host} = S2SState, case mod_matrix_gw_opt:matrix_id_as_jid(Host) of false -> S2SState; true -> To = xmpp:get_to(Pkt), ServiceHost = mod_matrix_gw_opt:host(Host), EscU = mod_matrix_gw_room:escape(To#jid.user), EscS = mod_matrix_gw_room:escape(To#jid.lserver), NewTo = jid:make(<>, ServiceHost), ejabberd_router:route(xmpp:set_to(Pkt, NewTo)), {stop, ignore} end. user_receive_packet({Pkt, C2SState} = Acc) -> #{lserver := Host} = C2SState, case mod_matrix_gw_opt:matrix_id_as_jid(Host) of false -> Acc; true -> ServiceHost = mod_matrix_gw_opt:host(Host), From = xmpp:get_from(Pkt), case From#jid.lserver of ServiceHost -> case binary:split(From#jid.user, <<"%">>) of [EscU, EscS] -> U = mod_matrix_gw_room:unescape(EscU), S = mod_matrix_gw_room:unescape(EscS), NewFrom = jid:make(U, S), {xmpp:set_from(Pkt, NewFrom), C2SState}; _ -> Acc end; _ -> Acc end end. route(Pkt) -> mod_matrix_gw_room:route(Pkt). depends(_Host, _Opts) -> []. mod_opt_type(host) -> econf:host(); mod_opt_type(matrix_domain) -> econf:binary(); mod_opt_type(key_name) -> econf:binary(); mod_opt_type(key) -> fun(Key) -> Key1 = (yconf:binary())(Key), Key2 = base64_decode(Key1), crypto:generate_key(eddsa, ed25519, Key2) end; mod_opt_type(matrix_id_as_jid) -> econf:bool(); mod_opt_type(persist) -> econf:bool(). mod_options(Host) -> [{matrix_domain, <<"@HOST@">>}, {host, <<"matrix.", Host/binary>>}, {key_name, <<"">>}, {key, {<<"">>, <<"">>}}, {matrix_id_as_jid, false}, {persist, false}]. mod_doc() -> #{desc => [?T("TODO")], example => [{?T("TODO"), ["listen:", " -", " port: 5280", " module: ejabberd_http", " request_handlers:", " /bosh: mod_bosh", " /websocket: ejabberd_http_ws", " /conversejs: mod_conversejs", "", "modules:", " mod_bosh: {}", " mod_conversejs:", " websocket_url: \"ws://@HOST@:5280/websocket\""]} ], opts => [{matrix_domain, #{value => ?T("Domain"), desc => ?T("TODO Specify a domain to act as the default for user JIDs. " "The keyword '@HOST@' is replaced with the hostname. " "The default value is '@HOST@'.")}} ] }. -endif.