mirror of
https://github.com/processone/ejabberd.git
synced 2024-12-22 17:28:25 +01:00
Add mod_matrix_gw
This commit is contained in:
parent
67a6776fba
commit
f44e23b8cc
28
include/mod_matrix_gw.hrl
Normal file
28
include/mod_matrix_gw.hrl
Normal file
@ -0,0 +1,28 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%%
|
||||
%%% 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.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-record(room_version,
|
||||
{id :: binary(),
|
||||
%% use the same field names as in Synapse
|
||||
knock_restricted_join_rule :: boolean(),
|
||||
enforce_int_power_levels :: boolean(),
|
||||
implicit_room_creator :: boolean(),
|
||||
updated_redaction_rules :: boolean()
|
||||
}).
|
913
src/mod_matrix_gw.erl
Normal file
913
src/mod_matrix_gw.erl
Normal file
@ -0,0 +1,913 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_matrix_gw.erl
|
||||
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
||||
%%% Purpose : Matrix gateway
|
||||
%%% Created : 23 Apr 2022 by Alexey Shchepin <alexey@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2022 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).
|
||||
-if(?OTP_RELEASE >= 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 = <<Trail/binary, Data/binary>>};
|
||||
{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(<<C, Cs/binary>>, S, Ts) -> parse_auth1(Cs, <<S/binary, C>>, 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(<<C, Cs/binary>>, Key, Val, Ts) ->
|
||||
parse_auth4(Cs, Key, <<Val/binary, C>>, 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, <<Val/binary, C>>, Ts);
|
||||
parse_auth3(<<C, Cs/binary>>, Key, Val, Ts) ->
|
||||
parse_auth3(Cs, Key, <<Val/binary, C>>, 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(<<C, Cs/binary>>, Key, Val, Ts) ->
|
||||
parse_auth4(Cs, Key, <<Val/binary, C>>, 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 -> <<B/binary, "===">>;
|
||||
2 -> <<B/binary, "==">>;
|
||||
3 -> <<B/binary, "=">>
|
||||
end,
|
||||
base64:decode(Fixed).
|
||||
|
||||
base64_encode(B) ->
|
||||
D = base64:encode(B),
|
||||
K = binary:longest_common_suffix([D, <<"==">>]),
|
||||
binary:part(D, 0, size(D) - K).
|
||||
|
||||
base64url_encode(B) ->
|
||||
D = base64_encode(B),
|
||||
D1 = binary:replace(D, <<"+">>, <<"-">>, [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) -> [$/, http_uri:encode(P)] end, Path)),
|
||||
URI =
|
||||
case Query of
|
||||
[] -> URI1;
|
||||
_ ->
|
||||
URI2 = str:join(
|
||||
lists:map(
|
||||
fun({K, V}) ->
|
||||
[http_uri:encode(K), $=, http_uri:encode(V)]
|
||||
end, Query), $&),
|
||||
<<URI1/binary, $?, URI2/binary>>
|
||||
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(<<EscU/binary, $%, EscS/binary>>, 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.
|
48
src/mod_matrix_gw_opt.erl
Normal file
48
src/mod_matrix_gw_opt.erl
Normal file
@ -0,0 +1,48 @@
|
||||
%% Generated automatically
|
||||
%% DO NOT EDIT: run `make options` instead
|
||||
|
||||
-module(mod_matrix_gw_opt).
|
||||
|
||||
-export([host/1]).
|
||||
-export([key/1]).
|
||||
-export([key_name/1]).
|
||||
-export([matrix_domain/1]).
|
||||
-export([matrix_id_as_jid/1]).
|
||||
-export([persist/1]).
|
||||
|
||||
-spec host(gen_mod:opts() | global | binary()) -> binary().
|
||||
host(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(host, Opts);
|
||||
host(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, host).
|
||||
|
||||
-spec key(gen_mod:opts() | global | binary()) -> any().
|
||||
key(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(key, Opts);
|
||||
key(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, key).
|
||||
|
||||
-spec key_name(gen_mod:opts() | global | binary()) -> binary().
|
||||
key_name(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(key_name, Opts);
|
||||
key_name(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, key_name).
|
||||
|
||||
-spec matrix_domain(gen_mod:opts() | global | binary()) -> binary().
|
||||
matrix_domain(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(matrix_domain, Opts);
|
||||
matrix_domain(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_domain).
|
||||
|
||||
-spec matrix_id_as_jid(gen_mod:opts() | global | binary()) -> boolean().
|
||||
matrix_id_as_jid(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(matrix_id_as_jid, Opts);
|
||||
matrix_id_as_jid(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_id_as_jid).
|
||||
|
||||
-spec persist(gen_mod:opts() | global | binary()) -> boolean().
|
||||
persist(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(persist, Opts);
|
||||
persist(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, persist).
|
||||
|
2652
src/mod_matrix_gw_room.erl
Normal file
2652
src/mod_matrix_gw_room.erl
Normal file
File diff suppressed because it is too large
Load Diff
583
src/mod_matrix_gw_s2s.erl
Normal file
583
src/mod_matrix_gw_s2s.erl
Normal file
@ -0,0 +1,583 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% File : mod_matrix_gw_s2s.erl
|
||||
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
||||
%%% Purpose : Matrix S2S
|
||||
%%% Created : 1 May 2022 by Alexey Shchepin <alexey@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2022 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_s2s).
|
||||
-if(?OTP_RELEASE >= 24).
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% API
|
||||
-export([start_link/2, supervisor/1, create_db/0,
|
||||
get_connection/2, check_auth/5, check_signature/2,
|
||||
get_matrix_host_port/2]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([init/1, terminate/3, code_change/4, callback_mode/0]).
|
||||
-export([handle_event/4]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-include("logger.hrl").
|
||||
-include("ejabberd_http.hrl").
|
||||
-include_lib("kernel/include/inet.hrl").
|
||||
|
||||
-record(matrix_s2s,
|
||||
{to :: binary(),
|
||||
pid :: pid()}).
|
||||
|
||||
-record(data,
|
||||
{host :: binary(),
|
||||
matrix_server :: binary(),
|
||||
matrix_host_port :: {binary(), integer()} | undefined,
|
||||
keys = #{},
|
||||
key_queue = #{}}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec start_link(binary(), binary()) ->
|
||||
{ok, Pid :: pid()} |
|
||||
ignore |
|
||||
{error, Error :: term()}.
|
||||
start_link(Host, MatrixServer) ->
|
||||
gen_statem:start_link(?MODULE, [Host, MatrixServer],
|
||||
ejabberd_config:fsm_limit_opts([])).
|
||||
|
||||
-spec supervisor(binary()) -> atom().
|
||||
supervisor(Host) ->
|
||||
gen_mod:get_module_proc(Host, mod_matrix_gw_s2s_sup).
|
||||
|
||||
create_db() ->
|
||||
ejabberd_mnesia:create(
|
||||
?MODULE, matrix_s2s,
|
||||
[{ram_copies, [node()]},
|
||||
{type, set},
|
||||
{attributes, record_info(fields, matrix_s2s)}]),
|
||||
ok.
|
||||
|
||||
get_connection(Host, MatrixServer) ->
|
||||
case mnesia:dirty_read(matrix_s2s, MatrixServer) of
|
||||
[] ->
|
||||
case supervisor:start_child(supervisor(Host),
|
||||
[Host, MatrixServer]) of
|
||||
{ok, undefined} -> {error, ignored};
|
||||
Res -> Res
|
||||
end;
|
||||
[#matrix_s2s{pid = Pid}] ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
get_key(Host, MatrixServer, KeyID) ->
|
||||
case mod_matrix_gw_opt:matrix_domain(Host) of
|
||||
MatrixServer ->
|
||||
{PubKey, _PrivKey} = mod_matrix_gw_opt:key(Host),
|
||||
TS = erlang:system_time(millisecond) + timer:hours(24 * 7),
|
||||
{ok, PubKey, TS};
|
||||
_ ->
|
||||
case get_connection(Host, MatrixServer) of
|
||||
{ok, S2SPid} ->
|
||||
gen_statem:call(S2SPid, {get_key, KeyID});
|
||||
Error -> Error
|
||||
end
|
||||
end.
|
||||
|
||||
get_matrix_host_port(Host, MatrixServer) ->
|
||||
case mod_matrix_gw_opt:matrix_domain(Host) of
|
||||
MatrixServer ->
|
||||
error;
|
||||
_ ->
|
||||
case get_connection(Host, MatrixServer) of
|
||||
{ok, S2SPid} ->
|
||||
gen_statem:call(S2SPid, get_matrix_host_port);
|
||||
Error -> Error
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
%process_query(Host, MatrixServer, AuthParams, Query, JSON, Request) ->
|
||||
% case get_connection(Host, MatrixServer) of
|
||||
% {ok, S2SPid} ->
|
||||
% #request{sockmod = SockMod, socket = Socket} = Request,
|
||||
% SockMod:controlling_process(Socket, S2SPid),
|
||||
% gen_statem:cast(S2SPid, {query, AuthParams, Query, JSON, Request}),
|
||||
% ok;
|
||||
% {error, _} = Error ->
|
||||
% Error
|
||||
% end.
|
||||
|
||||
check_auth(Host, MatrixServer, AuthParams, Content, Request) ->
|
||||
case get_connection(Host, MatrixServer) of
|
||||
{ok, S2SPid} ->
|
||||
#{<<"key">> := KeyID} = AuthParams,
|
||||
case catch gen_statem:call(S2SPid, {get_key, KeyID}) of
|
||||
{ok, VerifyKey, _ValidUntil} ->
|
||||
%% TODO: check ValidUntil
|
||||
Destination = mod_matrix_gw_opt:matrix_domain(Host),
|
||||
#{<<"sig">> := Sig} = AuthParams,
|
||||
JSON = #{<<"method">> => atom_to_binary(Request#request.method, latin1),
|
||||
<<"uri">> => Request#request.raw_path,
|
||||
<<"origin">> => MatrixServer,
|
||||
<<"destination">> => Destination,
|
||||
<<"signatures">> => #{
|
||||
MatrixServer => #{KeyID => Sig}
|
||||
}
|
||||
},
|
||||
JSON2 =
|
||||
case Content of
|
||||
none -> JSON;
|
||||
_ ->
|
||||
JSON#{<<"content">> => Content}
|
||||
end,
|
||||
case check_signature(JSON2, MatrixServer, KeyID, VerifyKey) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
?WARNING_MSG("Failed authentication: ~p", [JSON2]),
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
{error, _} = _Error ->
|
||||
false
|
||||
end.
|
||||
|
||||
check_signature(Host, JSON) ->
|
||||
case JSON of
|
||||
#{<<"sender">> := Sender,
|
||||
<<"signatures">> := Sigs} ->
|
||||
MatrixServer = mod_matrix_gw:get_id_domain_exn(Sender),
|
||||
case Sigs of
|
||||
#{MatrixServer := #{} = KeySig} ->
|
||||
case maps:next(maps:iterator(KeySig)) of
|
||||
{KeyID, _Sig, _} ->
|
||||
case catch get_key(Host, MatrixServer, KeyID) of
|
||||
{ok, VerifyKey, _ValidUntil} ->
|
||||
%% TODO: check ValidUntil
|
||||
case check_signature(JSON, MatrixServer, KeyID, VerifyKey) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
?WARNING_MSG("Failed authentication: ~p", [JSON]),
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec init(Args :: term()) ->
|
||||
{gen_statem:callback_mode(),
|
||||
State :: term(), Data :: term()} |
|
||||
{gen_statem:callback_mode(),
|
||||
State :: term(), Data :: term(),
|
||||
[gen_statem:action()] | gen_statem:action()} |
|
||||
ignore |
|
||||
{stop, Reason :: term()}.
|
||||
init([Host, MatrixServer]) ->
|
||||
mnesia:dirty_write(
|
||||
#matrix_s2s{to = MatrixServer,
|
||||
pid = self()}),
|
||||
{ok, state_name,
|
||||
#data{host = Host,
|
||||
matrix_server = MatrixServer}}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% If the gen_statem runs with CallbackMode =:= handle_event_function
|
||||
%% this function is called for every event a gen_statem receives.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_event(
|
||||
gen_statem:event_type(), Msg :: term(),
|
||||
State :: term(), Data :: term()) ->
|
||||
gen_statem:handle_event_result().
|
||||
%handle_event({call, From}, _Msg, State, Data) ->
|
||||
% {next_state, State, Data, [{reply, From, ok}]}.
|
||||
handle_event({call, From}, get_matrix_host_port, _State, Data) ->
|
||||
case Data#data.matrix_host_port of
|
||||
undefined ->
|
||||
Result = do_get_matrix_host_port(Data),
|
||||
Data2 = Data#data{matrix_host_port = Result},
|
||||
{keep_state, Data2, [{reply, From, Result}]};
|
||||
Result ->
|
||||
{keep_state_and_data, [{reply, From, Result}]}
|
||||
end;
|
||||
handle_event({call, From}, {get_key, KeyID}, State, Data) ->
|
||||
case maps:find(KeyID, Data#data.keys) of
|
||||
{ok, {ok, _, _} = Result} ->
|
||||
{keep_state, Data, [{reply, From, Result}]};
|
||||
{ok, error = Result} ->
|
||||
{keep_state, Data, [{reply, From, Result}]};
|
||||
{ok, pending} ->
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[From | Xs]
|
||||
end,
|
||||
[From],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{key_queue = KeyQueue}, []};
|
||||
error ->
|
||||
{MHost, MPort} = do_get_matrix_host_port(Data),
|
||||
URL = <<"https://", MHost/binary,
|
||||
":", (integer_to_binary(MPort))/binary,
|
||||
"/_matrix/key/v2/server/", KeyID/binary>>,
|
||||
Self = self(),
|
||||
httpc:request(get, {URL, []},
|
||||
[{timeout, 5000}],
|
||||
[{sync, false},
|
||||
{receiver,
|
||||
fun({_RequestId, Result}) ->
|
||||
gen_statem:cast(
|
||||
Self, {key_reply, KeyID, Result})
|
||||
end}]),
|
||||
Keys = (Data#data.keys)#{KeyID => pending},
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[From | Xs]
|
||||
end,
|
||||
[From],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{keys = Keys,
|
||||
key_queue = KeyQueue},
|
||||
[]}
|
||||
end;
|
||||
handle_event(cast, {query, AuthParams, _Query, _JSON, _Request} = Msg,
|
||||
State, Data) ->
|
||||
#{<<"key">> := KeyID} = AuthParams,
|
||||
case maps:find(KeyID, Data#data.keys) of
|
||||
{ok, {ok, VerifyKey, _ValidUntil}} ->
|
||||
Data2 = process_unverified_query(
|
||||
KeyID, VerifyKey, Msg, Data),
|
||||
{next_state, State, Data2, []};
|
||||
{ok, error} ->
|
||||
%TODO
|
||||
{next_state, State, Data, []};
|
||||
{ok, pending} ->
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[Msg | Xs]
|
||||
end,
|
||||
[Msg],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{key_queue = KeyQueue}, []};
|
||||
error ->
|
||||
{MHost, MPort} = do_get_matrix_host_port(Data),
|
||||
URL = <<"https://", MHost/binary,
|
||||
":", (integer_to_binary(MPort))/binary,
|
||||
"/_matrix/key/v2/server/", KeyID/binary>>,
|
||||
Self = self(),
|
||||
httpc:request(get, {URL, []},
|
||||
[{timeout, 5000}],
|
||||
[{sync, false},
|
||||
{receiver,
|
||||
fun({_RequestId, Result}) ->
|
||||
gen_statem:cast(
|
||||
Self, {key_reply, KeyID, Result})
|
||||
end}]),
|
||||
Keys = (Data#data.keys)#{KeyID => pending},
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[Msg | Xs]
|
||||
end,
|
||||
[Msg],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{keys = Keys,
|
||||
key_queue = KeyQueue},
|
||||
[]}
|
||||
end;
|
||||
handle_event(cast, {key_reply, KeyID, HTTPResult}, State, Data) ->
|
||||
case HTTPResult of
|
||||
{{_, 200, _}, _, SJSON} ->
|
||||
try
|
||||
JSON = jiffy:decode(SJSON, [return_maps]),
|
||||
?DEBUG("key ~p~n", [JSON]),
|
||||
#{<<"verify_keys">> := VerifyKeys} = JSON,
|
||||
#{KeyID := KeyData} = VerifyKeys,
|
||||
#{<<"key">> := SKey} = KeyData,
|
||||
VerifyKey = mod_matrix_gw:base64_decode(SKey),
|
||||
?DEBUG("key ~p~n", [VerifyKey]),
|
||||
?DEBUG("check ~p~n",
|
||||
[catch check_signature(
|
||||
JSON, Data#data.matrix_server,
|
||||
KeyID, VerifyKey)]),
|
||||
true = check_signature(
|
||||
JSON, Data#data.matrix_server,
|
||||
KeyID, VerifyKey),
|
||||
#{<<"valid_until_ts">> := ValidUntil} = JSON,
|
||||
ValidUntil2 =
|
||||
min(ValidUntil,
|
||||
erlang:system_time(millisecond) + timer:hours(24 * 7)),
|
||||
Keys = (Data#data.keys)#{KeyID => {ok, VerifyKey, ValidUntil2}},
|
||||
Froms = maps:get(KeyID, Data#data.key_queue, []),
|
||||
KeyQueue = maps:remove(KeyID, Data#data.key_queue),
|
||||
Data2 = Data#data{keys = Keys,
|
||||
key_queue = KeyQueue},
|
||||
Replies =
|
||||
lists:map(
|
||||
fun(From) ->
|
||||
{reply, From, {ok, VerifyKey, ValidUntil2}}
|
||||
end, Froms),
|
||||
?DEBUG("KEYS ~p~n", [{Keys, Data2}]),
|
||||
{next_state, State, Data2, Replies}
|
||||
catch
|
||||
_:_ ->
|
||||
%% TODO
|
||||
Keys2 = (Data#data.keys)#{KeyID => error},
|
||||
{next_state, State, Data#data{keys = Keys2}, []}
|
||||
end;
|
||||
_ ->
|
||||
%% TODO
|
||||
Keys = (Data#data.keys)#{KeyID => error},
|
||||
{next_state, State, Data#data{keys = Keys}, []}
|
||||
end;
|
||||
handle_event(cast, Msg, State, Data) ->
|
||||
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
||||
{next_state, State, Data, []};
|
||||
handle_event(info, Info, State, Data) ->
|
||||
?WARNING_MSG("Unexpected info: ~p", [Info]),
|
||||
{next_state, State, Data, []}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% This function is called by a gen_statem when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_statem terminates with
|
||||
%% Reason. The return value is ignored.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec terminate(Reason :: term(), State :: term(), Data :: term()) ->
|
||||
any().
|
||||
terminate(_Reason, _State, Data) ->
|
||||
mnesia:dirty_delete_object(
|
||||
#matrix_s2s{to = Data#data.matrix_server,
|
||||
pid = self()}),
|
||||
%% TODO: wait for messages
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Convert process state when code is changed
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec code_change(
|
||||
OldVsn :: term() | {down,term()},
|
||||
State :: term(), Data :: term(), Extra :: term()) ->
|
||||
{ok, NewState :: term(), NewData :: term()}.
|
||||
code_change(_OldVsn, State, Data, _Extra) ->
|
||||
{ok, State, Data}.
|
||||
|
||||
callback_mode() ->
|
||||
handle_event_function.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
do_get_matrix_host_port(Data) ->
|
||||
MatrixServer = Data#data.matrix_server,
|
||||
case binary:split(MatrixServer, <<":">>) of
|
||||
[Addr] ->
|
||||
case inet:parse_address(binary_to_list(Addr)) of
|
||||
{ok, _} ->
|
||||
{Addr, 8448};
|
||||
_ ->
|
||||
URL = <<"https://", Addr/binary, "/.well-known/matrix/server">>,
|
||||
HTTPRes =
|
||||
httpc:request(get, {URL, []},
|
||||
[{timeout, 5000}],
|
||||
[{sync, true},
|
||||
{body_format, binary}]),
|
||||
?DEBUG("HTTPRes ~p~n", [HTTPRes]),
|
||||
Res =
|
||||
case HTTPRes of
|
||||
{ok, {{_, 200, _}, _Headers, Body}} ->
|
||||
try
|
||||
case jiffy:decode(Body, [return_maps]) of
|
||||
#{<<"m.server">> := Server} ->
|
||||
case binary:split(Server, <<":">>) of
|
||||
[ServerAddr] ->
|
||||
{ServerAddr, 8448};
|
||||
[ServerAddr, ServerPort] ->
|
||||
{ServerAddr, binary_to_integer(ServerPort)}
|
||||
end
|
||||
end
|
||||
catch
|
||||
_:_ ->
|
||||
error
|
||||
end;
|
||||
_ ->
|
||||
error
|
||||
end,
|
||||
case Res of
|
||||
error ->
|
||||
SRVName =
|
||||
"_matrix._tcp." ++ binary_to_list(MatrixServer),
|
||||
case inet_res:getbyname(SRVName, srv, 5000) of
|
||||
{ok, HostEntry} ->
|
||||
case h_addr_list_to_host_ports(
|
||||
HostEntry#hostent.h_addr_list) of
|
||||
{ok, [{Host, Port} | _]} ->
|
||||
{list_to_binary(Host), Port};
|
||||
_ ->
|
||||
{MatrixServer, 8448}
|
||||
end;
|
||||
{error, _} ->
|
||||
{MatrixServer, 8448}
|
||||
end;
|
||||
_ ->
|
||||
Res
|
||||
end
|
||||
end;
|
||||
[Addr, SPort] ->
|
||||
case catch binary_to_integer(SPort) of
|
||||
Port when is_integer(Port) ->
|
||||
{Addr, Port};
|
||||
_ ->
|
||||
error
|
||||
end
|
||||
end.
|
||||
|
||||
%% Copied from xmpp_stream_out.erl
|
||||
-type host_port() :: {inet:hostname(), inet:port_number()}.
|
||||
-type h_addr_list() :: [{integer(), integer(), inet:port_number(), string()}].
|
||||
-spec h_addr_list_to_host_ports(h_addr_list()) -> {ok, [host_port(),...]} |
|
||||
{error, nxdomain}.
|
||||
h_addr_list_to_host_ports(AddrList) ->
|
||||
PrioHostPorts = lists:flatmap(
|
||||
fun({Priority, Weight, Port, Host}) ->
|
||||
N = case Weight of
|
||||
0 -> 0;
|
||||
_ -> (Weight + 1) * p1_rand:uniform()
|
||||
end,
|
||||
[{Priority * 65536 - N, Host, Port}];
|
||||
(_) ->
|
||||
[]
|
||||
end, AddrList),
|
||||
HostPorts = [{Host, Port}
|
||||
|| {_Priority, Host, Port} <- lists:usort(PrioHostPorts)],
|
||||
case HostPorts of
|
||||
[] -> {error, nxdomain};
|
||||
_ -> {ok, HostPorts}
|
||||
end.
|
||||
|
||||
|
||||
check_signature(JSON, SignatureName, KeyID, VerifyKey) ->
|
||||
try
|
||||
#{<<"signatures">> := Signatures} = JSON,
|
||||
#{SignatureName := SignatureData} = Signatures,
|
||||
#{KeyID := SSignature} = SignatureData,
|
||||
Signature = mod_matrix_gw:base64_decode(SSignature),
|
||||
JSON2 = maps:without([<<"signatures">>, <<"unsigned">>], JSON),
|
||||
Msg = mod_matrix_gw:encode_canonical_json(JSON2),
|
||||
public_key:verify(Msg, ignored, Signature, {ed_pub, ed25519, VerifyKey})
|
||||
catch
|
||||
_:_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
%process_unverified_queries(KeyID, Data) ->
|
||||
% case maps:find(KeyID, Data#data.keys) of
|
||||
% {ok, {ok, VerifyKey, _ValidUntil}} ->
|
||||
% Queue = maps:get(KeyID, Data#data.key_queue, []),
|
||||
% KeyQueue = maps:remove(KeyID, Data#data.key_queue),
|
||||
% Data2 = Data#data{key_queue = KeyQueue},
|
||||
% lists:foldl(
|
||||
% fun(Query, DataAcc) ->
|
||||
% process_unverified_query(KeyID, VerifyKey, Query, DataAcc)
|
||||
% end, Data2, Queue);
|
||||
% _ ->
|
||||
% %% TODO
|
||||
% Data
|
||||
% end.
|
||||
|
||||
process_unverified_query(
|
||||
KeyID, VerifyKey, {query, AuthParams, _Query, Content, Request} = _Msg, Data) ->
|
||||
Destination = mod_matrix_gw_opt:matrix_domain(Data#data.host),
|
||||
#{<<"sig">> := Sig} = AuthParams,
|
||||
JSON = #{<<"method">> => atom_to_binary(Request#request.method, latin1),
|
||||
<<"uri">> => Request#request.raw_path,
|
||||
<<"origin">> => Data#data.matrix_server,
|
||||
<<"destination">> => Destination,
|
||||
<<"signatures">> => #{
|
||||
Data#data.matrix_server => #{KeyID => Sig}
|
||||
}
|
||||
},
|
||||
JSON2 =
|
||||
case Content of
|
||||
none -> JSON;
|
||||
_ ->
|
||||
JSON#{<<"content">> => Content}
|
||||
end,
|
||||
case check_signature(JSON2, Data#data.matrix_server, KeyID, VerifyKey) of
|
||||
true ->
|
||||
todo_remove_me;
|
||||
%process_query(Msg, Data);
|
||||
false ->
|
||||
?WARNING_MSG("Failed authentication: ~p", [JSON]),
|
||||
%% TODO
|
||||
Data
|
||||
end.
|
||||
|
||||
-endif.
|
77
src/mod_matrix_gw_sup.erl
Normal file
77
src/mod_matrix_gw_sup.erl
Normal file
@ -0,0 +1,77 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% Created : 1 May 2022 by Alexey Shchepin <alexey@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2022 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_sup).
|
||||
-if(?OTP_RELEASE >= 24).
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start/1, start_link/1, procname/1]).
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API functions
|
||||
%%%===================================================================
|
||||
start(Host) ->
|
||||
Spec = #{id => procname(Host),
|
||||
start => {?MODULE, start_link, [Host]},
|
||||
restart => permanent,
|
||||
shutdown => infinity,
|
||||
type => supervisor,
|
||||
modules => [?MODULE]},
|
||||
supervisor:start_child(ejabberd_gen_mod_sup, Spec).
|
||||
|
||||
start_link(Host) ->
|
||||
Proc = procname(Host),
|
||||
supervisor:start_link({local, Proc}, ?MODULE, [Host]).
|
||||
|
||||
-spec procname(binary()) -> atom().
|
||||
procname(Host) ->
|
||||
gen_mod:get_module_proc(Host, ?MODULE).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Supervisor callbacks
|
||||
%%%===================================================================
|
||||
init([Host]) ->
|
||||
S2SName = mod_matrix_gw_s2s:supervisor(Host),
|
||||
RoomName = mod_matrix_gw_room:supervisor(Host),
|
||||
Specs =
|
||||
[#{id => S2SName,
|
||||
start => {ejabberd_tmp_sup, start_link, [S2SName, mod_matrix_gw_s2s]},
|
||||
restart => permanent,
|
||||
shutdown => infinity,
|
||||
type => supervisor,
|
||||
modules => [ejabberd_tmp_sup]},
|
||||
#{id => RoomName,
|
||||
start => {ejabberd_tmp_sup, start_link, [RoomName, mod_matrix_gw_room]},
|
||||
restart => permanent,
|
||||
shutdown => infinity,
|
||||
type => supervisor,
|
||||
modules => [ejabberd_tmp_sup]},
|
||||
#{id => mod_matrix_gw:procname(Host),
|
||||
start => {mod_matrix_gw, start_link, [Host]},
|
||||
restart => permanent,
|
||||
shutdown => timer:minutes(1),
|
||||
type => worker,
|
||||
modules => [mod_matrix_gw]}],
|
||||
{ok, {{one_for_one, 10, 1}, Specs}}.
|
||||
-endif.
|
Loading…
Reference in New Issue
Block a user