Packages
ejabberd
26.4.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_mqtt_bridge.erl
%%%-------------------------------------------------------------------
%%% @author Pawel Chmielowski <pawel@process-one.net>
%%% @copyright (C) 2002-2026 ProcessOne, SARL. All Rights Reserved.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%%-------------------------------------------------------------------
-module(mod_mqtt_bridge).
-behaviour(gen_mod).
%% gen_mod API
-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]).
-export([mod_doc/0]).
%% API
-export([mqtt_publish_hook/3]).
-include("logger.hrl").
-include("mqtt.hrl").
-include("translate.hrl").
%%%===================================================================
%%% API
%%%===================================================================
start(_Host, Opts) ->
User = mod_mqtt_bridge_opt:replication_user(Opts),
start_servers(User, element(1, mod_mqtt_bridge_opt:servers(Opts))),
{ok, [{hook, mqtt_publish, mqtt_publish_hook, 50}]}.
stop(Host) ->
stop_servers(element(1, mod_mqtt_bridge_opt:servers(Host))),
ok.
start_servers(User, Servers) ->
lists:foldl(
fun({Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication}, Started) ->
case Started of
#{Proc := _} ->
?DEBUG("Already started ~p", [Proc]),
Started;
_ ->
ChildSpec = {Proc,
{mod_mqtt_bridge_session, start_link,
[Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication, User]},
transient,
1000,
worker,
[mod_mqtt_bridge_session]},
Res = supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec),
?DEBUG("Starting ~p ~p", [Proc, Res]),
Started#{Proc => true}
end
end, #{}, Servers).
stop_servers(Servers) ->
lists:foreach(
fun({Proc, _Transport, _Host, _Port, _Path, _Publish, _Subscribe, _Authentication}) ->
try p1_server:call(Proc, stop)
catch _:_ -> ok
end,
supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
supervisor:delete_child(ejabberd_gen_mod_sup, Proc)
end, Servers).
reload(_Host, NewOpts, OldOpts) ->
OldServers = element(1, mod_mqtt_bridge_opt:servers(OldOpts)),
NewServers = element(1, mod_mqtt_bridge_opt:servers(NewOpts)),
Deleted = lists:filter(
fun(E) -> not lists:keymember(element(1, E), 1, NewServers) end,
OldServers),
Added = lists:filter(
fun(E) -> not lists:keymember(element(1, E), 1, OldServers) end,
NewServers),
stop_servers(Deleted),
start_servers(mod_mqtt_bridge_opt:replication_user(NewOpts), Added),
ok.
depends(_Host, _Opts) ->
[{mod_mqtt, hard}].
proc_name(Proto, Host, Port, Path) ->
HostB = list_to_binary(Host),
TransportB = list_to_binary(Proto),
PathB = case Path of
V when is_list(V) ->
list_to_binary(V);
_ -> <<>>
end,
binary_to_atom(<<"mod_mqtt_bridge_", TransportB/binary, "_", HostB/binary,
"_", (integer_to_binary(Port))/binary, PathB/binary>>, utf8).
-spec mqtt_publish_hook(jid:ljid(), publish(), non_neg_integer()) -> ok.
mqtt_publish_hook({_, S, _}, #publish{topic = Topic} = Pkt, _ExpiryTime) ->
{_, Publish} = mod_mqtt_bridge_opt:servers(S),
case maps:find(Topic, Publish) of
error -> ok;
{ok, Procs} ->
lists:foreach(
fun(Proc) ->
Proc ! {publish, Pkt}
end, Procs)
end.
%%%===================================================================
%%% Options
%%%===================================================================
-spec mod_options(binary()) ->
[{servers,
{[{atom(), mqtt | mqtts | mqtt5 | mqtt5s, binary(), non_neg_integer(),
#{binary() => binary()}, #{binary() => binary()}, map()}],
#{binary() => [atom()]}}} |
{atom(), any()}].
mod_options(Host) ->
[{servers, []},
{replication_user, jid:make(<<"admin">>, Host)}].
mod_opt_type(replication_user) ->
econf:jid();
mod_opt_type(servers) ->
econf:and_then(
econf:map(econf:url([mqtt, mqtts, mqtt5, mqtt5s, ws, wss, ws5, wss5]),
econf:options(
#{
publish => econf:map(econf:binary(), econf:binary(), [{return, map}]),
subscribe => econf:map(econf:binary(), econf:binary(), [{return, map}]),
authentication => econf:either(
econf:options(
#{
username => econf:binary(),
password => econf:binary()
}, [{return, map}]),
econf:options(
#{
certfile => econf:pem()
}, [{return, map}]))},
[{return, map}]),
[{return, map}]),
fun(Servers) ->
maps:fold(
fun(Url, Opts, {HAcc, PAcc}) ->
{ok, Scheme, _UserInfo, Host, Port, Path, _Query} =
misc:uri_parse(Url, [{mqtt, 1883}, {mqtts, 8883},
{mqtt5, 1883}, {mqtt5s, 8883},
{ws, 80}, {wss, 443},
{ws5, 80}, {wss5, 443}]),
Publish = maps:get(publish, Opts, #{}),
Subscribe = maps:get(subscribe, Opts, #{}),
Authentication = maps:get(authentication, Opts, []),
Proto = list_to_atom(Scheme),
Proc = proc_name(Scheme, Host, Port, Path),
PAcc2 = maps:fold(
fun(Topic, _RemoteTopic, Acc) ->
maps:update_with(Topic, fun(V) -> [Proc | V] end, [Proc], Acc)
end, PAcc, Publish),
{[{Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication} | HAcc], PAcc2}
end, {[], #{}}, Servers)
end
).
%%%===================================================================
%%% Doc
%%%===================================================================
mod_doc() ->
#{desc =>
[?T("This module adds ability to synchronize local MQTT topics with data on remote servers"),
?T("It can update topics on remote servers when local user updates local topic, or can subscribe "
"for changes on remote server, and update local copy when remote data is updated."),
?T("It is available since ejabberd 23.01.")],
example =>
["modules:",
" mod_mqtt_bridge:",
" replication_user: \"mqtt@xmpp.server.com\"",
" servers:",
" \"mqtt://server.com\":",
" authentication:",
" certfile: \"/etc/ejabberd/mqtt_server.pem\"",
" publish:",
" \"localA\": \"remoteA\" # local changes to 'localA' will be replicated on remote server as 'remoteA'",
" \"topicB\": \"topicB\"",
" subscribe:",
" \"remoteB\": \"localB\" # changes to 'remoteB' on remote server will be stored as 'localB' on local server"],
opts =>
[{servers,
#{value => "{ServerUrl: {Key: Value}}",
desc =>
?T("Declaration of data to share for each ServerUrl. "
"Server URLs can use schemas: 'mqtt', 'mqtts' (mqtt with tls), 'mqtt5', "
"'mqtt5s' (both to trigger v5 protocol), 'ws', 'wss', 'ws5', 'wss5'. "
"Keys must be:")},
[{authentication,
#{value => "{AuthKey: AuthValue}",
desc => ?T("List of authentication information, where AuthKey can be: "
"'username' and 'password' fields, or 'certfile' pointing to client certificate. "
"Certificate authentication can be used only with mqtts, mqtt5s, wss, wss5.")}},
{publish,
#{value => "{LocalTopic: RemoteTopic}",
desc => ?T("Either publish or subscribe must be set, or both.")}},
{subscribe,
#{value => "{RemoteTopic: LocalTopic}",
desc => ?T("Either publish or subscribe must be set, or both.")}}]},
{replication_user,
#{value => "JID",
desc =>
?T("Identifier of a user that will be assigned as owner of local changes.")}}]}.
%%%===================================================================
%%% Internal functions
%%%===================================================================