aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>2016-11-12 13:27:15 +0300
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>2016-11-12 13:27:15 +0300
commit78a44e01762e00102f5e3e3f0b49690cc7866c31 (patch)
treeb8ac7773f510ee3c1da4802bce2badc71c34c0b2
parentAdd more tests for offline storage (diff)
parentSupport several groups separated by ; in add_rosteritem command (diff)
Merge branch 'master' into xml-ng
Conflicts: src/adhoc.erl src/cyrsasl_oauth.erl src/ejabberd_c2s.erl src/ejabberd_config.erl src/ejabberd_service.erl src/gen_mod.erl src/mod_admin_extra.erl src/mod_announce.erl src/mod_carboncopy.erl src/mod_client_state.erl src/mod_configure.erl src/mod_echo.erl src/mod_mam.erl src/mod_muc.erl src/mod_muc_room.erl src/mod_offline.erl src/mod_pubsub.erl src/mod_stats.erl src/node_flat_sql.erl src/randoms.erl
-rw-r--r--Makefile.in5
-rw-r--r--config/config.exs2
-rw-r--r--config/ejabberd.exs169
-rw-r--r--config/ejabberd.yml667
-rw-r--r--ejabberd.service.template7
-rw-r--r--ejabberd.yml.example10
-rw-r--r--include/ejabberd_commands.hrl25
-rw-r--r--include/ejabberd_oauth.hrl26
-rw-r--r--include/ejabberd_service.hrl20
-rw-r--r--include/ejabberd_sm.hrl4
-rw-r--r--include/mod_muc_room.hrl10
-rw-r--r--include/ns.hrl2
-rw-r--r--include/xmpp_codec.hrl51
-rw-r--r--lib/ct_formatter.ex2
-rw-r--r--lib/ejabberd/config/attr.ex119
-rw-r--r--lib/ejabberd/config/config.ex145
-rw-r--r--lib/ejabberd/config/ejabberd_hook.ex23
-rw-r--r--lib/ejabberd/config/ejabberd_module.ex70
-rw-r--r--lib/ejabberd/config/logger/ejabberd_logger.ex32
-rw-r--r--lib/ejabberd/config/opts_formatter.ex46
-rw-r--r--lib/ejabberd/config/store.ex55
-rw-r--r--lib/ejabberd/config/validator/validation.ex40
-rw-r--r--lib/ejabberd/config/validator/validator_attrs.ex28
-rw-r--r--lib/ejabberd/config/validator/validator_dependencies.ex30
-rw-r--r--lib/ejabberd/config/validator/validator_utility.ex30
-rw-r--r--lib/ejabberd/config_util.ex18
-rw-r--r--lib/ejabberd/module.ex19
-rw-r--r--lib/mix/tasks/deps.tree.ex94
-rw-r--r--lib/mod_presence_demo.ex13
-rw-r--r--mix.exs69
-rw-r--r--mix.lock33
-rw-r--r--priv/msgs/cs.msg5
-rw-r--r--priv/msgs/cs.po8
-rw-r--r--rebar.config23
-rw-r--r--rebar.config.script31
-rw-r--r--specs/xmpp_codec.spec65
-rw-r--r--sql/lite.sql7
-rw-r--r--sql/mssql.sql10
-rw-r--r--sql/mysql.sql7
-rw-r--r--sql/pg.sql9
-rw-r--r--src/acl.erl62
-rw-r--r--src/cyrsasl_oauth.erl2
-rw-r--r--src/cyrsasl_scram.erl4
-rw-r--r--src/ejabberd.erl4
-rw-r--r--src/ejabberd_access_permissions.erl543
-rw-r--r--src/ejabberd_admin.erl16
-rw-r--r--src/ejabberd_app.erl28
-rw-r--r--src/ejabberd_auth_mnesia.erl2
-rw-r--r--src/ejabberd_auth_riak.erl2
-rw-r--r--src/ejabberd_auth_sql.erl2
-rw-r--r--src/ejabberd_c2s.erl201
-rw-r--r--src/ejabberd_commands.erl352
-rw-r--r--src/ejabberd_config.erl71
-rw-r--r--src/ejabberd_ctl.erl23
-rw-r--r--src/ejabberd_http.erl17
-rw-r--r--src/ejabberd_http_bind.erl3
-rw-r--r--src/ejabberd_http_ws.erl3
-rw-r--r--src/ejabberd_local.erl25
-rw-r--r--src/ejabberd_oauth.erl371
-rw-r--r--src/ejabberd_oauth_mnesia.erl65
-rw-r--r--src/ejabberd_oauth_rest.erl98
-rw-r--r--src/ejabberd_oauth_sql.erl78
-rw-r--r--src/ejabberd_s2s.erl26
-rw-r--r--src/ejabberd_s2s_out.erl3
-rw-r--r--src/ejabberd_service.erl94
-rw-r--r--src/ejabberd_sm.erl54
-rw-r--r--src/ejabberd_sm_redis.erl7
-rw-r--r--src/ejabberd_sql.erl8
-rw-r--r--src/ejabberd_web_admin.erl54
-rw-r--r--src/ejabberd_xmlrpc.erl80
-rw-r--r--src/ext_mod.erl37
-rw-r--r--src/extauth.erl3
-rw-r--r--src/gen_mod.erl63
-rw-r--r--src/http_p1.erl358
-rw-r--r--src/jid.erl26
-rw-r--r--src/jlib.erl36
-rw-r--r--src/mod_admin_extra.erl72
-rw-r--r--src/mod_announce.erl14
-rw-r--r--src/mod_carboncopy.erl6
-rw-r--r--src/mod_client_state.erl50
-rw-r--r--src/mod_configure.erl36
-rw-r--r--src/mod_delegation.erl325
-rw-r--r--src/mod_echo.erl2
-rw-r--r--src/mod_http_api.erl319
-rw-r--r--src/mod_http_upload_quota.erl6
-rw-r--r--src/mod_irc.erl2
-rw-r--r--src/mod_mam.erl109
-rw-r--r--src/mod_mix.erl2
-rw-r--r--src/mod_muc.erl119
-rw-r--r--src/mod_muc_admin.erl147
-rw-r--r--src/mod_muc_log.erl2
-rw-r--r--src/mod_muc_room.erl429
-rw-r--r--src/mod_offline.erl28
-rw-r--r--src/mod_offline_sql.erl2
-rw-r--r--src/mod_privacy_sql.erl2
-rw-r--r--src/mod_privilege.erl348
-rw-r--r--src/mod_pubsub.erl2
-rw-r--r--src/mod_roster.erl92
-rw-r--r--src/mod_stats.erl9
-rw-r--r--src/node_flat_sql.erl11
-rw-r--r--src/node_mb.erl3
-rw-r--r--src/node_mb_sql.erl158
-rw-r--r--src/nodetree_tree_sql.erl6
-rw-r--r--src/randoms.erl21
-rw-r--r--src/rest.erl181
-rw-r--r--src/xmpp_codec.erl547
-rw-r--r--src/xmpp_util.erl14
-rw-r--r--test/acl_test.exs1
-rw-r--r--test/ejabberd_admin_test.exs1
-rw-r--r--test/ejabberd_commands_mock_test.exs56
-rw-r--r--test/ejabberd_commands_test.exs23
-rw-r--r--test/ejabberd_cyrsasl_test.exs10
-rw-r--r--test/ejabberd_oauth_mock.exs7
-rw-r--r--test/elixir-config/attr_test.exs87
-rw-r--r--test/elixir-config/config_test.exs65
-rw-r--r--test/elixir-config/ejabberd_logger.exs49
-rw-r--r--test/elixir-config/shared/ejabberd.exs31
-rw-r--r--test/elixir-config/shared/ejabberd_different_from_default.exs9
-rw-r--r--test/elixir-config/shared/ejabberd_for_validation.exs20
-rw-r--r--test/elixir-config/validation_test.exs32
-rw-r--r--test/mod_admin_extra_test.exs4
-rw-r--r--test/mod_http_api_mock_test.exs97
-rw-r--r--test/mod_http_api_test.exs39
-rw-r--r--test/test_helper.exs7
124 files changed, 7129 insertions, 1323 deletions
diff --git a/Makefile.in b/Makefile.in
index 728b2fa98..b7c229fcc 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -211,6 +211,11 @@ install: all copy-files
> ejabberd.init
chmod 755 ejabberd.init
#
+ # Service script
+ $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*" ejabberd.service.template \
+ > ejabberd.service
+ chmod 755 ejabberd.service
+ #
# Spool directory
$(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR)
$(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT)
diff --git a/config/config.exs b/config/config.exs
index 4d3783480..0d1a3c720 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -4,7 +4,7 @@ use Mix.Config
config :ejabberd,
file: "config/ejabberd.yml",
log_path: 'log/ejabberd.log'
-
+
# Customize Mnesia directory:
config :mnesia,
dir: 'mnesiadb/'
diff --git a/config/ejabberd.exs b/config/ejabberd.exs
new file mode 100644
index 000000000..05c2b5d83
--- /dev/null
+++ b/config/ejabberd.exs
@@ -0,0 +1,169 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ log_rotate_size: 10485760,
+ log_rotate_date: "",
+ log_rotate_count: 1,
+ log_rate_limit: 100,
+ auth_method: :internal,
+ max_fsm_queue: 1000,
+ language: "en",
+ allow_contrib_modules: true,
+ hosts: ["localhost"],
+ shaper: shaper,
+ acl: acl,
+ access: access]
+ end
+
+ defp shaper do
+ [normal: 1000,
+ fast: 50000,
+ max_fsm_queue: 1000]
+ end
+
+ defp acl do
+ [local:
+ [user_regexp: "", loopback: [ip: "127.0.0.0/8"]]]
+ end
+
+ defp access do
+ [max_user_sessions: [all: 10],
+ max_user_offline_messages: [admin: 5000, all: 100],
+ local: [local: :allow],
+ c2s: [blocked: :deny, all: :allow],
+ c2s_shaper: [admin: :none, all: :normal],
+ s2s_shaper: [all: :fast],
+ announce: [admin: :allow],
+ configure: [admin: :allow],
+ muc_admin: [admin: :allow],
+ muc_create: [local: :allow],
+ muc: [all: :allow],
+ pubsub_createnode: [local: :allow],
+ register: [all: :allow],
+ trusted_network: [loopback: :allow]]
+ end
+
+ listen :ejabberd_c2s do
+ @opts [
+ port: 5222,
+ max_stanza_size: 65536,
+ shaper: :c2s_shaper,
+ access: :c2s]
+ end
+
+ listen :ejabberd_s2s_in do
+ @opts [port: 5269]
+ end
+
+ listen :ejabberd_http do
+ @opts [
+ port: 5280,
+ web_admin: true,
+ http_poll: true,
+ http_bind: true,
+ captcha: true]
+ end
+
+ module :mod_adhoc do
+ end
+
+ module :mod_announce do
+ @opts [access: :announce]
+ end
+
+ module :mod_blocking do
+ end
+
+ module :mod_caps do
+ end
+
+ module :mod_carboncopy do
+ end
+
+ module :mod_client_state do
+ @opts [
+ drop_chat_states: true,
+ queue_presence: false]
+ end
+
+ module :mod_configure do
+ end
+
+ module :mod_disco do
+ end
+
+ module :mod_irc do
+ end
+
+ module :mod_http_bind do
+ end
+
+ module :mod_last do
+ end
+
+ module :mod_muc do
+ @opts [
+ access: :muc,
+ access_create: :muc_create,
+ access_persistent: :muc_create,
+ access_admin: :muc_admin]
+ end
+
+ module :mod_offline do
+ @opts [access_max_user_messages: :max_user_offline_messages]
+ end
+
+ module :mod_ping do
+ end
+
+ module :mod_privacy do
+ end
+
+ module :mod_private do
+ end
+
+ module :mod_pubsub do
+ @opts [
+ access_createnode: :pubsub_createnode,
+ ignore_pep_from_offline: true,
+ last_item_cache: true,
+ plugins: ["flat", "hometree", "pep"]]
+ end
+
+ module :mod_register do
+ @opts [welcome_message: [
+ subject: "Welcome!",
+ body: "Hi.\nWelcome to this XMPP Server",
+ ip_access: :trusted_network,
+ access: :register]]
+ end
+
+ module :mod_roster do
+ end
+
+ module :mod_shared_roster do
+ end
+
+ module :mod_stats do
+ end
+
+ module :mod_time do
+ end
+
+ module :mod_version do
+ end
+
+ # Example of how to define a hook, called when the event
+ # specified is triggered.
+ #
+ # @event: Name of the event
+ # @opts: Params are optional. Available: :host and :priority.
+ # If missing, defaults are used. (host: :global | priority: 50)
+ # @callback Could be an anonymous function or a callback from a module,
+ # use the &ModuleName.function/arity format for that.
+ hook :register_user, [host: "localhost"], fn(user, server) ->
+ info("User registered: #{user} on #{server}")
+ end
+end
diff --git a/config/ejabberd.yml b/config/ejabberd.yml
new file mode 100644
index 000000000..80fc3c622
--- /dev/null
+++ b/config/ejabberd.yml
@@ -0,0 +1,667 @@
+###
+### ejabberd configuration file
+###
+###
+
+### The parameters used in this configuration file are explained in more detail
+### in the ejabberd Installation and Operation Guide.
+### Please consult the Guide in case of doubts, it is included with
+### your copy of ejabberd, and is also available online at
+### http://www.process-one.net/en/ejabberd/docs/
+
+### The configuration file is written in YAML.
+### Refer to http://en.wikipedia.org/wiki/YAML for the brief description.
+### However, ejabberd treats different literals as different types:
+###
+### - unquoted or single-quoted strings. They are called "atoms".
+### Example: dog, 'Jupiter', '3.14159', YELLOW
+###
+### - numeric literals. Example: 3, -45.0, .0
+###
+### - quoted or folded strings.
+### Examples of quoted string: "Lizzard", "orange".
+### Example of folded string:
+### > Art thou not Romeo,
+### and a Montague?
+
+### =======
+### LOGGING
+
+##
+## loglevel: Verbosity of log files generated by ejabberd.
+## 0: No ejabberd log at all (not recommended)
+## 1: Critical
+## 2: Error
+## 3: Warning
+## 4: Info
+## 5: Debug
+##
+loglevel: 4
+
+##
+## rotation: Describe how to rotate logs. Either size and/or date can trigger
+## log rotation. Setting count to N keeps N rotated logs. Setting count to 0
+## does not disable rotation, it instead rotates the file and keeps no previous
+## versions around. Setting size to X rotate log when it reaches X bytes.
+## To disable rotation set the size to 0 and the date to ""
+## Date syntax is taken from the syntax newsyslog uses in newsyslog.conf.
+## Some examples:
+## $D0 rotate every night at midnight
+## $D23 rotate every day at 23:00 hr
+## $W0D23 rotate every week on Sunday at 23:00 hr
+## $W5D16 rotate every week on Friday at 16:00 hr
+## $M1D0 rotate on the first day of every month at midnight
+## $M5D6 rotate on every 5th day of the month at 6:00 hr
+##
+log_rotate_size: 10485760
+log_rotate_date: ""
+log_rotate_count: 1
+
+##
+## overload protection: If you want to limit the number of messages per second
+## allowed from error_logger, which is a good idea if you want to avoid a flood
+## of messages when system is overloaded, you can set a limit.
+## 100 is ejabberd's default.
+log_rate_limit: 100
+
+##
+## watchdog_admins: Only useful for developers: if an ejabberd process
+## consumes a lot of memory, send live notifications to these XMPP
+## accounts.
+##
+## watchdog_admins:
+## - "bob@example.com"
+
+
+### ================
+### SERVED HOSTNAMES
+
+##
+## hosts: Domains served by ejabberd.
+## You can define one or several, for example:
+## hosts:
+## - "example.net"
+## - "example.com"
+## - "example.org"
+##
+hosts:
+ - "localhost"
+
+##
+## route_subdomains: Delegate subdomains to other XMPP servers.
+## For example, if this ejabberd serves example.org and you want
+## to allow communication with an XMPP server called im.example.org.
+##
+## route_subdomains: s2s
+
+### ===============
+### LISTENING PORTS
+
+##
+## listen: The ports ejabberd will listen on, which service each is handled
+## by and what options to start it with.
+##
+listen:
+ -
+ port: 5222
+ module: ejabberd_c2s
+ ##
+ ## If TLS is compiled in and you installed a SSL
+ ## certificate, specify the full path to the
+ ## file and uncomment these lines:
+ ##
+ ## certfile: "/path/to/ssl.pem"
+ ## starttls: true
+ ##
+ ## To enforce TLS encryption for client connections,
+ ## use this instead of the "starttls" option:
+ ##
+ ## starttls_required: true
+ ##
+ ## Custom OpenSSL options
+ ##
+ ## protocol_options:
+ ## - "no_sslv3"
+ ## - "no_tlsv1"
+ max_stanza_size: 65536
+ shaper: c2s_shaper
+ access: c2s
+ -
+ port: 5269
+ module: ejabberd_s2s_in
+ ##
+ ## ejabberd_service: Interact with external components (transports, ...)
+ ##
+ ## -
+ ## port: 8888
+ ## module: ejabberd_service
+ ## access: all
+ ## shaper_rule: fast
+ ## ip: "127.0.0.1"
+ ## hosts:
+ ## "icq.example.org":
+ ## password: "secret"
+ ## "sms.example.org":
+ ## password: "secret"
+
+ ##
+ ## ejabberd_stun: Handles STUN Binding requests
+ ##
+ ## -
+ ## port: 3478
+ ## transport: udp
+ ## module: ejabberd_stun
+
+ ##
+ ## To handle XML-RPC requests that provide admin credentials:
+ ##
+ ## -
+ ## port: 4560
+ ## module: ejabberd_xmlrpc
+ -
+ port: 5280
+ module: ejabberd_http
+ ## request_handlers:
+ ## "/pub/archive": mod_http_fileserver
+ web_admin: true
+ http_poll: true
+ http_bind: true
+ ## register: true
+ captcha: true
+
+##
+## s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections.
+## Allowed values are: false optional required required_trusted
+## You must specify a certificate file.
+##
+## s2s_use_starttls: optional
+
+##
+## s2s_certfile: Specify a certificate file.
+##
+## s2s_certfile: "/path/to/ssl.pem"
+
+## Custom OpenSSL options
+##
+## s2s_protocol_options:
+## - "no_sslv3"
+## - "no_tlsv1"
+
+##
+## domain_certfile: Specify a different certificate for each served hostname.
+##
+## host_config:
+## "example.org":
+## domain_certfile: "/path/to/example_org.pem"
+## "example.com":
+## domain_certfile: "/path/to/example_com.pem"
+
+##
+## S2S whitelist or blacklist
+##
+## Default s2s policy for undefined hosts.
+##
+## s2s_access: s2s
+
+##
+## Outgoing S2S options
+##
+## Preferred address families (which to try first) and connect timeout
+## in milliseconds.
+##
+## outgoing_s2s_families:
+## - ipv4
+## - ipv6
+## outgoing_s2s_timeout: 10000
+
+### ==============
+### AUTHENTICATION
+
+##
+## auth_method: Method used to authenticate the users.
+## The default method is the internal.
+## If you want to use a different method,
+## comment this line and enable the correct ones.
+##
+auth_method: internal
+
+##
+## Store the plain passwords or hashed for SCRAM:
+## auth_password_format: plain
+## auth_password_format: scram
+##
+## Define the FQDN if ejabberd doesn't detect it:
+## fqdn: "server3.example.com"
+
+##
+## Authentication using external script
+## Make sure the script is executable by ejabberd.
+##
+## auth_method: external
+## extauth_program: "/path/to/authentication/script"
+
+##
+## Authentication using ODBC
+## Remember to setup a database in the next section.
+##
+## auth_method: odbc
+
+##
+## Authentication using PAM
+##
+## auth_method: pam
+## pam_service: "pamservicename"
+
+##
+## Authentication using LDAP
+##
+## auth_method: ldap
+##
+## List of LDAP servers:
+## ldap_servers:
+## - "localhost"
+##
+## Encryption of connection to LDAP servers:
+## ldap_encrypt: none
+## ldap_encrypt: tls
+##
+## Port to connect to on LDAP servers:
+## ldap_port: 389
+## ldap_port: 636
+##
+## LDAP manager:
+## ldap_rootdn: "dc=example,dc=com"
+##
+## Password of LDAP manager:
+## ldap_password: "******"
+##
+## Search base of LDAP directory:
+## ldap_base: "dc=example,dc=com"
+##
+## LDAP attribute that holds user ID:
+## ldap_uids:
+## - "mail": "%u@mail.example.org"
+##
+## LDAP filter:
+## ldap_filter: "(objectClass=shadowAccount)"
+
+##
+## Anonymous login support:
+## auth_method: anonymous
+## anonymous_protocol: sasl_anon | login_anon | both
+## allow_multiple_connections: true | false
+##
+## host_config:
+## "public.example.org":
+## auth_method: anonymous
+## allow_multiple_connections: false
+## anonymous_protocol: sasl_anon
+##
+## To use both anonymous and internal authentication:
+##
+## host_config:
+## "public.example.org":
+## auth_method:
+## - internal
+## - anonymous
+
+### ==============
+### DATABASE SETUP
+
+## ejabberd by default uses the internal Mnesia database,
+## so you do not necessarily need this section.
+## This section provides configuration examples in case
+## you want to use other database backends.
+## Please consult the ejabberd Guide for details on database creation.
+
+##
+## MySQL server:
+##
+## odbc_type: mysql
+## odbc_server: "server"
+## odbc_database: "database"
+## odbc_username: "username"
+## odbc_password: "password"
+##
+## If you want to specify the port:
+## odbc_port: 1234
+
+##
+## PostgreSQL server:
+##
+## odbc_type: pgsql
+## odbc_server: "server"
+## odbc_database: "database"
+## odbc_username: "username"
+## odbc_password: "password"
+##
+## If you want to specify the port:
+## odbc_port: 1234
+##
+## If you use PostgreSQL, have a large database, and need a
+## faster but inexact replacement for "select count(*) from users"
+##
+## pgsql_users_number_estimate: true
+
+##
+## ODBC compatible or MSSQL server:
+##
+## odbc_type: odbc
+## odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd"
+
+##
+## Number of connections to open to the database for each virtual host
+##
+## odbc_pool_size: 10
+
+##
+## Interval to make a dummy SQL request to keep the connections to the
+## database alive. Specify in seconds: for example 28800 means 8 hours
+##
+## odbc_keepalive_interval: undefined
+
+### ===============
+### TRAFFIC SHAPERS
+
+shaper:
+ ##
+ ## The "normal" shaper limits traffic speed to 1000 B/s
+ ##
+ normal: 1000
+
+ ##
+ ## The "fast" shaper limits traffic speed to 50000 B/s
+ ##
+ fast: 50000
+
+##
+## This option specifies the maximum number of elements in the queue
+## of the FSM. Refer to the documentation for details.
+##
+max_fsm_queue: 1000
+
+###. ====================
+###' ACCESS CONTROL LISTS
+acl:
+ ##
+ ## The 'admin' ACL grants administrative privileges to XMPP accounts.
+ ## You can put here as many accounts as you want.
+ ##
+ ## admin:
+ ## user:
+ ## - "aleksey": "localhost"
+ ## - "ermine": "example.org"
+ ##
+ ## Blocked users
+ ##
+ ## blocked:
+ ## user:
+ ## - "baduser": "example.org"
+ ## - "test"
+
+ ## Local users: don't modify this.
+ ##
+ local:
+ user_regexp: ""
+
+ ##
+ ## More examples of ACLs
+ ##
+ ## jabberorg:
+ ## server:
+ ## - "jabber.org"
+ ## aleksey:
+ ## user:
+ ## - "aleksey": "jabber.ru"
+ ## test:
+ ## user_regexp: "^test"
+ ## user_glob: "test*"
+
+ ##
+ ## Loopback network
+ ##
+ loopback:
+ ip:
+ - "127.0.0.0/8"
+
+ ##
+ ## Bad XMPP servers
+ ##
+ ## bad_servers:
+ ## server:
+ ## - "xmpp.zombie.org"
+ ## - "xmpp.spam.com"
+
+##
+## Define specific ACLs in a virtual host.
+##
+## host_config:
+## "localhost":
+## acl:
+## admin:
+## user:
+## - "bob-local": "localhost"
+
+### ============
+### ACCESS RULES
+access:
+ ## Maximum number of simultaneous sessions allowed for a single user:
+ max_user_sessions:
+ all: 10
+ ## Maximum number of offline messages that users can have:
+ max_user_offline_messages:
+ admin: 5000
+ all: 100
+ ## This rule allows access only for local users:
+ local:
+ local: allow
+ ## Only non-blocked users can use c2s connections:
+ c2s:
+ blocked: deny
+ all: allow
+ ## For C2S connections, all users except admins use the "normal" shaper
+ c2s_shaper:
+ admin: none
+ all: normal
+ ## All S2S connections use the "fast" shaper
+ s2s_shaper:
+ all: fast
+ ## Only admins can send announcement messages:
+ announce:
+ admin: allow
+ ## Only admins can use the configuration interface:
+ configure:
+ admin: allow
+ ## Admins of this server are also admins of the MUC service:
+ muc_admin:
+ admin: allow
+ ## Only accounts of the local ejabberd server can create rooms:
+ muc_create:
+ local: allow
+ ## All users are allowed to use the MUC service:
+ muc:
+ all: allow
+ ## Only accounts on the local ejabberd server can create Pubsub nodes:
+ pubsub_createnode:
+ local: allow
+ ## In-band registration allows registration of any possible username.
+ ## To disable in-band registration, replace 'allow' with 'deny'.
+ register:
+ all: allow
+ ## Only allow to register from localhost
+ trusted_network:
+ loopback: allow
+ ## Do not establish S2S connections with bad servers
+ ## s2s:
+ ## bad_servers: deny
+ ## all: allow
+
+## By default the frequency of account registrations from the same IP
+## is limited to 1 account every 10 minutes. To disable, specify: infinity
+## registration_timeout: 600
+
+##
+## Define specific Access Rules in a virtual host.
+##
+## host_config:
+## "localhost":
+## access:
+## c2s:
+## admin: allow
+## all: deny
+## register:
+## all: deny
+
+### ================
+### DEFAULT LANGUAGE
+
+##
+## language: Default language used for server messages.
+##
+language: "en"
+
+##
+## Set a different default language in a virtual host.
+##
+## host_config:
+## "localhost":
+## language: "ru"
+
+### =======
+### CAPTCHA
+
+##
+## Full path to a script that generates the image.
+##
+## captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh"
+
+##
+## Host for the URL and port where ejabberd listens for CAPTCHA requests.
+##
+## captcha_host: "example.org:5280"
+
+##
+## Limit CAPTCHA calls per minute for JID/IP to avoid DoS.
+##
+## captcha_limit: 5
+
+### =======
+### MODULES
+
+##
+## Modules enabled in all ejabberd virtual hosts.
+##
+modules:
+ mod_adhoc: {}
+ ## mod_admin_extra: {}
+ mod_announce: # recommends mod_adhoc
+ access: announce
+ mod_blocking: {} # requires mod_privacy
+ mod_caps: {}
+ mod_carboncopy: {}
+ mod_client_state:
+ drop_chat_states: true
+ queue_presence: false
+ mod_configure: {} # requires mod_adhoc
+ mod_disco: {}
+ ## mod_echo: {}
+ mod_irc: {}
+ mod_http_bind: {}
+ ## mod_http_fileserver:
+ ## docroot: "/var/www"
+ ## accesslog: "/var/log/ejabberd/access.log"
+ mod_last: {}
+ mod_muc:
+ ## host: "conference.@HOST@"
+ access: muc
+ access_create: muc_create
+ access_persistent: muc_create
+ access_admin: muc_admin
+ ## mod_muc_log: {}
+ mod_offline:
+ access_max_user_messages: max_user_offline_messages
+ mod_ping: {}
+ ## mod_pres_counter:
+ ## count: 5
+ ## interval: 60
+ mod_privacy: {}
+ mod_private: {}
+ ## mod_proxy65: {}
+ mod_pubsub:
+ access_createnode: pubsub_createnode
+ ## reduces resource comsumption, but XEP incompliant
+ ignore_pep_from_offline: true
+ ## XEP compliant, but increases resource comsumption
+ ## ignore_pep_from_offline: false
+ last_item_cache: false
+ plugins:
+ - "flat"
+ - "hometree"
+ - "pep" # pep requires mod_caps
+ mod_register:
+ ##
+ ## Protect In-Band account registrations with CAPTCHA.
+ ##
+ ## captcha_protected: true
+
+ ##
+ ## Set the minimum informational entropy for passwords.
+ ##
+ ## password_strength: 32
+
+ ##
+ ## After successful registration, the user receives
+ ## a message with this subject and body.
+ ##
+ welcome_message:
+ subject: "Welcome!"
+ body: |-
+ Hi.
+ Welcome to this XMPP server.
+
+ ##
+ ## When a user registers, send a notification to
+ ## these XMPP accounts.
+ ##
+ ## registration_watchers:
+ ## - "admin1@example.org"
+
+ ##
+ ## Only clients in the server machine can register accounts
+ ##
+ ip_access: trusted_network
+
+ ##
+ ## Local c2s or remote s2s users cannot register accounts
+ ##
+ ## access_from: deny
+
+ access: register
+ mod_roster: {}
+ mod_shared_roster: {}
+ mod_stats: {}
+ mod_time: {}
+ mod_vcard: {}
+ mod_version: {}
+
+##
+## Enable modules with custom options in a specific virtual host
+##
+## host_config:
+## "localhost":
+## modules:
+## mod_echo:
+## host: "mirror.localhost"
+
+##
+## Enable modules management via ejabberdctl for installation and
+## uninstallation of public/private contributed modules
+## (enabled by default)
+##
+
+allow_contrib_modules: true
+
+### Local Variables:
+### mode: yaml
+### End:
+### vim: set filetype=yaml tabstop=8
diff --git a/ejabberd.service.template b/ejabberd.service.template
index 80b15adbd..49ba14737 100644
--- a/ejabberd.service.template
+++ b/ejabberd.service.template
@@ -12,6 +12,13 @@ ExecStop=@ctlscriptpath@/ejabberdctl stop
ExecReload=@ctlscriptpath@/ejabberdctl reload_config
Type=oneshot
RemainAfterExit=yes
+# The CAP_DAC_OVERRIDE capability is required for pam authentication to work
+CapabilityBoundingSet=CAP_DAC_OVERRIDE
+PrivateTmp=true
+PrivateDevices=true
+ProtectHome=true
+ProtectSystem=full
+NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
diff --git a/ejabberd.yml.example b/ejabberd.yml.example
index 72439e5e1..dae839fdc 100644
--- a/ejabberd.yml.example
+++ b/ejabberd.yml.example
@@ -147,6 +147,15 @@ listen:
## access: all
## shaper_rule: fast
## ip: "127.0.0.1"
+ ## privilege_access:
+ ## roster: "both"
+ ## message: "outgoing"
+ ## presence: "roster"
+ ## delegations:
+ ## "urn:xmpp:mam:1":
+ ## filtering: ["node"]
+ ## "http://jabber.org/protocol/pubsub":
+ ## filtering: []
## hosts:
## "icq.example.org":
## password: "secret"
@@ -580,6 +589,7 @@ modules:
mod_carboncopy: {}
mod_client_state: {}
mod_configure: {} # requires mod_adhoc
+ ##mod_delegation: {} # for xep0356
mod_disco: {}
## mod_echo: {}
mod_irc: {}
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index 81be06dc3..c5c34b743 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -26,6 +26,25 @@
{tuple, [rterm()]} | {list, rterm()} |
rescode | restuple.
+-type oauth_scope() :: atom().
+
+%% ejabberd_commands OAuth ReST ACL definition:
+%% Two fields exist that are used to control access on a command from ReST API:
+%% 1. Policy
+%% If policy is:
+%% - restricted: command is not exposed as OAuth Rest API.
+%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access
+%% - user: Command might be called by any server user.
+%% - open: Command can be called by anyone.
+%%
+%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as
+%% defined by access option.
+%% Access option can be a list of:
+%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command.
+%% - AccessRule name: direct name of the access rule to check in config file.
+%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter
+%% to command, so that the command can perform additional check.
+
-record(ejabberd_commands,
{name :: atom(),
tags = [] :: [atom()] | '_' | '$2',
@@ -36,19 +55,25 @@
function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user,
+ %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}]
+ access = [] :: [{atom(),atom(),atom()}|atom()],
result = {res, rescode} :: rterm() | '_' | '$2',
args_desc = none :: none | [string()] | '_',
result_desc = none :: none | string() | '_',
args_example = none :: none | [any()] | '_',
result_example = none :: any()}).
+%% TODO Fix me: Type is not up to date
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
tags :: [atom()],
desc :: string(),
longdesc :: string(),
+ version :: integer(),
module :: atom(),
function :: atom(),
args :: [aterm()],
+ policy :: open | restricted | admin | user,
+ access :: [{atom(),atom(),atom()}|atom()],
result :: rterm()}.
%% @type ejabberd_commands() = #ejabberd_commands{
diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl
new file mode 100644
index 000000000..6b5a9bcc8
--- /dev/null
+++ b/include/ejabberd_oauth.hrl
@@ -0,0 +1,26 @@
+%%%----------------------------------------------------------------------
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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(oauth_token, {
+ token = <<"">> :: binary() | '_',
+ us = {<<"">>, <<"">>} :: {binary(), binary()} | '_',
+ scope = [] :: [binary()] | '_',
+ expire :: integer() | '$1'
+ }).
diff --git a/include/ejabberd_service.hrl b/include/ejabberd_service.hrl
new file mode 100644
index 000000000..7cd3b6943
--- /dev/null
+++ b/include/ejabberd_service.hrl
@@ -0,0 +1,20 @@
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("jlib.hrl").
+
+-type filter_attr() :: {binary(), [binary()]}.
+
+-record(state,
+ {socket :: ejabberd_socket:socket_state(),
+ sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket,
+ streamid = <<"">> :: binary(),
+ host_opts = dict:new() :: ?TDICT,
+ host = <<"">> :: binary(),
+ access :: atom(),
+ check_from = true :: boolean(),
+ server_hosts = ?MYHOSTS :: [binary()],
+ privilege_access :: [attr()],
+ delegations :: [filter_attr()],
+ last_pres = dict:new() :: ?TDICT}).
+
+-type(state() :: #state{} ).
diff --git a/include/ejabberd_sm.hrl b/include/ejabberd_sm.hrl
index 38298d66a..f86ab1c15 100644
--- a/include/ejabberd_sm.hrl
+++ b/include/ejabberd_sm.hrl
@@ -1,9 +1,9 @@
-ifndef(EJABBERD_SM_HRL).
-define(EJABBERD_SM_HRL, true).
--record(session, {sid, usr, us, priority, info}).
+-record(session, {sid, usr, us, priority, info = []}).
-record(session_counter, {vhost, count}).
--type sid() :: {erlang:timestamp(), pid()} | {erlang:timestamp(), undefined}.
+-type sid() :: {erlang:timestamp(), pid()}.
-type ip() :: {inet:ip_address(), inet:port_number()} | undefined.
-type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()}
| {oor, boolean()} | {auth_module, atom()}
diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl
index fc20f44c6..dd414a8d8 100644
--- a/include/mod_muc_room.hrl
+++ b/include/mod_muc_room.hrl
@@ -78,11 +78,15 @@
jid :: jid(),
nick :: binary(),
role :: role(),
- is_subscriber = false :: boolean(),
- subscriptions = [] :: [binary()],
+ %%is_subscriber = false :: boolean(),
+ %%subscriptions = [] :: [binary()],
last_presence :: xmlel()
}).
+-record(subscriber, {jid :: jid(),
+ nick = <<>> :: binary(),
+ nodes = [] :: [binary()]}).
+
-record(activity,
{
message_time = 0 :: integer(),
@@ -102,6 +106,8 @@
jid = #jid{} :: jid(),
config = #config{} :: config(),
users = (?DICT):new() :: ?TDICT,
+ subscribers = (?DICT):new() :: ?TDICT,
+ subscriber_nicks = (?DICT):new() :: ?TDICT,
last_voice_request_time = treap:empty() :: treap:treap(),
robots = (?DICT):new() :: ?TDICT,
nicks = (?DICT):new() :: ?TDICT,
diff --git a/include/ns.hrl b/include/ns.hrl
index 7955129ef..d94c2a95f 100644
--- a/include/ns.hrl
+++ b/include/ns.hrl
@@ -170,6 +170,8 @@
-define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>).
-define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>).
-define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>).
+-define(NS_PRIVILEGE, <<"urn:xmpp:privilege:1">>).
+-define(NS_DELEGATION, <<"urn:xmpp:delegation:1">>).
-define(NS_MUCSUB, <<"urn:xmpp:mucsub:0">>).
-define(NS_MUCSUB_NODES_PRESENCE, <<"urn:xmpp:mucsub:nodes:presence">>).
-define(NS_MUCSUB_NODES_MESSAGES, <<"urn:xmpp:mucsub:nodes:messages">>).
diff --git a/include/xmpp_codec.hrl b/include/xmpp_codec.hrl
index 443769bb7..981f7f4c2 100644
--- a/include/xmpp_codec.hrl
+++ b/include/xmpp_codec.hrl
@@ -146,6 +146,10 @@
height :: non_neg_integer()}).
-type thumbnail() :: #thumbnail{}.
+-record(privilege_perm, {access :: 'message' | 'presence' | 'roster',
+ type :: 'both' | 'get' | 'managed_entity' | 'none' | 'outgoing' | 'roster' | 'set'}).
+-type privilege_perm() :: #privilege_perm{}.
+
-record(muc_decline, {reason = <<>> :: binary(),
from :: jid:jid(),
to :: jid:jid()}).
@@ -176,6 +180,14 @@
-record(starttls_proceed, {}).
-type starttls_proceed() :: #starttls_proceed{}.
+-record(forwarded, {delay :: #delay{},
+ sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
+-type forwarded() :: #forwarded{}.
+
+-record(privilege, {perms = [] :: [#privilege_perm{}],
+ forwarded :: #forwarded{}}).
+-type privilege() :: #privilege{}.
+
-record(client_id, {id = <<>> :: binary()}).
-type client_id() :: #client_id{}.
@@ -184,10 +196,6 @@
xmlns = <<>> :: binary()}).
-type sm_resumed() :: #sm_resumed{}.
--record(forwarded, {delay :: #delay{},
- sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
--type forwarded() :: #forwarded{}.
-
-record(sm_enable, {max :: non_neg_integer(),
resume = false :: boolean(),
xmlns = <<>> :: binary()}).
@@ -215,6 +223,10 @@
-record(private, {xml_els = [] :: [fxml:xmlel()]}).
-type private() :: #private{}.
+-record(delegation_query, {to :: jid:jid(),
+ delegate = [] :: [binary()]}).
+-type delegation_query() :: #delegation_query{}.
+
-record(db_verify, {from = <<>> :: binary(),
to = <<>> :: binary(),
id = <<>> :: binary(),
@@ -534,6 +546,10 @@
continue :: binary()}).
-type muc_invite() :: #muc_invite{}.
+-record(delegated, {ns = <<>> :: binary(),
+ attrs = [] :: [binary()]}).
+-type delegated() :: #delegated{}.
+
-record(carbons_disable, {}).
-type carbons_disable() :: #carbons_disable{}.
@@ -838,6 +854,10 @@
sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
-type stanza_error() :: #stanza_error{}.
+-record(delegation, {delegated = [] :: [#delegated{}],
+ forwarded :: #forwarded{}}).
+-type delegation() :: #delegation{}.
+
-record(mix_join, {jid :: jid:jid(),
subscribe = [] :: [binary()]}).
-type mix_join() :: #mix_join{}.
@@ -905,21 +925,18 @@
utc :: erlang:timestamp()}).
-type time() :: #time{}.
--type xmpp_element() :: muc_admin() |
- compression() |
+-type xmpp_element() :: compression() |
ps_subscription() |
xdata_option() |
version() |
- ps_affiliation() |
- mam_fin() |
sm_a() |
bob_data() |
media() |
stanza_id() |
starttls_proceed() |
+ forwarded() |
client_id() |
sm_resumed() |
- forwarded() |
xevent() |
privacy_list() |
carbons_sent() |
@@ -932,6 +949,7 @@
mix_participant() |
compressed() |
block_list() |
+ delegated() |
rsm_set() |
'see-other-host'() |
hint() |
@@ -953,10 +971,10 @@
compress() |
bytestreams() |
adhoc_actions() |
+ privacy_query() |
muc_history() |
identity() |
feature_csi() |
- privacy_query() |
delay() |
thumbnail() |
vcard_tel() |
@@ -993,6 +1011,7 @@
nick() |
p1_ack() |
block() |
+ delegation() |
mix_join() |
xmpp_session() |
xdata() |
@@ -1014,6 +1033,7 @@
adhoc_command() |
sm_failed() |
ping() |
+ privilege_perm() |
privacy_item() |
disco_item() |
ps_item() |
@@ -1027,12 +1047,13 @@
sic() |
ps_options() |
starttls() |
+ db_verify() |
+ roster_query() |
media_uri() |
muc_destroy() |
vcard_key() |
csi() |
- db_verify() |
- roster_query() |
+ delegation_query() |
mam_query() |
bookmark_url() |
vcard_email() |
@@ -1051,6 +1072,7 @@
carbons_private() |
mix_leave() |
muc_subscribe() |
+ privilege() |
muc_unique() |
sasl_response() |
message() |
@@ -1064,4 +1086,7 @@
sasl_auth() |
p1_push() |
oob_x() |
- unblock().
+ unblock() |
+ muc_admin() |
+ ps_affiliation() |
+ mam_fin().
diff --git a/lib/ct_formatter.ex b/lib/ct_formatter.ex
index 47c487ac4..0c301353b 100644
--- a/lib/ct_formatter.ex
+++ b/lib/ct_formatter.ex
@@ -3,7 +3,7 @@ defmodule ExUnit.CTFormatter do
use GenEvent
- import ExUnit.Formatter, only: [format_time: 2, format_filters: 2, format_test_failure: 5,
+ import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5,
format_test_case_failure: 5]
def init(opts) do
diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex
new file mode 100644
index 000000000..9d17b157d
--- /dev/null
+++ b/lib/ejabberd/config/attr.ex
@@ -0,0 +1,119 @@
+defmodule Ejabberd.Config.Attr do
+ @moduledoc """
+ Module used to work with the attributes parsed from
+ an elixir block (do...end).
+
+ Contains functions for extracting attrs from a block
+ and validation.
+ """
+
+ @type attr :: {atom(), any()}
+
+ @attr_supported [
+ active:
+ [type: :boolean, default: true],
+ git:
+ [type: :string, default: ""],
+ name:
+ [type: :string, default: ""],
+ opts:
+ [type: :list, default: []],
+ dependency:
+ [type: :list, default: []]
+ ]
+
+ @doc """
+ Takes a block with annotations and extracts the list
+ of attributes.
+ """
+ @spec extract_attrs_from_block_with_defaults(any()) :: [attr]
+ def extract_attrs_from_block_with_defaults(block) do
+ block
+ |> extract_attrs_from_block
+ |> put_into_list_if_not_already
+ |> insert_default_attrs_if_missing
+ end
+
+ @doc """
+ Takes an attribute or a list of attrs and validate them.
+
+ Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes.
+ """
+ @spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}]
+ def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1)
+ def validate(attr), do: validate([attr]) |> List.first
+
+ @doc """
+ Returns the type of an attribute, given its name.
+ """
+ @spec get_type_for_attr(atom()) :: atom()
+ def get_type_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:type)
+ end
+
+ @doc """
+ Returns the default value for an attribute, given its name.
+ """
+ @spec get_default_for_attr(atom()) :: any()
+ def get_default_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:default)
+ end
+
+ # Private API
+
+ # Given an elixir block (do...end) returns a list with the annotations
+ # or a single annotation.
+ @spec extract_attrs_from_block(any()) :: [attr] | attr
+ defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1)
+ defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs)
+ defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value}
+ defp extract_attrs_from_block(nil), do: []
+
+ # In case extract_attrs_from_block returns a single attribute,
+ # then put it into a list. (Ensures attrs are always into a list).
+ @spec put_into_list_if_not_already([attr] | attr) :: [attr]
+ defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs
+ defp put_into_list_if_not_already(attr), do: [attr]
+
+ # Given a list of attributes, it inserts the missing attribute with their
+ # default value.
+ @spec insert_default_attrs_if_missing([attr]) :: [attr]
+ defp insert_default_attrs_if_missing(attrs) do
+ Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) ->
+ case Keyword.has_key?(acc, attr_name) do
+ true -> acc
+ false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name))
+ end
+ end
+ end
+
+ # Given an attribute, validates it and return a tuple with
+ # {:ok, attr} or {:error, attr, cause}
+ @spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()}
+ defp valid_attr?({attr_name, param} = attr) do
+ case Keyword.get(@attr_supported, attr_name) do
+ nil -> {:error, attr, :attr_not_supported}
+ [{:type, param_type} | _] -> case is_of_type?(param, param_type) do
+ true -> {:ok, attr}
+ false -> {:error, attr, :type_not_supported}
+ end
+ end
+ end
+
+ # Given an attribute value and a type, it returns a true
+ # if the value its of the type specified, false otherwise.
+
+ # Usefoul for checking if an attr value respects the type
+ # specified for the annotation.
+ @spec is_of_type?(any(), atom()) :: boolean()
+ defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true
+ defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true
+ defp is_of_type?(param, type) when type == :list and is_list(param), do: true
+ defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true
+ defp is_of_type?(_param, type) when type == :any, do: true
+ defp is_of_type?(_, _), do: false
+end
diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex
new file mode 100644
index 000000000..4d1270bc1
--- /dev/null
+++ b/lib/ejabberd/config/config.ex
@@ -0,0 +1,145 @@
+defmodule Ejabberd.Config do
+ @moduledoc """
+ Base module for configuration file.
+
+ Imports macros for the config DSL and contains functions
+ for working/starting the configuration parsed.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.EjabberdLogger
+
+ defmacro __using__(_opts) do
+ quote do
+ import Ejabberd.Config, only: :macros
+ import Ejabberd.Logger
+
+ @before_compile Ejabberd.Config
+ end
+ end
+
+ # Validate the modules parsed and log validation errors at compile time.
+ # Could be also possible to interrupt the compilation&execution by throwing
+ # an exception if necessary.
+ def __before_compile__(_env) do
+ get_modules_parsed_in_order
+ |> EjabberdModule.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ @doc """
+ Given the path of the config file, it evaluates it.
+ """
+ def init(file_path, force \\ false) do
+ init_already_executed = Ejabberd.Config.Store.get(:module_name) != []
+
+ case force do
+ true ->
+ Ejabberd.Config.Store.stop
+ Ejabberd.Config.Store.start_link
+ do_init(file_path)
+ false ->
+ if not init_already_executed, do: do_init(file_path)
+ end
+ end
+
+ @doc """
+ Returns a list with all the opts, formatted for ejabberd.
+ """
+ def get_ejabberd_opts do
+ get_general_opts
+ |> Dict.put(:modules, get_modules_parsed_in_order())
+ |> Dict.put(:listeners, get_listeners_parsed_in_order())
+ |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd
+ end
+
+ @doc """
+ Register the hooks defined inside the elixir config file.
+ """
+ def start_hooks do
+ get_hooks_parsed_in_order()
+ |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1)
+ end
+
+ ###
+ ### MACROS
+ ###
+
+ defmacro listen(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:listeners, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro module(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:modules, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro hook(hook_name, opts, fun) do
+ quote do
+ Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{
+ hook: unquote(hook_name),
+ opts: unquote(opts),
+ fun: unquote(fun)
+ })
+ end
+ end
+
+ # Private API
+
+ defp do_init(file_path) do
+ # File evaluation
+ Code.eval_file(file_path) |> extract_and_store_module_name()
+
+ # Getting start/0 config
+ Ejabberd.Config.Store.get(:module_name)
+ |> case do
+ nil -> IO.puts "[ ERR ] Configuration module not found."
+ [module] -> call_start_func_and_store_data(module)
+ end
+
+ # Fetching git modules and install them
+ get_modules_parsed_in_order()
+ |> EjabberdModule.fetch_git_repos
+ end
+
+ # Returns the modules from the store
+ defp get_modules_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse
+
+ # Returns the listeners from the store
+ defp get_listeners_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse
+
+ defp get_hooks_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse
+
+ # Returns the general config options
+ defp get_general_opts,
+ do: Ejabberd.Config.Store.get(:general) |> List.first
+
+ # Gets the general ejabberd options calling
+ # the start/0 function and stores them.
+ defp call_start_func_and_store_data(module) do
+ opts = apply(module, :start, [])
+ Ejabberd.Config.Store.put(:general, opts)
+ end
+
+ # Stores the configuration module name
+ defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do
+ Ejabberd.Config.Store.put(:module_name, mod)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex
new file mode 100644
index 000000000..8b7858d23
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_hook.ex
@@ -0,0 +1,23 @@
+defmodule Ejabberd.Config.EjabberdHook do
+ @moduledoc """
+ Module containing functions for manipulating
+ ejabberd hooks.
+ """
+
+ defstruct hook: nil, opts: [], fun: nil
+
+ alias Ejabberd.Config.EjabberdHook
+
+ @type t :: %EjabberdHook{}
+
+ @doc """
+ Register a hook to ejabberd.
+ """
+ @spec start(EjabberdHook.t) :: none
+ def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do
+ host = Keyword.get(opts, :host, :global)
+ priority = Keyword.get(opts, :priority, 50)
+
+ :ejabberd_hooks.add(hook, host, fun, priority)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex
new file mode 100644
index 000000000..4de9a302e
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_module.ex
@@ -0,0 +1,70 @@
+defmodule Ejabberd.Config.EjabberdModule do
+ @moduledoc """
+ Module representing a module block in the configuration file.
+ It offers functions for validation and for starting the modules.
+
+ Warning: The name is EjabberdModule to not collide with
+ the already existing Elixir.Module.
+ """
+
+ @type t :: %{module: atom, attrs: [Attr.t]}
+
+ defstruct [:module, :attrs]
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.Validation
+
+ @doc """
+ Given a list of modules / single module
+ it runs different validators on them.
+
+ For each module, returns a {:ok, mod} or {:error, mod, errors}
+ """
+ def validate(modules) do
+ Validation.validate(modules)
+ end
+
+ @doc """
+ Given a list of modules, it takes only the ones with
+ a git attribute and tries to fetch the repo,
+ then, it install them through :ext_mod.install/1
+ """
+ @spec fetch_git_repos([EjabberdModule.t]) :: none()
+ def fetch_git_repos(modules) do
+ modules
+ |> Enum.filter(&is_git_module?/1)
+ |> Enum.each(&fetch_and_install_git_module/1)
+ end
+
+ # Private API
+
+ defp is_git_module?(%EjabberdModule{attrs: attrs}) do
+ case Keyword.get(attrs, :git) do
+ "" -> false
+ repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/)
+ end
+ end
+
+ defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do
+ repo = Keyword.get(attrs, :git)
+ mod_name = case Keyword.get(attrs, :name) do
+ "" -> infer_mod_name_from_git_url(repo)
+ name -> name
+ end
+
+ path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}"
+ fetch_and_store_repo_source_if_not_exists(path, repo)
+ :ext_mod.install(mod_name) # Have to check if overwrites an already present mod
+ end
+
+ defp fetch_and_store_repo_source_if_not_exists(path, repo) do
+ unless File.exists?(path) do
+ IO.puts "[info] Fetching: #{repo}"
+ :os.cmd('git clone #{repo} #{path}')
+ end
+ end
+
+ defp infer_mod_name_from_git_url(repo),
+ do: String.split(repo, "/") |> List.last |> String.replace(".git", "")
+end
diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex
new file mode 100644
index 000000000..270fbfaa6
--- /dev/null
+++ b/lib/ejabberd/config/logger/ejabberd_logger.ex
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.EjabberdLogger do
+ @moduledoc """
+ Module used to log validation errors given validated modules
+ given validated modules.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Given a list of modules validated, in the form of {:ok, mod} or
+ {:error, mod, errors}, it logs to the user the errors found.
+ """
+ @spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t]
+ def log_errors(modules_validated) when is_list(modules_validated) do
+ Enum.each modules_validated, &do_log_errors/1
+ modules_validated
+ end
+
+ defp do_log_errors({:ok, _mod}), do: nil
+ defp do_log_errors({:error, _mod, errors}), do: Enum.each errors, &do_log_errors/1
+ defp do_log_errors({:attribute, errors}), do: Enum.each errors, &log_attribute_error/1
+ defp do_log_errors({:dependency, errors}), do: Enum.each errors, &log_dependency_error/1
+
+ defp log_attribute_error({{attr_name, val}, :attr_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} is not supported."
+
+ defp log_attribute_error({{attr_name, val}, :type_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)."
+
+ defp log_dependency_error({module, :not_found}), do:
+ IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency."
+end
diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex
new file mode 100644
index 000000000..b7010ddfe
--- /dev/null
+++ b/lib/ejabberd/config/opts_formatter.ex
@@ -0,0 +1,46 @@
+defmodule Ejabberd.Config.OptsFormatter do
+ @moduledoc """
+ Module for formatting options parsed into the format
+ ejabberd uses.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Takes a keyword list with keys corresponding to
+ the keys requested by the ejabberd config (ex: modules: mods)
+ and formats them to be correctly evaluated by ejabberd.
+
+ Look at how Config.get_ejabberd_opts/0 is constructed for
+ more informations.
+ """
+ @spec format_opts_for_ejabberd([{atom(), any()}]) :: list()
+ def format_opts_for_ejabberd(opts) do
+ opts
+ |> format_attrs_for_ejabberd
+ end
+
+ defp format_attrs_for_ejabberd(opts) when is_list(opts),
+ do: Enum.map opts, &format_attrs_for_ejabberd/1
+
+ defp format_attrs_for_ejabberd({:listeners, mods}),
+ do: {:listen, format_listeners_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({:modules, mods}),
+ do: {:modules, format_mods_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({key, opts}) when is_atom(key),
+ do: {key, opts}
+
+ defp format_mods_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ {mod, attrs[:opts]}
+ end
+ end
+
+ defp format_listeners_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ Keyword.put(attrs[:opts], :module, mod)
+ end
+ end
+end
diff --git a/lib/ejabberd/config/store.ex b/lib/ejabberd/config/store.ex
new file mode 100644
index 000000000..72beea64c
--- /dev/null
+++ b/lib/ejabberd/config/store.ex
@@ -0,0 +1,55 @@
+defmodule Ejabberd.Config.Store do
+ @moduledoc """
+ Module used for storing the modules parsed from
+ the configuration file.
+
+ Example:
+ - Store.put(:modules, mod1)
+ - Store.put(:modules, mod2)
+
+ - Store.get(:modules) :: [mod1, mod2]
+
+ Be carefoul: when retrieving data you get them
+ in the order inserted into the store, which normally
+ is the reversed order of how the modules are specified
+ inside the configuration file. To resolve this just use
+ a Enum.reverse/1.
+ """
+
+ @name __MODULE__
+
+ def start_link do
+ Agent.start_link(fn -> %{} end, name: @name)
+ end
+
+ @doc """
+ Stores a value based on the key. If the key already exists,
+ then it inserts the new element, maintaining all the others.
+ It uses a list for this.
+ """
+ @spec put(atom, any) :: :ok
+ def put(key, val) do
+ Agent.update @name, &Map.update(&1, key, [val], fn coll ->
+ [val | coll]
+ end)
+ end
+
+ @doc """
+ Gets a value based on the key passed.
+ Returns always a list.
+ """
+ @spec get(atom) :: [any]
+ def get(key) do
+ Agent.get @name, &Map.get(&1, key, [])
+ end
+
+ @doc """
+ Stops the store.
+ It uses Agent.stop underneath, so be aware that exit
+ could be called.
+ """
+ @spec stop() :: :ok
+ def stop do
+ Agent.stop @name
+ end
+end
diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex
new file mode 100644
index 000000000..2fe00361a
--- /dev/null
+++ b/lib/ejabberd/config/validator/validation.ex
@@ -0,0 +1,40 @@
+defmodule Ejabberd.Config.Validation do
+ @moduledoc """
+ Module used to validate a list of modules.
+ """
+
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map}
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.Validator
+ alias Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module or a list of modules it runs validators on them
+ and returns {:ok, mod} or {:error, mod, errors}, for each
+ of them.
+ """
+ @spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result]
+ def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1))
+ def validate(module), do: validate([module])
+
+ # Private API
+
+ @spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result
+ defp do_validate(modules, mod) do
+ {modules, mod, %{}}
+ |> Validator.Attrs.validate
+ |> Validator.Dependencies.validate
+ |> resolve_validation_result
+ end
+
+ @spec resolve_validation_result(mod_validation) :: mod_validation_result
+ defp resolve_validation_result({_modules, mod, errors}) do
+ case errors do
+ err when err == %{} -> {:ok, mod}
+ err -> {:error, mod, err}
+ end
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex
new file mode 100644
index 000000000..94117ab21
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_attrs.ex
@@ -0,0 +1,28 @@
+defmodule Ejabberd.Config.Validator.Attrs do
+ @moduledoc """
+ Validator module used to validate attributes.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+
+ import Ejabberd.Config.ValidatorUtility
+ alias Ejabberd.Config.Attr
+
+ @doc """
+ Given a module (with the form used for validation)
+ it runs Attr.validate/1 on each attribute and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ errors = Enum.reduce mod.attrs, errors, fn(attr, err) ->
+ case Attr.validate(attr) do
+ {:ok, attr} -> err
+ {:error, attr, cause} -> put_error(err, :attribute, {attr, cause})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex
new file mode 100644
index 000000000..d44c8a136
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_dependencies.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.Validator.Dependencies do
+ @moduledoc """
+ Validator module used to validate dependencies specified
+ with the @dependency annotation.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ import Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module (with the form used for validation)
+ it checks if the @dependency annotation is respected and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ module_names = extract_module_names(modules)
+ dependencies = mod.attrs[:dependency]
+
+ errors = Enum.reduce dependencies, errors, fn(req_module, err) ->
+ case req_module in module_names do
+ true -> err
+ false -> put_error(err, :dependency, {req_module, :not_found})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex
new file mode 100644
index 000000000..17805f748
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_utility.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.ValidatorUtility do
+ @moduledoc """
+ Module used as a base validator for validation modules.
+ Imports utility functions for working with validation structures.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Inserts an error inside the errors collection, for the given key.
+ If the key doesn't exists then it creates an empty collection
+ and inserts the value passed.
+ """
+ @spec put_error(map, atom, any) :: map
+ def put_error(errors, key, val) do
+ Map.update errors, key, [val], fn coll ->
+ [val | coll]
+ end
+ end
+
+ @doc """
+ Given a list of modules it extracts and returns a list
+ of the module names (which are Elixir.Module).
+ """
+ @spec extract_module_names(EjabberdModule.t) :: [atom]
+ def extract_module_names(modules) when is_list(modules) do
+ modules
+ |> Enum.map(&Map.get(&1, :module))
+ end
+end
diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex
new file mode 100644
index 000000000..6592104a2
--- /dev/null
+++ b/lib/ejabberd/config_util.ex
@@ -0,0 +1,18 @@
+defmodule Ejabberd.ConfigUtil do
+ @moduledoc """
+ Module containing utility functions for
+ the config file.
+ """
+
+ @doc """
+ Returns true when the config file is based on elixir.
+ """
+ @spec is_elixir_config(list) :: boolean
+ def is_elixir_config(filename) when is_list(filename) do
+ is_elixir_config(to_string(filename))
+ end
+
+ def is_elixir_config(filename) do
+ String.ends_with?(filename, "exs")
+ end
+end
diff --git a/lib/ejabberd/module.ex b/lib/ejabberd/module.ex
new file mode 100644
index 000000000..9fb3f040c
--- /dev/null
+++ b/lib/ejabberd/module.ex
@@ -0,0 +1,19 @@
+defmodule Ejabberd.Module do
+
+ defmacro __using__(opts) do
+ logger_enabled = Keyword.get(opts, :logger, true)
+
+ quote do
+ @behaviour :gen_mod
+ import Ejabberd.Module
+
+ unquote(if logger_enabled do
+ quote do: import Ejabberd.Logger
+ end)
+ end
+ end
+
+ # gen_mod callbacks
+ def depends(_host, _opts), do: []
+ def mod_opt_type(_), do: []
+end
diff --git a/lib/mix/tasks/deps.tree.ex b/lib/mix/tasks/deps.tree.ex
new file mode 100644
index 000000000..94cb85a50
--- /dev/null
+++ b/lib/mix/tasks/deps.tree.ex
@@ -0,0 +1,94 @@
+defmodule Mix.Tasks.Ejabberd.Deps.Tree do
+ use Mix.Task
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @shortdoc "Lists all ejabberd modules and their dependencies"
+
+ @moduledoc """
+ Lists all ejabberd modules and their dependencies.
+
+ The project must have ejabberd as a dependency.
+ """
+
+ def run(_argv) do
+ # First we need to start manually the store to be available
+ # during the compilation of the config file.
+ Ejabberd.Config.Store.start_link
+ Ejabberd.Config.init(:ejabberd_config.get_ejabberd_config_path())
+
+ Mix.shell.info "ejabberd modules"
+
+ Ejabberd.Config.Store.get(:modules)
+ |> Enum.reverse # Because of how mods are stored inside the store
+ |> format_mods
+ |> Mix.shell.info
+ end
+
+ defp format_mods(mods) when is_list(mods) do
+ deps_tree = build_dependency_tree(mods)
+ mods_used_as_dependency = get_mods_used_as_dependency(deps_tree)
+
+ keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency)
+ |> format_mods_into_string
+ end
+
+ defp build_dependency_tree(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ deps = attrs[:dependency]
+ build_dependency_tree(mods, mod, deps)
+ end
+ end
+
+ defp build_dependency_tree(mods, mod, []), do: %{module: mod, dependency: []}
+ defp build_dependency_tree(mods, mod, deps) when is_list(deps) do
+ dependencies = Enum.map deps, fn dep ->
+ dep_deps = get_dependencies_of_mod(mods, dep)
+ build_dependency_tree(mods, dep, dep_deps)
+ end
+
+ %{module: mod, dependency: dependencies}
+ end
+
+ defp get_mods_used_as_dependency(mods) when is_list(mods) do
+ Enum.reduce mods, [], fn(mod, acc) ->
+ case mod do
+ %{dependency: []} -> acc
+ %{dependency: deps} -> get_mod_names(deps) ++ acc
+ end
+ end
+ end
+
+ defp get_mod_names([]), do: []
+ defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten
+ defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)]
+
+ defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do
+ Enum.filter mods, fn %{module: mod} ->
+ not mod in mods_used_as_dep
+ end
+ end
+
+ defp get_dependencies_of_mod(deps, mod_name) do
+ Enum.find(deps, &(Map.get(&1, :module) == mod_name))
+ |> Map.get(:attrs)
+ |> Keyword.get(:dependency)
+ end
+
+ defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0)
+ defp format_mods_into_string([], _indentation), do: ""
+ defp format_mods_into_string(mods, indentation) when is_list(mods) do
+ Enum.reduce mods, "", fn(mod, acc) ->
+ acc <> format_mods_into_string(mod, indentation)
+ end
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do
+ "\n├── #{mod}" <> format_mods_into_string(deps, 2)
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do
+ spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end
+ "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4)
+ end
+end
diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex
index 89fc60d87..09bf58405 100644
--- a/lib/mod_presence_demo.ex
+++ b/lib/mod_presence_demo.ex
@@ -1,21 +1,20 @@
defmodule ModPresenceDemo do
- import Ejabberd.Logger # this allow using info, error, etc for logging
- @behaviour :gen_mod
+ use Ejabberd.Module
def start(host, _opts) do
info('Starting ejabberd module Presence Demo')
- Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
-
+
def stop(host) do
info('Stopping ejabberd module Presence Demo')
- Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
-
+
def on_presence(user, _server, _resource, _packet) do
info('Receive presence for #{user}')
:none
- end
+ end
end
diff --git a/mix.exs b/mix.exs
index 0806e1210..c77f2abb4 100644
--- a/mix.exs
+++ b/mix.exs
@@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do
def project do
[app: :ejabberd,
- version: "16.06.0",
+ version: "16.11.0",
description: description,
elixir: "~> 1.2",
elixirc_paths: ["lib"],
@@ -11,11 +11,13 @@ defmodule Ejabberd.Mixfile do
compilers: [:asn1] ++ Mix.compilers,
erlc_options: erlc_options,
erlc_paths: ["asn1", "src"],
+ # Elixir tests are starting the part of ejabberd they need
+ aliases: [test: "test --no-start"],
package: package,
deps: deps]
end
- defp description do
+ def description do
"""
Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform.
"""
@@ -26,9 +28,8 @@ defmodule Ejabberd.Mixfile do
applications: [:ssl],
included_applications: [:lager, :mnesia, :p1_utils, :cache_tab,
:fast_tls, :stringprep, :fast_xml,
- :stun, :fast_yaml, :ezlib, :iconv,
- :esip, :jiffy, :p1_oauth2, :p1_xmlrpc, :eredis,
- :p1_mysql, :p1_pgsql, :sqlite3]]
+ :stun, :fast_yaml, :esip, :jiffy, :p1_oauth2]
+ ++ cond_apps]
end
defp erlc_options do
@@ -38,7 +39,7 @@ defmodule Ejabberd.Mixfile do
end
defp deps do
- [{:lager, "~> 3.0.0"},
+ [{:lager, "~> 3.2"},
{:p1_utils, "~> 1.0"},
{:cache_tab, "~> 1.0"},
{:stringprep, "~> 1.0"},
@@ -49,17 +50,40 @@ defmodule Ejabberd.Mixfile do
{:esip, "~> 1.0"},
{:jiffy, "~> 0.14.7"},
{:p1_oauth2, "~> 0.6.1"},
- {:p1_xmlrpc, "~> 1.15"},
- {:p1_mysql, "~> 1.0"},
- {:p1_pgsql, "~> 1.1"},
- {:sqlite3, "~> 1.1"},
- {:ezlib, "~> 1.0"},
- {:iconv, "~> 1.0"},
- {:eredis, "~> 1.0"},
- {:exrm, "~> 1.0.0-rc7", only: :dev}]
+ {:exrm, "~> 1.0.0", only: :dev},
+ # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
+ # version 3.20:
+ {:relx, "~> 3.21", only: :dev},
+ {:ex_doc, ">= 0.0.0", only: :dev}]
+ ++ cond_deps
end
- defp package do
+ defp cond_deps do
+ for {:true, dep} <- [{config(:mysql), {:p1_mysql, "~> 1.0"}},
+ {config(:pgsql), {:p1_pgsql, "~> 1.1"}},
+ {config(:sqlite), {:sqlite3, "~> 1.1"}},
+ {config(:riak), {:riakc, "~> 2.4"}},
+ {config(:redis), {:eredis, "~> 1.0"}},
+ {config(:zlib), {:ezlib, "~> 1.0"}},
+ {config(:iconv), {:iconv, "~> 1.0"}},
+ {config(:pam), {:p1_pam, "~> 1.0"}},
+ {config(:tools), {:luerl, github: "rvirding/luerl", tag: "v0.2"}},
+ {config(:tools), {:meck, "~> 0.8.4"}},
+ {config(:tools), {:moka, github: "processone/moka", tag: "1.0.5c"}}], do:
+ dep
+ end
+
+ defp cond_apps do
+ for {:true, app} <- [{config(:redis), :eredis},
+ {config(:mysql), :p1_mysql},
+ {config(:pgsql), :p1_pgsql},
+ {config(:sqlite), :sqlite3},
+ {config(:zlib), :ezlib},
+ {config(:iconv), :iconv}], do:
+ app
+ end
+
+ def package do
[# These are the default files included in the package
files: ["lib", "src", "priv", "mix.exs", "include", "README.md", "COPYING"],
maintainers: ["ProcessOne"],
@@ -69,6 +93,21 @@ defmodule Ejabberd.Mixfile do
"Source" => "https://github.com/processone/ejabberd",
"ProcessOne" => "http://www.process-one.net/"}]
end
+
+ def vars do
+ case :file.consult("vars.config") do
+ {:ok,config} -> config
+ _ -> [zlib: true, iconv: true]
+ end
+ end
+
+ defp config(key) do
+ case vars[key] do
+ nil -> false
+ value -> value
+ end
+ end
+
end
defmodule Mix.Tasks.Compile.Asn1 do
diff --git a/mix.lock b/mix.lock
index d576c518f..e515fd346 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,26 +1,23 @@
%{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []},
- "cache_tab": {:hex, :cache_tab, "1.0.3", "0e3c40dde2fe2a6a4db241d7583cea0cc1bcf29e546a0a22f15b75366b2f336e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
+ "cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
- "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
+ "earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []},
"erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
- "esip": {:hex, :esip, "1.0.6", "cb1ced88fae4c4a4888d9023c2c13b2239e14f8e360aee134c964b4a36dcc34d", [:rebar3], [{:stun, "1.0.5", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]},
- "exrm": {:hex, :exrm, "1.0.6", "f708fc091dcacb93c1da58254a1ab34166d5ac3dca162877e878fe5d7a9e9dce", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
+ "esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]},
+ "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
+ "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
- "fast_tls": {:hex, :fast_tls, "1.0.5", "8b970a91d4131fe5b9d47ffaccc2466944293c88dc5cc75a25548d73d57f7b77", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
- "fast_xml": {:hex, :fast_xml, "1.1.13", "85eca0a003598dbb0644320bd9bdc5fef30ad6285ab2aa80e2b5b82e65b79aa8", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
- "fast_yaml": {:hex, :fast_yaml, "1.0.4", "075ffb55f6ff3aa2f0461b8bfd1218e2f91e632c1675fc535963b9de7834800e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
+ "fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
+ "fast_xml": {:hex, :fast_xml, "1.1.15", "6d23eb7f874e1357cf80a48d75a7bd0c8f6318029dc4b70122e9f54911f57f83", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
+ "fast_yaml": {:hex, :fast_yaml, "1.0.6", "3fe6feb7935ae8028b337e53e1db29e73ad3bca8041108f6a8f73b7175ece75c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []},
- "goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []},
- "iconv": {:hex, :iconv, "1.0.0", "5ff1c54e5b3b9a8235de872632e9612c7952acdf89bc21db2f2efae0e72647be", [:rebar3], []},
+ "goldrush": {:hex, :goldrush, "0.1.8", "2024ba375ceea47e27ea70e14d2c483b2d8610101b4e852ef7f89163cdb6e649", [:rebar3], []},
+ "iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
- "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
- "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
+ "lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]},
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
- "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
- "p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []},
- "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
+ "p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []},
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
- "relx": {:hex, :relx, "3.20.0", "b515b8317d25b3a1508699294c3d1fa6dc0527851dffc87446661bce21a36710", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]},
- "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
- "stringprep": {:hex, :stringprep, "1.0.4", "f8f94d838ed202787699ff71d67b65481d350bda32b232ba1db52faca8eaed39", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
- "stun": {:hex, :stun, "1.0.5", "ec1d9928f25451d6fd2d2ade58c46b58b8d2c8326ddea3a667e926d04792f82c", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]}}
+ "relx": {:hex, :relx, "3.21.1", "f989dc520730efd9075e9f4debcb8ba1d7d1e86b018b0bcf45a2eb80270b4ad6", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
+ "stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
+ "stun": {:hex, :stun, "1.0.7", "904dc6f26a3c30c54881c4c3003699f2a4968067ee6b3aecdf9895aad02df75e", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}}
diff --git a/priv/msgs/cs.msg b/priv/msgs/cs.msg
index f0c749887..01897aadf 100644
--- a/priv/msgs/cs.msg
+++ b/priv/msgs/cs.msg
@@ -144,7 +144,7 @@
{"Import Users from Dir at ","Importovat uživatele z adresáře na "}.
{"Import Users From jabberd14 Spool Files","Importovat uživatele z jabberd14 spool souborů"}.
{"Improper message type","Nesprávný typ zprávy"}.
-{"Incoming s2s Connections:",""}.
+{"Incoming s2s Connections:","Příchozí s2s spojení:"}.
{"Incorrect password","Nesprávné heslo"}.
{"Invalid affiliation: ~s","Neplatné přiřazení: ~s"}.
{"Invalid role: ~s","Neplatná role: ~s"}.
@@ -333,7 +333,6 @@
{"September",". září"}.
{"Server ~b","Server ~b"}.
{"Server:","Server:"}.
-{"Server","Server"}.
{"Set message of the day and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}.
{"Set message of the day on all hosts and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}.
{"Shared Roster Groups","Skupiny pro sdílený seznam kontaktů"}.
@@ -409,10 +408,10 @@
{"User JID","Jabber ID uživatele"}.
{"User Management","Správa uživatelů"}.
{"Username:","Uživatelské jméno:"}.
-{"User ~s",""}.
{"Users are not allowed to register accounts so quickly","Je zakázáno registrovat účty v tak rychlém sledu"}.
{"Users Last Activity","Poslední aktivita uživatele"}.
{"Users","Uživatelé"}.
+{"User ~s","Uživatel ~s"}.
{"User","Uživatel"}.
{"Validate","Ověřit"}.
{"vCard User Search","Hledání uživatelů podle vizitek"}.
diff --git a/priv/msgs/cs.po b/priv/msgs/cs.po
index 81c60756e..4f06621ac 100644
--- a/priv/msgs/cs.po
+++ b/priv/msgs/cs.po
@@ -242,9 +242,7 @@ msgstr "Odchozí s2s spojení:"
#: ejabberd_web_admin.erl:1559
msgid "Incoming s2s Connections:"
-msgstr ""
-"Příchozí\n"
-" s2s spojení:"
+msgstr "Příchozí s2s spojení:"
#: ejabberd_web_admin.erl:1595 ejabberd_web_admin.erl:1794
#: ejabberd_web_admin.erl:1804 ejabberd_web_admin.erl:2214 mod_roster.erl:1429
@@ -258,9 +256,7 @@ msgstr "Změnit heslo"
#: ejabberd_web_admin.erl:1673
msgid "User ~s"
-msgstr ""
-"Uživatel\n"
-" ~s"
+msgstr "Uživatel ~s"
#: ejabberd_web_admin.erl:1684
msgid "Connected Resources:"
diff --git a/rebar.config b/rebar.config
index 6eb23f0f7..27439109b 100644
--- a/rebar.config
+++ b/rebar.config
@@ -9,16 +9,15 @@
{deps, [{lager, ".*", {git, "https://github.com/basho/lager", {tag, "3.2.1"}}},
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.5"}}},
- {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.3"}}},
- {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.6"}}},
- {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.5"}}},
- {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.14"}}},
- {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.6"}}},
- {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.7"}}},
- {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.5"}}},
+ {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.4"}}},
+ {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.7"}}},
+ {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.6"}}},
+ {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.15"}}},
+ {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.7"}}},
+ {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.8"}}},
+ {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.6"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}},
- {p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}},
{luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}},
{if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
{tag, "1.0.1"}}}},
@@ -34,16 +33,16 @@
{tag, "2.4.1"}}}},
%% Elixir support, needed to run tests
{if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir",
- {tag, "v1.1.1"}}}},
+ {tag, {if_version_above, "17", "v1.2.6", "v1.1.1"}}}}},
%% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin
{if_var_true, elixir, {rebar_elixir_plugin, ".*",
{git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}},
{if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv",
- {tag, "1.0.1"}}}},
+ {tag, "1.0.2"}}}},
{if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck",
{tag, "0.8.4"}}}},
{if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git",
- {tag, "1.0.5b"}}}},
+ {tag, "1.0.5c"}}}},
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
{tag, "v1.0.8"}}}}]}.
@@ -70,7 +69,9 @@
{if_var_true, debug, debug_info},
{if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}},
{if_var_match, db_type, mssql, {d, 'mssql'}},
+ {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
{if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}},
+ {if_version_above, "18", {d, 'STRONG_RAND_BYTES'}},
{if_var_true, hipe, native},
{src_dirs, [asn1, src,
{if_var_true, tools, tools},
diff --git a/rebar.config.script b/rebar.config.script
index 166f1cbec..ccafba7ec 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -19,7 +19,7 @@ ModCfg0 = fun(F, Cfg, [Key|Tail], Op, Default) ->
[{Key, F(F, OldVal, Tail, Op, Default)} | PartCfg]
end
end,
-ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end.
+ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end,
Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config")) of
{ok, Terms} ->
@@ -28,6 +28,13 @@ Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config"))
[]
end,
+ProcessSingleVar = fun(F, Var, Tail) ->
+ case F(F, [Var], []) of
+ [] -> Tail;
+ [Val] -> [Val | Tail]
+ end
+ end,
+
ProcessVars = fun(_F, [], Acc) ->
lists:reverse(Acc);
(F, [{Type, Ver, Value} | Tail], Acc) when
@@ -40,17 +47,31 @@ ProcessVars = fun(_F, [], Acc) ->
SysVer < Ver
end,
if Include ->
- F(F, Tail, [Value | Acc]);
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
true ->
F(F, Tail, Acc)
end;
+ (F, [{Type, Ver, Value, ElseValue} | Tail], Acc) when
+ Type == if_version_above orelse
+ Type == if_version_below ->
+ SysVer = erlang:system_info(otp_release),
+ Include = if Type == if_version_above ->
+ SysVer > Ver;
+ true ->
+ SysVer < Ver
+ end,
+ if Include ->
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
+ true ->
+ F(F, Tail, ProcessSingleVar(F, ElseValue, Acc))
+ end;
(F, [{Type, Var, Value} | Tail], Acc) when
Type == if_var_true orelse
Type == if_var_false ->
Flag = Type == if_var_true,
case proplists:get_bool(Var, Cfg) of
V when V == Flag ->
- F(F, Tail, [Value | Acc]);
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
_ ->
F(F, Tail, Acc)
end;
@@ -59,7 +80,7 @@ ProcessVars = fun(_F, [], Acc) ->
Type == if_var_no_match ->
case proplists:get_value(Var, Cfg) of
V when V == Match ->
- F(F, Tail, [Value | Acc]);
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
_ ->
F(F, Tail, Acc)
end;
@@ -146,7 +167,7 @@ Conf6 = case {lists:keyfind(cover_enabled, 1, Conf5), os:getenv("TRAVIS")} of
Conf5
end,
-%io:format("ejabberd configuration:~n ~p~n", [Conf5]),
+%io:format("ejabberd configuration:~n ~p~n", [Conf6]),
Conf6.
diff --git a/specs/xmpp_codec.spec b/specs/xmpp_codec.spec
index 608e1fe2d..12bc4b6a9 100644
--- a/specs/xmpp_codec.spec
+++ b/specs/xmpp_codec.spec
@@ -3387,6 +3387,71 @@
dec = {dec_int, [0, infinity]},
enc = {enc_int, []}}]}).
+-xml(privilege_perm,
+ #elem{name = <<"perm">>,
+ xmlns = <<"urn:xmpp:privilege:1">>,
+ result = {privilege_perm, '$access', '$type'},
+ attrs = [#attr{name = <<"access">>,
+ required = true,
+ dec = {dec_enum, [[roster, message, presence]]},
+ enc = {enc_enum, []}},
+ #attr{name = <<"type">>,
+ required = true,
+ dec = {dec_enum, [[none, get, set, both,
+ outgoing, roster,
+ managed_entity]]},
+ enc = {enc_enum, []}}]}).
+
+-xml(privilege,
+ #elem{name = <<"privilege">>,
+ xmlns = <<"urn:xmpp:privilege:1">>,
+ result = {privilege, '$perms', '$forwarded'},
+ refs = [#ref{name = privilege_perm, label = '$perms'},
+ #ref{name = forwarded, min = 0,
+ max = 1, label = '$forwarded'}]}).
+
+-xml(delegated_attribute,
+ #elem{name = <<"attribute">>,
+ xmlns = <<"urn:xmpp:delegation:1">>,
+ result = '$name',
+ attrs = [#attr{name = <<"name">>,
+ required = true}]}).
+
+-xml(delegated,
+ #elem{name = <<"delegated">>,
+ xmlns = <<"urn:xmpp:delegation:1">>,
+ result = {delegated, '$ns', '$attrs'},
+ attrs = [#attr{name = <<"namespace">>,
+ label = '$ns',
+ required = true}],
+ refs = [#ref{name = delegated_attribute,
+ label = '$attrs'}]}).
+
+-xml(delegation,
+ #elem{name = <<"delegation">>,
+ xmlns = <<"urn:xmpp:delegation:1">>,
+ result = {delegation, '$delegated', '$forwarded'},
+ refs = [#ref{name = delegated, label = '$delegated'},
+ #ref{name = forwarded, min = 0,
+ max = 1, label = '$forwarded'}]}).
+
+-xml(delegate,
+ #elem{name = <<"delegate">>,
+ xmlns = <<"urn:xmpp:delegation:1">>,
+ result = '$namespace',
+ attrs = [#attr{name = <<"namespace">>,
+ required = true}]}).
+
+-xml(delegation_query,
+ #elem{name = <<"query">>,
+ xmlns = <<"urn:xmpp:delegation:1">>,
+ result = {delegation_query, '$to', '$delegate'},
+ attrs = [#attr{name = <<"to">>,
+ required = true,
+ dec = {dec_jid, []},
+ enc = {enc_jid, []}}],
+ refs = [#ref{name = delegate, label = '$delegate'}]}).
+
-spec dec_tzo(_) -> {integer(), integer()}.
dec_tzo(Val) ->
[H1, M1] = str:tokens(Val, <<":">>),
diff --git a/sql/lite.sql b/sql/lite.sql
index 1741ea950..aacea11e7 100644
--- a/sql/lite.sql
+++ b/sql/lite.sql
@@ -313,3 +313,10 @@ CREATE TABLE sm (
CREATE UNIQUE INDEX i_sm_sid ON sm(usec, pid);
CREATE INDEX i_sm_node ON sm(node);
CREATE INDEX i_sm_username ON sm(username);
+
+CREATE TABLE oauth_token (
+ token text NOT NULL PRIMARY KEY,
+ jid text NOT NULL,
+ scope text NOT NULL,
+ expire bigint NOT NULL
+);
diff --git a/sql/mssql.sql b/sql/mssql.sql
index 45378d246..0dfaa7161 100644
--- a/sql/mssql.sql
+++ b/sql/mssql.sql
@@ -480,3 +480,13 @@ ON DELETE CASCADE;
ALTER TABLE [dbo].[pubsub_state] CHECK CONSTRAINT [pubsub_state_ibfk_1];
+CREATE TABLE [dbo].[oauth_token] (
+ [token] [varchar] (250) NOT NULL,
+ [jid] [text] NOT NULL,
+ [scope] [text] NOT NULL,
+ [expire] [bigint] NOT NULL,
+ CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED
+(
+ [token] ASC
+)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
+) TEXTIMAGE_ON [PRIMARY];
diff --git a/sql/mysql.sql b/sql/mysql.sql
index 5150fc45b..3d253c574 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -328,3 +328,10 @@ CREATE TABLE sm (
CREATE UNIQUE INDEX i_sid ON sm(usec, pid(75));
CREATE INDEX i_node ON sm(node(75));
CREATE INDEX i_username ON sm(username);
+
+CREATE TABLE oauth_token (
+ token varchar(191) NOT NULL PRIMARY KEY,
+ jid text NOT NULL,
+ scope text NOT NULL,
+ expire bigint NOT NULL
+) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
diff --git a/sql/pg.sql b/sql/pg.sql
index 1bc4f397c..3d7de4285 100644
--- a/sql/pg.sql
+++ b/sql/pg.sql
@@ -330,3 +330,12 @@ CREATE TABLE sm (
CREATE UNIQUE INDEX i_sm_sid ON sm USING btree (usec, pid);
CREATE INDEX i_sm_node ON sm USING btree (node);
CREATE INDEX i_sm_username ON sm USING btree (username);
+
+CREATE TABLE oauth_token (
+ token text NOT NULL,
+ jid text NOT NULL,
+ scope text NOT NULL,
+ expire bigint NOT NULL
+);
+
+CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token);
diff --git a/src/acl.erl b/src/acl.erl
index 7519e12e2..e3fdfcae1 100644
--- a/src/acl.erl
+++ b/src/acl.erl
@@ -31,11 +31,13 @@
-export([add_access/3, clear/0]).
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
- load_from_config/0, match_rule/3,
+ load_from_config/0, match_rule/3, any_rules_allowed/3,
transform_options/1, opt_type/1, acl_rule_matches/3,
acl_rule_verify/1, access_matches/3,
transform_access_rules_config/1,
- access_rules_validator/1, shaper_rules_validator/1]).
+ parse_ip_netmask/1,
+ access_rules_validator/1, shaper_rules_validator/1,
+ normalize_spec/1, resolve_access/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -74,12 +76,6 @@
-export_type([acl/0]).
start() ->
- case catch mnesia:table_info(acl, storage_type) of
- disc_copies ->
- mnesia:delete_table(acl);
- _ ->
- ok
- end,
mnesia:create_table(acl,
[{ram_copies, [node()]}, {type, bag},
{local_content, true},
@@ -261,6 +257,7 @@ normalize_spec(Spec) ->
{server, S} -> {server, nameprep(S)};
{resource, R} -> {resource, resourceprep(R)};
{server_regexp, SR} -> {server_regexp, b(SR)};
+ {resource_regexp, R} -> {resource_regexp, b(R)};
{server_glob, S} -> {server_glob, b(S)};
{resource_glob, R} -> {resource_glob, b(R)};
{ip, {Net, Mask}} -> {ip, {Net, Mask}};
@@ -274,6 +271,15 @@ normalize_spec(Spec) ->
end
end.
+-spec any_rules_allowed(global | binary(), access_name(),
+ jid() | ljid() | inet:ip_address()) -> boolean().
+
+any_rules_allowed(Host, Access, Entity) ->
+ lists:any(fun (Rule) ->
+ allow == acl:match_rule(Host, Rule, Entity)
+ end,
+ Access).
+
-spec match_rule(global | binary(), access_name(),
jid() | ljid() | inet:ip_address()) -> any().
@@ -432,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
acl_rule_matches(_ACL, _Data, _Host) ->
false.
--spec access_matches(atom()|list(), any(), global|binary()) -> any().
-access_matches(all, _Data, _Host) ->
- allow;
-access_matches(none, _Data, _Host) ->
- deny;
-access_matches(Name, Data, Host) when is_atom(Name) ->
- GAccess = ets:lookup(access, {Name, global}),
+resolve_access(all, _Host) ->
+ all;
+resolve_access(none, _Host) ->
+ none;
+resolve_access(Name, Host) when is_atom(Name) ->
+ GAccess = mnesia:dirty_read(access, {Name, global}),
LAccess =
- if Host /= global -> ets:lookup(access, {Name, Host});
+ if Host /= global -> mnesia:dirty_read(access, {Name, Host});
true -> []
end,
case GAccess ++ LAccess of
[] ->
- deny;
+ [];
AccessList ->
- Rules = lists:flatmap(
+ lists:flatmap(
fun(#access{rules = Rs}) ->
Rs
- end, AccessList),
- access_rules_matches(Rules, Data, Host)
+ end, AccessList)
end;
-access_matches(Rules, Data, Host) when is_list(Rules) ->
- access_rules_matches(Rules, Data, Host).
-
+resolve_access(Rules, _Host) when is_list(Rules) ->
+ Rules.
+
+-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny.
+access_matches(Rules, Data, Host) ->
+ case resolve_access(Rules, Host) of
+ all -> allow;
+ none -> deny;
+ RRules -> access_rules_matches(RRules, Data, Host)
+ end.
-spec access_rules_matches(list(), any(), global|binary()) -> any().
@@ -473,7 +484,7 @@ access_rules_matches([], _Data, _Host, Default) ->
Default.
get_aclspecs(ACL, Host) ->
- ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}).
+ mnesia:dirty_read(acl, {ACL, Host}) ++ mnesia:dirty_read(acl, {ACL, global}).
is_regexp_match(String, RegExp) ->
case ejabberd_regexp:run(String, RegExp) of
@@ -676,7 +687,8 @@ transform_options({acl, Name, Type}, Opts) ->
{server_regexp, SR} -> {server_regexp, [b(SR)]};
{server_glob, S} -> {server_glob, [b(S)]};
{ip, S} -> {ip, [b(S)]};
- {resource_glob, R} -> {resource_glob, [b(R)]}
+ {resource_glob, R} -> {resource_glob, [b(R)]};
+ {resource_regexp, R} -> {resource_regexp, [b(R)]}
end,
[{acl, [{Name, [T]}]}|Opts];
transform_options({access, Name, Rules}, Opts) ->
diff --git a/src/cyrsasl_oauth.erl b/src/cyrsasl_oauth.erl
index 09d143ef5..21dedc6db 100644
--- a/src/cyrsasl_oauth.erl
+++ b/src/cyrsasl_oauth.erl
@@ -51,7 +51,7 @@ mech_step(State, ClientIn) ->
{ok,
[{username, User}, {authzid, AuthzId},
{auth_module, ejabberd_oauth}]};
- false ->
+ _ ->
{error, 'not-authorized', User}
end;
_ -> {error, 'bad-protocol'}
diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl
index fdc40cd86..1e2a5c681 100644
--- a/src/cyrsasl_scram.erl
+++ b/src/cyrsasl_scram.erl
@@ -85,7 +85,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
if is_tuple(Ret) -> Ret;
true ->
TempSalt =
- crypto:rand_bytes(?SALT_LENGTH),
+ randoms:bytes(?SALT_LENGTH),
SaltedPassword =
scram:salted_password(Ret,
TempSalt,
@@ -99,7 +99,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
str:substr(ClientIn,
str:str(ClientIn, <<"n=">>)),
ServerNonce =
- jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)),
+ jlib:encode_base64(randoms:bytes(?NONCE_LENGTH)),
ServerFirstMessage =
iolist_to_binary(
["r=",
diff --git a/src/ejabberd.erl b/src/ejabberd.erl
index 6bd2422ae..5a6fc64d7 100644
--- a/src/ejabberd.erl
+++ b/src/ejabberd.erl
@@ -105,8 +105,6 @@ start_app([], _Type, _StartFlag) ->
ok.
check_app_modules(App, StartFlag) ->
- {A, B, C} = p1_time_compat:timestamp(),
- random:seed(A, B, C),
sleep(5000),
case application:get_key(App, modules) of
{ok, Mods} ->
@@ -140,7 +138,7 @@ exit_or_halt(Reason, StartFlag) ->
end.
sleep(N) ->
- timer:sleep(random:uniform(N)).
+ timer:sleep(randoms:uniform(N)).
get_module_file(App, Mod) ->
BaseName = atom_to_list(Mod),
diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl
new file mode 100644
index 000000000..7ce75aa9c
--- /dev/null
+++ b/src/ejabberd_access_permissions.erl
@@ -0,0 +1,543 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_access_permissions.erl
+%%% Author : Paweł Chmielowski <pawel@process-one.net>
+%%% Purpose : Administrative functions and commands
+%%% Created : 7 Sep 2016 by Paweł Chmielowski <pawel@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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(ejabberd_access_permissions).
+-author("pawel@process-one.net").
+
+-include("ejabberd_commands.hrl").
+-include("logger.hrl").
+
+-behaviour(gen_server).
+-behavior(ejabberd_config).
+
+%% API
+-export([start_link/0,
+ parse_api_permissions/1,
+ can_access/2,
+ invalidate/0,
+ opt_type/1,
+ show_current_definitions/0,
+ register_permission_addon/2,
+ unregister_permission_addon/1]).
+
+%% gen_server callbacks
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-define(SERVER, ?MODULE).
+
+-record(state, {
+ definitions = none,
+ fragments_generators = []
+}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+-spec can_access(atom(), map()) -> allow | deny.
+can_access(Cmd, CallerInfo) ->
+ gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}).
+
+-spec invalidate() -> ok.
+invalidate() ->
+ gen_server:cast(?MODULE, invalidate).
+
+-spec register_permission_addon(atom(), fun()) -> ok.
+register_permission_addon(Name, Fun) ->
+ gen_server:call(?MODULE, {register_config_fragment_generator, Name, Fun}).
+
+-spec unregister_permission_addon(atom()) -> ok.
+unregister_permission_addon(Name) ->
+ gen_server:call(?MODULE, {unregister_config_fragment_generator, Name}).
+
+-spec show_current_definitions() -> any().
+show_current_definitions() ->
+ gen_server:call(?MODULE, show_current_definitions).
+
+%%--------------------------------------------------------------------
+%% @doc
+%% Starts the server
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}.
+start_link() ->
+ gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Initializes the server
+%%
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @end
+%%--------------------------------------------------------------------
+-spec init(Args :: term()) ->
+ {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term()} | ignore.
+init([]) ->
+ {ok, #state{}}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling call messages
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_call(Request :: term(), From :: {pid(), Tag :: term()},
+ State :: #state{}) ->
+ {reply, Reply :: term(), NewState :: #state{}} |
+ {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
+ CallerModule = maps:get(caller_module, CallerInfo, none),
+ Host = maps:get(caller_host, CallerInfo, global),
+ {State2, Defs0} = get_definitions(State),
+ Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0,
+ Res = lists:foldl(
+ fun({Name, _} = Def, none) ->
+ case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of
+ true ->
+ ?DEBUG("Command '~p' execution allowed by rule '~s' (CallerInfo=~p)", [Cmd, Name, CallerInfo]),
+ allow;
+ _ ->
+ none
+ end;
+ (_, Val) ->
+ Val
+ end, none, Defs),
+ Res2 = case Res of
+ allow -> allow;
+ _ ->
+ ?DEBUG("Command '~p' execution denied (CallerInfo=~p)", [Cmd, CallerInfo]),
+ deny
+ end,
+ {reply, Res2, State2};
+handle_call(show_current_definitions, _From, State) ->
+ {State2, Defs} = get_definitions(State),
+ {reply, Defs, State2};
+handle_call({register_config_fragment_generator, Name, Fun}, _From, #state{fragments_generators = Gens} = State) ->
+ NGens = lists:keystore(Name, 1, Gens, {Name, Fun}),
+ {reply, ok, State#state{fragments_generators = NGens}};
+handle_call({unregister_config_fragment_generator, Name}, _From, #state{fragments_generators = Gens} = State) ->
+ NGens = lists:keydelete(Name, 1, Gens),
+ {reply, ok, State#state{fragments_generators = NGens}};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling cast messages
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_cast(Request :: term(), State :: #state{}) ->
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_cast(invalidate, State) ->
+ {noreply, State#state{definitions = none}};
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling all non call/cast messages
+%%
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_info(Info :: timeout() | term(), State :: #state{}) ->
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% This function is called by a gen_server 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_server terminates
+%% with Reason. The return value is ignored.
+%%
+%% @spec terminate(Reason, State) -> void()
+%% @end
+%%--------------------------------------------------------------------
+-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
+ State :: #state{}) -> term().
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Convert process state when code is changed
+%%
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @end
+%%--------------------------------------------------------------------
+-spec code_change(OldVsn :: term() | {down, term()}, State :: #state{},
+ Extra :: term()) ->
+ {ok, NewState :: #state{}} | {error, Reason :: term()}.
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+-spec get_definitions(#state{}) -> {#state{}, any()}.
+get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) ->
+ DefaultOptions = [{<<"console commands">>,
+ {[ejabberd_ctl],
+ [{acl, all}],
+ {all, none}}},
+ {<<"admin access">>,
+ {[],
+ [{acl, admin}],
+ {all, [start, stop]}}}],
+ NDefs = case Defs of
+ none ->
+ ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions),
+ AllCommands = ejabberd_commands:get_commands_definition(),
+ Frags = lists:foldl(
+ fun({_Name, Generator}, Acc) ->
+ Acc ++ Generator()
+ end, [], Gens),
+ lists:map(
+ fun({Name, {From, Who, {Add, Del}}}) ->
+ Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
+ {Name, {From, Who, Cmds}}
+ end, ApiPerms ++ Frags);
+ V ->
+ V
+ end,
+ {State#state{definitions = NDefs}, NDefs}.
+
+matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) ->
+ case What == all orelse lists:member(Cmd, What) of
+ true ->
+ case From == [] orelse lists:member(Module, From) of
+ true ->
+ Scope = maps:get(oauth_scope, CallerInfo, none),
+ lists:any(
+ fun({access, Access}) when Scope == none ->
+ acl:access_matches(Access, CallerInfo, Host) == allow;
+ ({acl, _} = Acl) when Scope == none ->
+ acl:acl_rule_matches(Acl, CallerInfo, Host);
+ ({oauth, Scopes, List}) when Scope /= none ->
+ case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of
+ true ->
+ lists:any(
+ fun({access, Access}) ->
+ acl:access_matches(Access, CallerInfo, Host) == allow;
+ ({acl, _} = Acl) ->
+ acl:acl_rule_matches(Acl, CallerInfo, Host)
+ end, List);
+ _ ->
+ false
+ end;
+ (_) ->
+ false
+ end, Who);
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end.
+
+filter_commands_with_permissions(AllCommands, Add, Del) ->
+ CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []),
+ CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []),
+ lists:map(fun(#ejabberd_commands{name = N}) -> N end,
+ CommandsAdd -- CommandsDel).
+
+filter_commands_with_patterns([], _Patterns, Acc) ->
+ Acc;
+filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
+ case command_matches_patterns(C, Patterns) of
+ true ->
+ filter_commands_with_patterns(CRest, Patterns, [C | Acc]);
+ _ ->
+ filter_commands_with_patterns(CRest, Patterns, Acc)
+ end.
+
+command_matches_patterns(_, all) ->
+ true;
+command_matches_patterns(_, none) ->
+ false;
+command_matches_patterns(_, []) ->
+ false;
+command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) ->
+ case lists:member(Tag, Tags) of
+ true ->
+ true;
+ _ ->
+ command_matches_patterns(C, Tail)
+ end;
+command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) ->
+ true;
+command_matches_patterns(C, [_ | Tail]) ->
+ command_matches_patterns(C, Tail).
+
+%%%===================================================================
+%%% Options parsing code
+%%%===================================================================
+
+parse_api_permissions(Data) when is_list(Data) ->
+ throw({replace_with, [parse_api_permission(Name, Args) || {Name, Args} <- Data]}).
+
+parse_api_permission(Name, Args) ->
+ {From, Who, What} = case key_split(Args, [{from, []}, {who, none}, {what, []}]) of
+ {error, Msg} ->
+ report_error(<<"~s inside api_permission '~s' section">>, [Msg, Name]);
+ Val -> Val
+ end,
+ {Name, {parse_from(Name, From), parse_who(Name, Who, oauth), parse_what(Name, What)}}.
+
+parse_from(_Name, Module) when is_atom(Module) ->
+ [Module];
+parse_from(Name, Modules) when is_list(Modules) ->
+ lists:foreach(fun(Module) when is_atom(Module) ->
+ ok;
+ (Val) ->
+ report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
+ [Val, Name])
+ end, Modules),
+ Modules;
+parse_from(Name, Val) ->
+ report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_who(Name, Atom, ParseOauth) when is_atom(Atom) ->
+ parse_who(Name, [Atom], ParseOauth);
+parse_who(Name, Defs, ParseOauth) when is_list(Defs) ->
+ lists:map(
+ fun([{access, Val}]) ->
+ try acl:access_rules_validator(Val) of
+ Rule ->
+ {access, Rule}
+ catch
+ throw:{invalid_syntax, Msg} ->
+ report_error(<<"Invalid access rule: '~s' used inside 'who' section for api_permission '~s'">>,
+ [Msg, Name]);
+ throw:{replace_with, NVal} ->
+ {access, NVal};
+ error:_ ->
+ report_error(<<"Invalid access rule '~p' used inside 'who' section for api_permission '~s'">>,
+ [Val, Name])
+ end;
+ ([{oauth, OauthList}]) when is_list(OauthList) ->
+ case ParseOauth of
+ oauth ->
+ Nested = parse_who(Name, lists:flatten(OauthList), scope),
+ {Scopes, Rest} = lists:partition(
+ fun({scope, _}) -> true;
+ (_) -> false
+ end, Nested),
+ case Scopes of
+ [] ->
+ report_error(<<"Oauth rule must contain at least one scope rule in 'who' section for api_permission '~s'">>,
+ [Name]);
+ _ ->
+ {oauth, lists:foldl(fun({scope, S}, A) -> S ++ A end, [], Scopes), Rest}
+ end;
+ scope ->
+ report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>,
+ [Name])
+ end;
+ ({scope, ScopeList}) ->
+ case ParseOauth of
+ oauth ->
+ report_error(<<"Scope can be included only inside oauth rule in 'who' section for api_permission '~s'">>,
+ [Name]);
+ scope ->
+ ScopeList2 = case ScopeList of
+ V when is_binary(V) -> [V];
+ V2 when is_list(V2) -> V2;
+ V3 ->
+ report_error(<<"Invalid value for scope '~p' in 'who' section for api_permission '~s'">>,
+ [V3, Name])
+ end,
+ {scope, ScopeList2}
+ end;
+ (Atom) when is_atom(Atom) ->
+ {acl, Atom};
+ ([Other]) ->
+ try acl:normalize_spec(Other) of
+ Rule2 ->
+ {acl, Rule2}
+ catch
+ _:_ ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Other, Name])
+ end;
+ (Invalid) ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Invalid, Name])
+ end, Defs);
+parse_who(Name, Val, _ParseOauth) ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_what(Name, Binary) when is_binary(Binary) ->
+ parse_what(Name, [Binary]);
+parse_what(Name, Defs) when is_list(Defs) ->
+ {A, D} = lists:foldl(
+ fun(Def, {Add, Del}) ->
+ case parse_single_what(Def) of
+ {error, Err} ->
+ report_error(<<"~s used in value '~p' in 'what' section for api_permission '~s'">>,
+ [Err, Def, Name]);
+ all ->
+ {case Add of none -> none; _ -> all end, Del};
+ {neg, all} ->
+ {none, all};
+ {neg, Value} ->
+ {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end};
+ Value ->
+ {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del}
+ end
+ end, {[], []}, Defs),
+ case {A, D} of
+ {[], _} ->
+ {none, all};
+ {A2, []} ->
+ {A2, none};
+ V ->
+ V
+ end;
+parse_what(Name, Val) ->
+ report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_single_what(<<"*">>) ->
+ all;
+parse_single_what(<<"!*">>) ->
+ {neg, all};
+parse_single_what(<<"!", Rest/binary>>) ->
+ case parse_single_what(Rest) of
+ {neg, _} ->
+ {error, <<"Double negation">>};
+ {error, _} = Err ->
+ Err;
+ V ->
+ {neg, V}
+ end;
+parse_single_what(<<"[tag:", Rest/binary>>) ->
+ case binary:split(Rest, <<"]">>) of
+ [TagName, <<"">>] ->
+ case parse_single_what(TagName) of
+ {error, _} = Err ->
+ Err;
+ V when is_atom(V) ->
+ {tag, V};
+ _ ->
+ {error, <<"Invalid tag">>}
+ end;
+ _ ->
+ {error, <<"Invalid tag">>}
+ end;
+parse_single_what(Binary) when is_binary(Binary) ->
+ case is_valid_command_name(Binary) of
+ true ->
+ binary_to_atom(Binary, latin1);
+ _ ->
+ {error, <<"Invalid value">>}
+ end;
+parse_single_what(_) ->
+ {error, <<"Invalid value">>}.
+
+is_valid_command_name(<<>>) ->
+ false;
+is_valid_command_name(Val) ->
+ is_valid_command_name2(Val).
+
+is_valid_command_name2(<<>>) ->
+ true;
+is_valid_command_name2(<<K:8, Rest/binary>>) when K >= $a andalso K =< $z orelse K == $_ ->
+ is_valid_command_name2(Rest);
+is_valid_command_name2(_) ->
+ false.
+
+key_split(Args, Fields) ->
+ {_, Order1, Results1, Required1} = lists:foldl(
+ fun({Field, Default}, {Idx, Order, Results, Required}) ->
+ {Idx + 1, maps:put(Field, Idx, Order), [Default | Results], Required};
+ (Field, {Idx, Order, Results, Required}) ->
+ {Idx + 1, maps:put(Field, Idx, Order), [none | Results], maps:put(Field, 1, Required)}
+ end, {1, #{}, [], #{}}, Fields),
+ key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}).
+
+key_split([], _Results, _Order, Required, _Duplicates) when map_size(Required) > 0 ->
+ parse_error(<<"Missing fields '~s">>, [str:join(maps:keys(Required), <<", ">>)]);
+key_split([], Results, _Order, _Required, _Duplicates) ->
+ Results;
+key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) ->
+ case maps:find(Arg, Order) of
+ {ok, Idx} ->
+ case maps:is_key(Arg, Duplicates) of
+ false ->
+ Results2 = setelement(Idx, Results, Value),
+ key_split(Rest, Results2, Order, maps:remove(Arg, Required), maps:put(Arg, 1, Duplicates));
+ true ->
+ parse_error(<<"Duplicate field '~s'">>, [Arg])
+ end;
+ _ ->
+ parse_error(<<"Unknown field '~s'">>, [Arg])
+ end.
+
+report_error(Format, Args) ->
+ throw({invalid_syntax, iolist_to_binary(io_lib:format(Format, Args))}).
+
+parse_error(Format, Args) ->
+ {error, iolist_to_binary(io_lib:format(Format, Args))}.
+
+opt_type(api_permissions) ->
+ fun parse_api_permissions/1;
+opt_type(_) ->
+ [api_permissions].
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 87ac76875..8622ea8d0 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -87,6 +87,7 @@ get_commands_spec() ->
args = [], result = {res, rescode}},
#ejabberd_commands{name = reopen_log, tags = [logs, server],
desc = "Reopen the log files",
+ policy = admin,
module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}},
#ejabberd_commands{name = rotate_log, tags = [logs, server],
@@ -129,6 +130,7 @@ get_commands_spec() ->
#ejabberd_commands{name = register, tags = [accounts],
desc = "Register a user",
+ policy = admin,
module = ?MODULE, function = register,
args = [{user, binary}, {host, binary}, {password, binary}],
result = {res, restuple}},
@@ -166,7 +168,7 @@ get_commands_spec() ->
#ejabberd_commands{name = list_cluster, tags = [cluster],
desc = "List nodes that are part of the cluster handled by Node",
module = ?MODULE, function = list_cluster,
- args = [],
+ args = [],
result = {nodes, {list, {node, atom}}}},
#ejabberd_commands{name = import_file, tags = [mnesia],
@@ -220,7 +222,7 @@ get_commands_spec() ->
desc = "Delete offline messages older than DAYS",
module = ?MODULE, function = delete_old_messages,
args = [{days, integer}], result = {res, rescode}},
-
+
#ejabberd_commands{name = export2sql, tags = [mnesia],
desc = "Export virtual host information from Mnesia tables to SQL files",
module = ejd2sql, function = export,
@@ -378,13 +380,12 @@ register(User, Host, Password) ->
{atomic, ok} ->
{ok, io_lib:format("User ~s@~s successfully registered", [User, Host])};
{atomic, exists} ->
- String = io_lib:format("User ~s@~s already registered at node ~p",
- [User, Host, node()]),
- {exists, String};
+ Msg = io_lib:format("User ~s@~s already registered", [User, Host]),
+ {error, conflict, 10090, Msg};
{error, Reason} ->
String = io_lib:format("Can't register user ~s@~s at node ~p: ~p",
[User, Host, node(), Reason]),
- {cannot_register, String}
+ {error, cannot_register, 10001, String}
end.
unregister(User, Host) ->
@@ -402,7 +403,8 @@ registered_vhosts() ->
reload_config() ->
ejabberd_config:reload_file(),
acl:start(),
- shaper:start().
+ shaper:start(),
+ ejabberd_access_permissions:invalidate().
%%%
%%% Cluster management
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl
index 703614f63..e88f24e1e 100644
--- a/src/ejabberd_app.erl
+++ b/src/ejabberd_app.erl
@@ -45,16 +45,19 @@ start(normal, _Args) ->
write_pid_file(),
jid:start(),
start_apps(),
+ start_elixir_application(),
ejabberd:check_app(ejabberd),
randoms:start(),
db_init(),
start(),
translate:start(),
+ ejabberd_access_permissions:start_link(),
ejabberd_ctl:init(),
ejabberd_commands:init(),
ejabberd_admin:start(),
gen_mod:start(),
ext_mod:start(),
+ setup_if_elixir_conf_used(),
ejabberd_config:start(),
set_settings_from_config(),
acl:start(),
@@ -74,6 +77,7 @@ start(normal, _Args) ->
ejabberd_oauth:start(),
gen_mod:start_modules(),
ejabberd_listener:start_listeners(),
+ register_elixir_config_hooks(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),
Sup;
start(_, _) ->
@@ -221,6 +225,7 @@ start_apps() ->
ejabberd:start_app(fast_tls),
ejabberd:start_app(fast_xml),
ejabberd:start_app(stringprep),
+ http_p1:start(),
ejabberd:start_app(cache_tab).
opt_type(net_ticktime) ->
@@ -237,3 +242,26 @@ opt_type(modules) ->
Mods)
end;
opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime].
+
+setup_if_elixir_conf_used() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config.Store':start_link();
+ false -> ok
+ end.
+
+register_elixir_config_hooks() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config':start_hooks();
+ false -> ok
+ end.
+
+start_elixir_application() ->
+ case ejabberd_config:is_elixir_enabled() of
+ true ->
+ case application:ensure_started(elixir) of
+ ok -> ok;
+ {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", [])
+ end;
+ _ ->
+ ok
+ end.
diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl
index 2a4554d15..f36c9fbc7 100644
--- a/src/ejabberd_auth_mnesia.erl
+++ b/src/ejabberd_auth_mnesia.erl
@@ -450,7 +450,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
- Salt = crypto:rand_bytes(?SALT_LENGTH),
+ Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =
diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl
index c74f1b28e..05add262e 100644
--- a/src/ejabberd_auth_riak.erl
+++ b/src/ejabberd_auth_riak.erl
@@ -270,7 +270,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
- Salt = crypto:rand_bytes(?SALT_LENGTH),
+ Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =
diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl
index d6d945e02..93dac4f4f 100644
--- a/src/ejabberd_auth_sql.erl
+++ b/src/ejabberd_auth_sql.erl
@@ -406,7 +406,7 @@ password_to_scram(Password) ->
?SCRAM_DEFAULT_ITERATION_COUNT).
password_to_scram(Password, IterationCount) ->
- Salt = crypto:rand_bytes(?SALT_LENGTH),
+ Salt = randoms:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Password, Salt,
IterationCount),
StoredKey =
diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl
index 986310546..7ef708d31 100644
--- a/src/ejabberd_c2s.erl
+++ b/src/ejabberd_c2s.erl
@@ -32,6 +32,7 @@
-protocol({xep, 78, '2.5'}).
-protocol({xep, 138, '2.0'}).
-protocol({xep, 198, '1.3'}).
+-protocol({xep, 356, '7.1'}).
-update_info({update, 0}).
@@ -48,10 +49,16 @@
send_element/2,
socket_type/0,
get_presence/1,
+ get_last_presence/1,
get_aux_field/2,
set_aux_field/3,
del_aux_field/2,
get_subscription/2,
+ get_queued_stanzas/1,
+ get_csi_state/1,
+ set_csi_state/2,
+ get_resume_timeout/1,
+ set_resume_timeout/2,
send_filtered/5,
broadcast/4,
get_subscribed/1,
@@ -112,9 +119,12 @@
mgmt_pending_since,
mgmt_timeout,
mgmt_max_timeout,
+ mgmt_ack_timeout,
+ mgmt_ack_timer,
mgmt_resend,
mgmt_stanzas_in = 0,
mgmt_stanzas_out = 0,
+ mgmt_stanzas_req = 0,
ask_offline = true,
lang = <<"">>}).
@@ -182,6 +192,9 @@ socket_type() -> xml_stream.
get_presence(FsmRef) ->
(?GEN_FSM):sync_send_all_state_event(FsmRef,
{get_presence}, 1000).
+get_last_presence(FsmRef) ->
+ (?GEN_FSM):sync_send_all_state_event(FsmRef,
+ {get_last_presence}, 1000).
-spec get_aux_field(any(), state()) -> {ok, any()} | error.
get_aux_field(Key, #state{aux_fields = Opts}) ->
@@ -218,6 +231,27 @@ get_subscription(LFrom, StateData) ->
true -> none
end.
+get_queued_stanzas(#state{mgmt_queue = Queue} = StateData) ->
+ lists:map(fun({_N, Time, El}) ->
+ add_resent_delay_info(StateData, El, Time)
+ end, queue:to_list(Queue)).
+
+get_csi_state(#state{csi_state = CsiState}) ->
+ CsiState.
+
+set_csi_state(#state{} = StateData, CsiState) ->
+ StateData#state{csi_state = CsiState};
+set_csi_state(FsmRef, CsiState) ->
+ FsmRef ! {set_csi_state, CsiState}.
+
+get_resume_timeout(#state{mgmt_timeout = Timeout}) ->
+ Timeout.
+
+set_resume_timeout(#state{} = StateData, Timeout) ->
+ StateData#state{mgmt_timeout = Timeout};
+set_resume_timeout(FsmRef, Timeout) ->
+ FsmRef ! {set_resume_timeout, Timeout}.
+
-spec send_filtered(pid(), binary(), jid(), jid(), stanza()) -> any().
send_filtered(FsmRef, Feature, From, To, Packet) ->
FsmRef ! {send_filtered, Feature, From, To, Packet}.
@@ -282,13 +316,18 @@ init([{SockMod, Socket}, Opts]) ->
_ -> 1000
end,
ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of
- Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout;
+ RTimeo when is_integer(RTimeo), RTimeo >= 0 -> RTimeo;
_ -> 300
end,
MaxResumeTimeout = case proplists:get_value(max_resume_timeout, Opts) of
Max when is_integer(Max), Max >= ResumeTimeout -> Max;
_ -> ResumeTimeout
end,
+ AckTimeout = case proplists:get_value(ack_timeout, Opts) of
+ ATimeo when is_integer(ATimeo), ATimeo > 0 -> ATimeo * 1000;
+ infinity -> undefined;
+ _ -> 60000
+ end,
ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of
Resend when is_boolean(Resend) -> Resend;
if_offline -> if_offline;
@@ -312,6 +351,7 @@ init([{SockMod, Socket}, Opts]) ->
mgmt_max_queue = MaxAckQueue,
mgmt_timeout = ResumeTimeout,
mgmt_max_timeout = MaxResumeTimeout,
+ mgmt_ack_timeout = AckTimeout,
mgmt_resend = ResendOnTimeout},
{ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}.
@@ -1147,6 +1187,15 @@ handle_sync_event({get_presence}, _From, StateName,
Resource = StateData#state.resource,
Reply = {User, Resource, Show, Status},
fsm_reply(Reply, StateName, StateData);
+handle_sync_event({get_last_presence}, _From, StateName,
+ StateData) ->
+ User = StateData#state.user,
+ Server = StateData#state.server,
+ PresLast = StateData#state.pres_last,
+ Resource = StateData#state.resource,
+ Reply = {User, Server, Resource, PresLast},
+ fsm_reply(Reply, StateName, StateData);
+
handle_sync_event(get_subscribed, _From, StateName,
StateData) ->
Subscribed = (?SETS):to_list(StateData#state.pres_f),
@@ -1159,7 +1208,7 @@ handle_sync_event({resume_session, Time}, _From, _StateName,
StateData#state.user,
StateData#state.server,
StateData#state.resource),
- {stop, normal, {ok, StateData}, StateData#state{mgmt_state = resumed}};
+ {stop, normal, {resume, StateData}, StateData#state{mgmt_state = resumed}};
handle_sync_event({resume_session, _Time}, _From, StateName,
StateData) ->
{reply, {error, <<"Previous session not found">>}, StateName, StateData};
@@ -1347,8 +1396,13 @@ handle_info({route, From, To, Packet}, StateName, StateData) when ?is_stanza(Pac
groupchat -> ok;
headline -> ok;
_ ->
- ejabberd_router:route_error(
- To, From, Packet, xmpp:err_service_unavailable())
+ case xmpp:has_subtag(Packet, #muc_user{}) of
+ true ->
+ ok;
+ false ->
+ ejabberd_router:route_error(
+ To, From, Packet, xmpp:err_service_unavailable())
+ end
end,
{false, StateData}
end
@@ -1444,8 +1498,24 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) ->
From, jid:make(USR), Packet)
end, lists:usort(Recipients)),
fsm_next_state(StateName, StateData);
+handle_info({set_csi_state, CsiState}, StateName, StateData) ->
+ fsm_next_state(StateName, StateData#state{csi_state = CsiState});
+handle_info({set_resume_timeout, Timeout}, StateName, StateData) ->
+ fsm_next_state(StateName, StateData#state{mgmt_timeout = Timeout});
handle_info(dont_ask_offline, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{ask_offline = false});
+handle_info(close, StateName, StateData) ->
+ ?DEBUG("Timeout waiting for stream management acknowledgement of ~s",
+ [jid:to_string(StateData#state.jid)]),
+ close(self()),
+ fsm_next_state(StateName, StateData#state{mgmt_ack_timer = undefined});
+handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) ->
+ %% This happens if the resume_session/1 request timed out; the new session
+ %% now receives the late response.
+ ?DEBUG("Received old session state for ~s after failed resumption",
+ [jid:to_string(OldStateData#state.jid)]),
+ handle_unacked_stanzas(OldStateData#state{mgmt_resend = false}),
+ fsm_next_state(StateName, StateData);
handle_info(Info, StateName, StateData) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
fsm_next_state(StateName, StateData).
@@ -1562,6 +1632,7 @@ send_text(StateData, Text) ->
send_element(StateData, El) when StateData#state.mgmt_state == pending ->
?DEBUG("Cannot send element while waiting for resumption: ~p", [El]);
send_element(StateData, #xmlel{} = El) when StateData#state.xml_socket ->
+ ?DEBUG("Send XML on stream = ~p", [fxml:element_to_binary(El)]),
(StateData#state.sockmod):send_xml(StateData#state.socket,
{xmlstreamelement, El});
send_element(StateData, #xmlel{} = El) ->
@@ -1585,8 +1656,8 @@ send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive ->
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending ->
mgmt_queue_add(StateData, Stanza);
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active ->
- NewStateData = send_stanza_and_ack_req(StateData, Stanza),
- mgmt_queue_add(NewStateData, Stanza);
+ NewStateData = mgmt_queue_add(StateData, Stanza),
+ mgmt_send_stanza(NewStateData, Stanza);
send_stanza(StateData, Stanza) ->
send_element(StateData, Stanza),
StateData.
@@ -2101,13 +2172,25 @@ fsm_next_state(session_established, StateData) ->
?C2S_HIBERNATE_TIMEOUT};
fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) ->
{stop, normal, StateData};
-fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} =
- StateData) ->
+fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined,
+ sid = SID, jid = JID, ip = IP,
+ conn = Conn, auth_module = AuthModule,
+ server = Host} = StateData) ->
+ case StateData of
+ #state{mgmt_ack_timer = undefined} ->
+ ok;
+ #state{mgmt_ack_timer = Timer} ->
+ erlang:cancel_timer(Timer)
+ end,
?INFO_MSG("Waiting for resumption of stream for ~s",
- [jid:to_string(StateData#state.jid)]),
+ [jid:to_string(JID)]),
+ Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}],
+ NewStateData = ejabberd_hooks:run_fold(c2s_session_pending, Host, StateData,
+ [SID, JID, Info]),
{next_state, wait_for_resume,
- StateData#state{mgmt_state = pending, mgmt_pending_since = os:timestamp()},
- StateData#state.mgmt_timeout};
+ NewStateData#state{mgmt_state = pending,
+ mgmt_pending_since = os:timestamp()},
+ NewStateData#state.mgmt_timeout};
fsm_next_state(wait_for_resume, StateData) ->
Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since),
Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1),
@@ -2338,15 +2421,16 @@ handle_r(StateData) ->
-spec handle_a(state(), sm_a()) -> state().
handle_a(StateData, #sm_a{h = H}) ->
- check_h_attribute(StateData, H).
+ NewStateData = check_h_attribute(StateData, H),
+ maybe_renew_ack_request(NewStateData).
-spec handle_resume(state(), sm_resume()) -> {ok, state()} | error.
handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
R = case stream_mgmt_enabled(StateData) of
true ->
case inherit_session_state(StateData, PrevID) of
- {ok, InheritedState} ->
- {ok, InheritedState, H};
+ {ok, InheritedState, Info} ->
+ {ok, InheritedState, Info, H};
{error, Err, InH} ->
{error, #sm_failed{reason = 'item-not-found',
h = InH, xmlns = Xmlns}, Err};
@@ -2360,7 +2444,7 @@ handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
<<"XEP-0198 disabled">>}
end,
case R of
- {ok, ResumedState, NumHandled} ->
+ {ok, ResumedState, ResumedInfo, NumHandled} ->
NewState = check_h_attribute(ResumedState, NumHandled),
AttrXmlns = NewState#state.mgmt_xmlns,
AttrId = make_resume_id(NewState),
@@ -2374,11 +2458,16 @@ handle_resume(StateData, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) ->
end,
handle_unacked_stanzas(NewState, SendFun),
send_element(NewState, #sm_r{xmlns = AttrXmlns}),
- FlushedState = csi_flush_queue(NewState),
- NewStateData = FlushedState#state{csi_state = active},
+ NewState1 = csi_flush_queue(NewState),
+ NewState2 = ejabberd_hooks:run_fold(c2s_session_resumed,
+ StateData#state.server,
+ NewState1,
+ [NewState1#state.sid,
+ NewState1#state.jid,
+ ResumedInfo]),
?INFO_MSG("Resumed session for ~s",
- [jid:to_string(NewStateData#state.jid)]),
- {ok, NewStateData};
+ [jid:to_string(NewState2#state.jid)]),
+ {ok, NewState2};
{error, El, Msg} ->
send_element(StateData, El),
?INFO_MSG("Cannot resume session for ~s@~s: ~s",
@@ -2413,15 +2502,45 @@ update_num_stanzas_in(#state{mgmt_state = MgmtState} = StateData, El)
update_num_stanzas_in(StateData, _El) ->
StateData.
--spec send_stanza_and_ack_req(state(), stanza()) -> state().
-send_stanza_and_ack_req(StateData, Stanza) ->
- AckReq = #sm_r{xmlns = StateData#state.mgmt_xmlns},
- case send_element(StateData, Stanza) == ok andalso
- send_element(StateData, AckReq) == ok of
+mgmt_send_stanza(StateData, Stanza) ->
+ case send_element(StateData, Stanza) of
+ ok ->
+ maybe_request_ack(StateData);
+ _ ->
+ StateData#state{mgmt_state = pending}
+ end.
+
+maybe_request_ack(#state{mgmt_ack_timer = undefined} = StateData) ->
+ request_ack(StateData);
+maybe_request_ack(StateData) ->
+ StateData.
+
+request_ack(#state{mgmt_xmlns = Xmlns,
+ mgmt_ack_timeout = AckTimeout} = StateData) ->
+ AckReq = #sm_r{xmlns = Xmlns},
+ case {send_element(StateData, AckReq), AckTimeout} of
+ {ok, undefined} ->
+ ok;
+ {ok, Timeout} ->
+ Timer = erlang:send_after(Timeout, self(), close),
+ StateData#state{mgmt_ack_timer = Timer,
+ mgmt_stanzas_req = StateData#state.mgmt_stanzas_out};
+ _ ->
+ StateData#state{mgmt_state = pending}
+ end.
+
+maybe_renew_ack_request(#state{mgmt_ack_timer = undefined} = StateData) ->
+ StateData;
+maybe_renew_ack_request(#state{mgmt_ack_timer = Timer,
+ mgmt_queue = Queue,
+ mgmt_stanzas_out = NumStanzasOut,
+ mgmt_stanzas_req = NumStanzasReq} = StateData) ->
+ erlang:cancel_timer(Timer),
+ case NumStanzasReq < NumStanzasOut andalso not queue:is_empty(Queue) of
true ->
- StateData;
+ request_ack(StateData#state{mgmt_ack_timer = undefined});
false ->
- StateData#state{mgmt_state = pending}
+ StateData#state{mgmt_ack_timer = undefined}
end.
-spec mgmt_queue_add(state(), xmpp_element()) -> state().
@@ -2473,7 +2592,12 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F)
fun({_, Time, Pkt}) ->
From = xmpp:get_from(Pkt),
To = xmpp:get_to(Pkt),
- F(From, To, Pkt, Time)
+ case {From, To} of
+ {#jid{}, #jid{}} ->
+ F(From, To, Pkt, Time);
+ {_, _} ->
+ ?DEBUG("Dropping stanza due to invalid JID(s)", [])
+ end
end, queue:to_list(Queue))
end;
handle_unacked_stanzas(_StateData, _F) ->
@@ -2540,7 +2664,8 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData)
[StateData, From,
StateData#state.jid, El]) of
true ->
- ok;
+ ?DEBUG("Dropping archived message stanza from ~p",
+ [jid:to_string(xmpp:get_from(El))]);
false ->
ReRoute(From, To, El, Time)
end
@@ -2580,7 +2705,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
OldPID ->
OldSID = {Time, OldPID},
case catch resume_session(OldSID) of
- {ok, OldStateData} ->
+ {resume, OldStateData} ->
NewSID = {Time, self()}, % Old time, new PID
Priority = case OldStateData#state.pres_last of
undefined ->
@@ -2604,13 +2729,13 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
pres_timestamp = OldStateData#state.pres_timestamp,
privacy_list = OldStateData#state.privacy_list,
aux_fields = OldStateData#state.aux_fields,
- csi_state = OldStateData#state.csi_state,
mgmt_xmlns = OldStateData#state.mgmt_xmlns,
mgmt_queue = OldStateData#state.mgmt_queue,
mgmt_timeout = OldStateData#state.mgmt_timeout,
mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in,
mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out,
- mgmt_state = active}};
+ mgmt_state = active,
+ csi_state = active}, Info};
{error, Msg} ->
{error, Msg};
_ ->
@@ -2623,7 +2748,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
-spec resume_session({integer(), pid()}) -> any().
resume_session({Time, PID}) ->
- (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 5000).
+ (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 15000).
-spec make_resume_id(state()) -> binary().
make_resume_id(StateData) ->
@@ -2640,11 +2765,11 @@ add_resent_delay_info(#state{server = From}, El, Time) ->
%%% XEP-0352
%%%----------------------------------------------------------------------
-spec csi_filter_stanza(state(), stanza()) -> state().
-csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData,
- Stanza) ->
+csi_filter_stanza(#state{csi_state = CsiState, jid = JID, server = Server} =
+ StateData, Stanza) ->
{StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_filter_stanza, Server,
{StateData, [Stanza]},
- [Server, Stanza]),
+ [Server, JID, Stanza]),
StateData2 = lists:foldl(fun(CurStanza, AccState) ->
send_stanza(AccState, CurStanza)
end, StateData1#state{csi_state = active},
@@ -2652,9 +2777,11 @@ csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData,
StateData2#state{csi_state = CsiState}.
-spec csi_flush_queue(state()) -> state().
-csi_flush_queue(#state{csi_state = CsiState, server = Server} = StateData) ->
+csi_flush_queue(#state{csi_state = CsiState, jid = JID, server = Server} =
+ StateData) ->
{StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_flush_queue, Server,
- {StateData, []}, [Server]),
+ {StateData, []},
+ [Server, JID]),
StateData2 = lists:foldl(fun(CurStanza, AccState) ->
send_stanza(AccState, CurStanza)
end, StateData1#state{csi_state = active},
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 9d41f50c2..8d74ad5a2 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -218,22 +218,26 @@
get_command_format/1,
get_command_format/2,
get_command_format/3,
- get_command_policy/1,
+ get_command_policy_and_scope/1,
get_command_definition/1,
get_command_definition/2,
get_tags_commands/0,
get_tags_commands/1,
- get_commands/0,
+ get_exposed_commands/0,
register_commands/1,
unregister_commands/1,
+ expose_commands/1,
execute_command/2,
execute_command/3,
execute_command/4,
execute_command/5,
execute_command/6,
- opt_type/1,
- get_commands_spec/0
- ]).
+ opt_type/1,
+ get_commands_spec/0,
+ get_commands_definition/0,
+ get_commands_definition/1,
+ execute_command2/3,
+ execute_command2/4]).
-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
@@ -273,28 +277,32 @@ get_commands_spec() ->
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}].
init() ->
- mnesia:delete_table(ejabberd_commands),
mnesia:create_table(ejabberd_commands,
- [{ram_copies, [node()]},
+ [{ram_copies, [node()]},
{local_content, true},
- {attributes, record_info(fields, ejabberd_commands)},
- {type, bag}]),
+ {attributes, record_info(fields, ejabberd_commands)},
+ {type, bag}]),
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
- register_commands(get_commands_spec()).
+ register_commands(get_commands_spec()),
+ ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0).
-spec register_commands([ejabberd_commands()]) -> ok.
%% @doc Register ejabberd commands.
%% If a command is already registered, a warning is printed and the
%% old command is preserved.
+%% A registered command is not directly available to be called through
+%% ejabberd ReST API. It need to be exposed to be available through API.
register_commands(Commands) ->
lists:foreach(
fun(Command) ->
- % XXX check if command exists
- mnesia:dirty_write(Command)
- % ?DEBUG("This command is already defined:~n~p", [Command])
+ %% XXX check if command exists
+ mnesia:dirty_write(Command)
+ %% ?DEBUG("This command is already defined:~n~p", [Command])
end,
- Commands).
+ Commands),
+ ejabberd_access_permissions:invalidate(),
+ ok.
-spec unregister_commands([ejabberd_commands()]) -> ok.
@@ -304,7 +312,28 @@ unregister_commands(Commands) ->
fun(Command) ->
mnesia:dirty_delete_object(Command)
end,
- Commands).
+ Commands),
+ ejabberd_access_permissions:invalidate(),
+ ok.
+
+%% @doc Expose command through ejabberd ReST API.
+%% Pass a list of command names or policy to expose.
+-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}.
+
+expose_commands(Commands) ->
+ Names = lists:map(fun(#ejabberd_commands{name = Name}) ->
+ Name;
+ (Name) when is_atom(Name) ->
+ Name
+ end,
+ Commands),
+
+ case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of
+ {aborted, Reason} ->
+ {error, Reason};
+ {atomic, Result} ->
+ Result
+ end.
-spec list_commands() -> [{atom(), [aterm()], string()}].
@@ -319,8 +348,8 @@ list_commands() ->
list_commands(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
- args = Args,
- desc = Desc} <- Commands].
+ args = Args,
+ desc = Desc} <- Commands].
-spec list_commands_policy(integer()) ->
@@ -331,10 +360,10 @@ list_commands(Version) ->
list_commands_policy(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc, Policy} ||
- #ejabberd_commands{name = Name,
- args = Args,
- desc = Desc,
- policy = Policy} <- Commands].
+ #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc,
+ policy = Policy} <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
@@ -356,27 +385,33 @@ get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth, #{}),
#ejabberd_commands{args = Args,
result = Result,
- policy = Policy} =
- get_command_definition(Name, Version),
+ policy = Policy} =
+ get_command_definition(Name, Version),
case Policy of
- user when Admin;
- Auth == noauth ->
- {[{user, binary}, {server, binary} | Args], Result};
- _ ->
- {Args, Result}
+ user when Admin;
+ Auth == noauth ->
+ {[{user, binary}, {server, binary} | Args], Result};
+ _ ->
+ {Args, Result}
end.
--spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}.
+-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
%% @doc return command policy.
-get_command_policy(Name) ->
+get_command_policy_and_scope(Name) ->
case get_command_definition(Name) of
- #ejabberd_commands{policy = Policy} ->
- {ok, Policy};
+ #ejabberd_commands{policy = Policy} = Cmd ->
+ {ok, Policy, cmd_scope(Cmd)};
command_not_found ->
{error, command_not_found}
end.
+%% The oauth scopes for a command are the command name itself,
+%% also might include either 'ejabberd:user' or 'ejabberd:admin'
+cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) ->
+ [erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin].
+
+
-spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command.
@@ -388,46 +423,61 @@ get_command_definition(Name) ->
%% @doc Get the definition record of a command in a given API version.
get_command_definition(Name, Version) ->
case lists:reverse(
- lists:sort(
- mnesia:dirty_select(
- ejabberd_commands,
- ets:fun2ms(
- fun(#ejabberd_commands{name = N, version = V} = C)
- when N == Name, V =< Version ->
- {V, C}
- end)))) of
- [{_, Command} | _ ] -> Command;
- _E -> throw(unknown_command)
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = N, version = V} = C)
+ when N == Name, V =< Version ->
+ {V, C}
+ end)))) of
+ [{_, Command} | _ ] -> Command;
+ _E -> throw({error, unknown_command})
end.
+get_commands_definition() ->
+ get_commands_definition(?DEFAULT_VERSION).
+
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
% @doc Returns all commands for a given API version
get_commands_definition(Version) ->
L = lists:reverse(
- lists:sort(
- mnesia:dirty_select(
- ejabberd_commands,
- ets:fun2ms(
- fun(#ejabberd_commands{name = Name, version = V} = C)
- when V =< Version ->
- {Name, V, C}
- end)))),
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = Name, version = V} = C)
+ when V =< Version ->
+ {Name, V, C}
+ end)))),
F = fun({_Name, _V, Command}, []) ->
- [Command];
- ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
- Acc;
- ({_Name, _V, Command}, Acc) -> [Command | Acc]
- end,
+ [Command];
+ ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
+ Acc;
+ ({_Name, _V, Command}, Acc) -> [Command | Acc]
+ end,
lists:foldl(F, [], L).
+execute_command2(Name, Arguments, CallerInfo) ->
+ execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION).
+
+execute_command2(Name, Arguments, CallerInfo, Version) ->
+ Command = get_command_definition(Name, Version),
+ case ejabberd_access_permissions:can_access(Name, CallerInfo) of
+ allow ->
+ do_execute_command(Command, Arguments);
+ _ ->
+ throw({error, access_rules_unauthorized})
+ end.
+
%% @spec (Name::atom(), Arguments) -> ResultTerm
%% where
%% Arguments = [any()]
%% @doc Execute a command.
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
-%% no_auth_provided
+%% no_auth_provided | access_rules_unauthorized
execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION).
@@ -488,41 +538,64 @@ execute_command(AccessCommands, Auth, Name, Arguments) ->
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
-%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
+%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
-execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
+ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
Auth = case is_admin(Name, Auth1, CallerInfo) of
true -> admin;
false -> Auth1
end,
+ TokenJID = oauth_token_user(Auth1),
Command = get_command_definition(Name, Version),
- AccessCommands = get_access_commands(AccessCommands1, Version),
+ AccessCommands = get_all_access_commands(AccessCommands1),
+
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
- ok -> execute_command2(Auth, Command, Arguments)
+ ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
+ end.
+
+
+execute_check_policy(
+ _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ noauth, _JID, Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ _Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) ->
+ execute_check_access(JID, Command, Arguments);
+execute_check_policy(
+ admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ execute_check_access(JID, Command, Arguments);
+execute_check_policy(
+ {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ execute_check_access(JID, Command, [User, Server | Arguments]).
+
+execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_access(undefined, _Command, _Arguments) ->
+ throw({error, access_rules_unauthorized});
+execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) ->
+ %% TODO Review: Do we have smarter / better way to check rule on other Host than global ?
+ Host = global,
+ Rules = lists:map(fun({Mod, AccessName, Default}) ->
+ gen_mod:get_module_opt(Host, Mod,
+ AccessName, fun(A) -> A end, Default);
+ (Default) ->
+ Default
+ end, AccessRefs),
+ case acl:any_rules_allowed(Host, Rules, FromJID) of
+ true ->
+ do_execute_command(Command, Arguments);
+ false ->
+ throw({error, access_rules_unauthorized})
end.
-execute_command2(
- _Auth, #ejabberd_commands{policy = open} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- _Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- _Auth, #ejabberd_commands{policy = admin} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- admin, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- noauth, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- {User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, [User, Server | Arguments]).
-
-execute_command2(Command, Arguments) ->
+do_execute_command(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
@@ -592,31 +665,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
Command1
end,
AccessCommandsAllowed =
- lists:filter(
- fun({Access, Commands, ArgumentRestrictions}) ->
- case check_access(Command, Access, Auth, CallerInfo) of
- true ->
- check_access_command(Commands, Command,
- ArgumentRestrictions,
- Method, Arguments);
- false ->
- false
- end;
- ({Access, Commands}) ->
- ArgumentRestrictions = [],
- case check_access(Command, Access, Auth, CallerInfo) of
- true ->
- check_access_command(Commands, Command,
- ArgumentRestrictions,
- Method, Arguments);
- false ->
- false
- end
- end,
- AccessCommands),
+ lists:filter(
+ fun({Access, Commands, ArgumentRestrictions}) ->
+ case check_access(Command, Access, Auth, CallerInfo) of
+ true ->
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
+ Method, Arguments);
+ false ->
+ false
+ end;
+ ({Access, Commands}) ->
+ ArgumentRestrictions = [],
+ case check_access(Command, Access, Auth, CallerInfo) of
+ true ->
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
+ Method, Arguments);
+ false ->
+ false
+ end
+ end,
+ AccessCommands),
case AccessCommandsAllowed of
- [] -> throw({error, account_unprivileged});
- L when is_list(L) -> ok
+ [] -> throw({error, account_unprivileged});
+ L when is_list(L) -> ok
end.
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
@@ -627,11 +700,11 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
check_auth(_Command, noauth) ->
no_auth_provided;
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
- Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
- case ejabberd_oauth:check_token(User, Server, Scope, Token) of
+ ScopeList = cmd_scope(Command),
+ case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
true ->
{ok, User, Server};
- false ->
+ _ ->
throw({error, invalid_account_data})
end;
check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) ->
@@ -680,9 +753,9 @@ check_access2(Access, AccessInfo, Server) ->
check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments) ->
case Commands==all orelse lists:member(Method, Commands) of
- true -> check_access_arguments(Command, ArgumentRestrictions,
- Arguments);
- false -> false
+ true -> check_access_arguments(Command, ArgumentRestrictions,
+ Arguments);
+ false -> false
end.
check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
@@ -705,19 +778,23 @@ tag_arguments(ArgsDefs, Args) ->
Args).
+%% Get commands for all version
+get_all_access_commands(AccessCommands) ->
+ get_access_commands(AccessCommands, ?DEFAULT_VERSION).
+
get_access_commands(undefined, Version) ->
- Cmds = get_commands(Version),
+ Cmds = get_exposed_commands(Version),
[{?POLICY_ACCESS, Cmds, []}];
get_access_commands(AccessCommands, _Version) ->
AccessCommands.
-get_commands() ->
- get_commands(?DEFAULT_VERSION).
-get_commands(Version) ->
+get_exposed_commands() ->
+ get_exposed_commands(?DEFAULT_VERSION).
+get_exposed_commands(Version) ->
Opts0 = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
- []),
+ []),
Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0),
CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList],
@@ -727,31 +804,38 @@ get_commands(Version) ->
Cmds =
lists:foldl(
fun([{add_commands, L}], Acc) ->
- Cmds = case L of
- open -> OpenCmds;
- restricted -> RestrictedCmds;
- admin -> AdminCmds;
- user -> UserCmds;
- _ when is_list(L) -> L
- end,
+ Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
lists:usort(Cmds ++ Acc);
([{remove_commands, L}], Acc) ->
- Cmds = case L of
- open -> OpenCmds;
- restricted -> RestrictedCmds;
- admin -> AdminCmds;
- user -> UserCmds;
- _ when is_list(L) -> L
- end,
+ Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
Acc -- Cmds;
(_, Acc) -> Acc
- end, AdminCmds ++ UserCmds, Opts),
+ end, [], Opts),
Cmds.
+%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry
+expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) ->
+ lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc;
+ (user, Acc) -> UserCmds ++ Acc;
+ (admin, Acc) -> AdminCmds ++ Acc;
+ (restricted, Acc) -> RestrictedCmds ++ Acc;
+ (Command, Acc) when is_atom(Command) ->
+ [Command|Acc]
+ end, [], L).
+
+oauth_token_user(noauth) ->
+ undefined;
+oauth_token_user(admin) ->
+ undefined;
+oauth_token_user({User, Server, _, _}) ->
+ jid:make(User, Server, <<>>).
+
is_admin(_Name, admin, _Extra) ->
true;
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
false;
+is_admin(_Name, Map, _extra) when is_map(Map) ->
+ true;
is_admin(Name, Auth, Extra) ->
{ACLInfo, Server} = case Auth of
{U, S, _, _} ->
@@ -773,6 +857,14 @@ is_admin(Name, Auth, Extra) ->
deny -> false
end.
+permission_addon() ->
+ [{<<"'commands' option compatibility shim">>,
+ {[],
+ [{access, ejabberd_config:get_option(commands_admin_access,
+ fun(V) -> V end,
+ none)}],
+ {get_exposed_commands(), []}}}].
+
opt_type(commands_admin_access) -> fun acl:access_rules_validator/1;
opt_type(commands) ->
fun(V) when is_list(V) -> V end;
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index d82e32b9a..af26767f8 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -29,14 +29,16 @@
-export([start/0, load_file/1, reload_file/0, read_file/1,
add_global_option/2, add_local_option/2,
get_global_option/2, get_local_option/2,
- get_global_option/3, get_local_option/3,
- get_option/2, get_option/3, add_option/2, has_option/1,
- get_vh_by_auth_method/1, is_file_readable/1,
- get_version/0, get_myhosts/0, get_mylang/0,
- prepare_opt_val/4, convert_table_to_binary/5,
- transform_options/1, collect_options/1, default_db/2,
- convert_to_yaml/1, convert_to_yaml/2, v_db/2,
- env_binary_to_list/2, opt_type/1, may_hide_data/1]).
+ get_global_option/3, get_local_option/3,
+ get_option/2, get_option/3, add_option/2, has_option/1,
+ get_vh_by_auth_method/1, is_file_readable/1,
+ get_version/0, get_myhosts/0, get_mylang/0,
+ get_ejabberd_config_path/0, is_using_elixir_config/0,
+ prepare_opt_val/4, convert_table_to_binary/5,
+ transform_options/1, collect_options/1, default_db/2,
+ convert_to_yaml/1, convert_to_yaml/2, v_db/2,
+ env_binary_to_list/2, opt_type/1, may_hide_data/1,
+ is_elixir_enabled/0, v_dbs/1, v_dbs_mods/1]).
-export([start/2]).
@@ -147,7 +149,18 @@ read_file(File) ->
{include_modules_configs, true}]).
read_file(File, Opts) ->
- Terms1 = get_plain_terms_file(File, Opts),
+ Terms1 = case is_elixir_enabled() of
+ true ->
+ case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of
+ true ->
+ 'Elixir.Ejabberd.Config':init(File),
+ 'Elixir.Ejabberd.Config':get_ejabberd_opts();
+ false ->
+ get_plain_terms_file(File, Opts)
+ end;
+ false ->
+ get_plain_terms_file(File, Opts)
+ end,
Terms_macros = case proplists:get_bool(replace_macros, Opts) of
true -> replace_macros(Terms1);
false -> Terms1
@@ -165,7 +178,8 @@ read_file(File, Opts) ->
-spec load_file(string()) -> ok.
load_file(File) ->
- State = read_file(File),
+ State0 = read_file(File),
+ State = validate_opts(State0),
set_opts(State).
-spec reload_file() -> ok.
@@ -318,7 +332,9 @@ get_absolute_path(File) ->
File;
relative ->
{ok, Dir} = file:get_cwd(),
- filename:absname_join(Dir, File)
+ filename:absname_join(Dir, File);
+ volumerelative ->
+ filename:absname(File)
end.
@@ -877,7 +893,20 @@ v_db(Mod, Type) ->
[] -> erlang:error(badarg)
end.
--spec default_db(global | binary(), module()) -> atom().
+-spec v_dbs(module()) -> [atom()].
+
+v_dbs(Mod) ->
+ lists:flatten(ets:match(module_db, {Mod, '$1'})).
+
+-spec v_dbs_mods(module()) -> [module()].
+
+v_dbs_mods(Mod) ->
+ lists:map(fun([M]) ->
+ binary_to_atom(<<(atom_to_binary(Mod, utf8))/binary, "_",
+ (atom_to_binary(M, utf8))/binary>>, utf8)
+ end, ets:match(module_db, {Mod, '$1'})).
+
+-spec default_db(binary(), module()) -> atom().
default_db(Host, Module) ->
case ejabberd_config:get_option(
@@ -1010,7 +1039,6 @@ replace_module(mod_private_odbc) -> {mod_private, sql};
replace_module(mod_roster_odbc) -> {mod_roster, sql};
replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, sql};
replace_module(mod_vcard_odbc) -> {mod_vcard, sql};
-replace_module(mod_vcard_ldap) -> {mod_vcard, ldap};
replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, sql};
replace_module(mod_pubsub_odbc) -> {mod_pubsub, sql};
replace_module(Module) ->
@@ -1041,6 +1069,23 @@ replace_modules(Modules) ->
%% Elixir module naming
%% ====================
+-ifdef(ELIXIR_ENABLED).
+is_elixir_enabled() ->
+ true.
+-else.
+is_elixir_enabled() ->
+ false.
+-endif.
+
+is_using_elixir_config() ->
+ case is_elixir_enabled() of
+ true ->
+ Config = get_ejabberd_config_path(),
+ 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config);
+ false ->
+ false
+ end.
+
%% If module name start with uppercase letter, this is an Elixir module:
is_elixir_module(Module) ->
case atom_to_list(Module) of
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index d52d1c0a9..a96a28016 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -212,7 +212,7 @@ process(["help" | Mode], Version) ->
end;
process(["--version", Arg | Args], _) ->
- Version =
+ Version =
try
list_to_integer(Arg)
catch _:_ ->
@@ -321,10 +321,15 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
{ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) ->
- Result = ejabberd_commands:execute_command(AccessCommands,
- Auth, Command,
- ArgsFormatted,
- Version),
+ CI = case Auth of
+ {U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S};
+ _ -> #{}
+ end,
+ CI2 = CI#{caller_module => ?MODULE},
+ Result = ejabberd_commands:execute_command2(Command,
+ ArgsFormatted,
+ CI2,
+ Version),
format_result(Result, ResultFormat);
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
{NumCompa, TextCompa} =
@@ -374,6 +379,12 @@ format_arg2(Arg, Parse)->
format_result({error, ErrorAtom}, _) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
+%% An error should always be allowed to return extended error to help with API.
+%% Extended error is of the form:
+%% {error, type :: atom(), code :: int(), Desc :: string()}
+format_result({error, ErrorAtom, Code, _Msg}, _) ->
+ {io_lib:format("Error: ~p", [ErrorAtom]), make_status(Code)};
+
format_result(Atom, {_Name, atom}) ->
io_lib:format("~p", [Atom]);
@@ -433,6 +444,8 @@ format_result(404, {_Name, _}) ->
make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS;
+make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR;
+make_status(Code) when is_integer(Code), Code > 0 -> Code;
make_status(_Error) -> ?STATUS_ERROR.
get_list_commands(Version) ->
diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl
index 35679ccd3..c6c31a971 100644
--- a/src/ejabberd_http.erl
+++ b/src/ejabberd_http.erl
@@ -145,9 +145,14 @@ init({SockMod, Socket}, Opts) ->
DefinedHandlers = gen_mod:get_opt(
request_handlers, Opts,
fun(Hs) ->
+ Hs1 = lists:map(fun
+ ({Mod, Path}) when is_atom(Mod) -> {Path, Mod};
+ ({Path, Mod}) -> {Path, Mod}
+ end, Hs),
+
[{str:tokens(
iolist_to_binary(Path), <<"/">>),
- Mod} || {Path, Mod} <- Hs]
+ Mod} || {Path, Mod} <- Hs1]
end, []),
RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++
Admin ++ Bind ++ XMLRPC,
@@ -391,7 +396,9 @@ extract_path_query(#state{request_method = Method,
socket = _Socket} = State)
when (Method =:= 'POST' orelse Method =:= 'PUT') andalso
is_integer(Len) ->
- {NewState, Data} = recv_data(State, Len),
+ case recv_data(State, Len) of
+ error -> {State, false};
+ {NewState, Data} ->
?DEBUG("client data: ~p~n", [Data]),
case catch url_decode_q_split(Path) of
{'EXIT', _} -> {NewState, false};
@@ -403,6 +410,7 @@ extract_path_query(#state{request_method = Method,
LQ -> LQ
end,
{NewState, {LPath, LQuery, Data}}
+ end
end;
extract_path_query(State) ->
{State, false}.
@@ -520,7 +528,7 @@ recv_data(State, Len, Acc) ->
recv_data(State, Len - byte_size(Data), <<Acc/binary, Data/binary>>);
Err ->
?DEBUG("Cannot receive HTTP data: ~p", [Err]),
- <<"">>
+ error
end;
_ ->
Trail = (State#state.trail),
@@ -763,7 +771,8 @@ parse_auth(<<"Basic ", Auth64/binary>>) ->
undefined;
Pos ->
{User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1),
- {User, Pass}
+ PassUtf8 = unicode:characters_to_binary(binary_to_list(Pass), utf8),
+ {User, PassUtf8}
end;
parse_auth(<<"Bearer ", SToken/binary>>) ->
Token = str:strip(SToken),
diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl
index 20edaa178..db529e69e 100644
--- a/src/ejabberd_http_bind.erl
+++ b/src/ejabberd_http_bind.erl
@@ -336,8 +336,9 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
init([Sid, Key, IP, HOpts]) ->
?DEBUG("started: ~p", [{Sid, Key, IP}]),
Opts1 = ejabberd_c2s_config:get_c2s_limits(),
- SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
+ SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
+ ({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl
index 02df19e63..b92345dd4 100644
--- a/src/ejabberd_http_ws.erl
+++ b/src/ejabberd_http_ws.erl
@@ -112,8 +112,9 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) ->
%%% Internal
init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
- SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
+ SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
+ ({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl
index c2bf453a5..210575e5e 100644
--- a/src/ejabberd_local.erl
+++ b/src/ejabberd_local.erl
@@ -184,6 +184,8 @@ refresh_iq_handlers() ->
ejabberd_local ! refresh_iq_handlers.
-spec bounce_resource_packet(jid(), jid(), stanza()) -> ok.
+bounce_resource_packet(_From, _To, #presence{}) ->
+ ok;
bounce_resource_packet(From, To, Packet) ->
Lang = xmpp:get_lang(Packet),
Txt = <<"No available resource found">>,
@@ -282,25 +284,16 @@ do_route(From, To, Packet) ->
?DEBUG("local route~n\tfrom ~p~n\tto ~p~n\tpacket "
"~P~n",
[From, To, Packet, 8]),
+ Type = xmpp:get_type(Packet),
if To#jid.luser /= <<"">> ->
ejabberd_sm:route(From, To, Packet);
- To#jid.lresource == <<"">> ->
- case Packet of
- #iq{} ->
- process_iq(From, To, Packet);
- #message{type = T} when T /= headline, T /= error ->
- Err = xmpp:make_error(Packet, xmpp:err_service_unavailable()),
- ejabberd_router:route(To, From, Err);
- _ -> ok
- end;
+ is_record(Packet, iq), To#jid.lresource == <<"">> ->
+ process_iq(From, To, Packet);
+ Type == result; Type == error; Type == headline ->
+ ok;
true ->
- case xmpp:get_type(Packet) of
- error -> ok;
- result -> ok;
- _ ->
- ejabberd_hooks:run(local_send_to_resource_hook,
- To#jid.lserver, [From, To, Packet])
- end
+ ejabberd_hooks:run(local_send_to_resource_hook,
+ To#jid.lserver, [From, To, Packet])
end.
-spec update_table() -> ok.
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 4e9dc8d08..318feb3f8 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -39,16 +39,17 @@
authenticate_user/2,
authenticate_client/2,
verify_resowner_scope/3,
- verify_client_scope/3,
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
+ check_token/1,
check_token/4,
check_token/2,
+ scope_in_scope_list/2,
process/2,
opt_type/1]).
--export([oauth_issue_token/1, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
+-export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
-include("xmpp.hrl").
@@ -57,6 +58,7 @@
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
+-include("ejabberd_oauth.hrl").
-include("ejabberd_commands.hrl").
@@ -65,23 +67,30 @@
%% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
%% (as it has access to ejabberd command line).
--record(oauth_token, {
- token = {<<"">>, <<"">>} :: {binary(), binary()},
- us = {<<"">>, <<"">>} :: {binary(), binary()} | server_admin,
- scope = [] :: [binary()],
- expire :: integer()
- }).
--define(EXPIRE, 3600).
+-define(EXPIRE, 4294967).
start() ->
- init_db(mnesia, ?MYNAME),
+ DBMod = get_db_backend(),
+ DBMod:init(),
+ MaxSize =
+ ejabberd_config:get_option(
+ oauth_cache_size,
+ fun(I) when is_integer(I), I>0 -> I end,
+ 1000),
+ LifeTime =
+ ejabberd_config:get_option(
+ oauth_cache_life_time,
+ fun(I) when is_integer(I), I>0 -> I end,
+ timer:hours(1) div 1000),
+ cache_tab:new(oauth_token,
+ [{max_size, MaxSize}, {life_time, LifeTime}]),
Expire = expire(),
application:set_env(oauth2, backend, ejabberd_oauth),
application:set_env(oauth2, expiry_time, Expire),
application:start(oauth2),
ChildSpec = {?MODULE, {?MODULE, start_link, []},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec),
ejabberd_commands:register_commands(get_commands_spec()),
ok.
@@ -90,57 +99,63 @@ start() ->
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
- desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins",
+ desc = "Issue an oauth token for the given jid",
module = ?MODULE, function = oauth_issue_token,
- args = [{scopes, string}],
+ args = [{jid, string},{ttl, integer}, {scopes, string}],
policy = restricted,
- args_example = ["connected_users_number;muc_online_rooms"],
- args_desc = ["List of scopes to allow, separated by ';'"],
+ args_example = ["user@server.com", "connected_users_number;muc_online_rooms"],
+ args_desc = ["Jid for which issue token",
+ "Time to live of generated token in seconds",
+ "List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
- desc = "List oauth tokens, their scope, and how many seconds remain until expirity",
+ desc = "List oauth tokens, their user and scope, and how many seconds remain until expirity",
module = ?MODULE, function = oauth_list_tokens,
args = [],
policy = restricted,
- result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}}
+ result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_list_scopes, tags = [oauth],
- desc = "List scopes that can be granted to tokens generated through the command line",
+ desc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow",
module = ?MODULE, function = oauth_list_scopes,
args = [],
policy = restricted,
- result = {scopes, {list, {scope, string}}}
+ result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
desc = "Revoke authorization for a token",
module = ?MODULE, function = oauth_revoke_token,
args = [{token, string}],
policy = restricted,
- result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}},
+ result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}},
result_desc = "List of remaining tokens"
}
].
-oauth_issue_token(ScopesString) ->
+oauth_issue_token(Jid, TTLSeconds, ScopesString) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
- case oauth2:authorize_client_credentials(ejabberd_ctl, Scopes, none) of
- {ok, {_AppCtx, Authorization}} ->
- {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, none),
+ case jid:from_string(list_to_binary(Jid)) of
+ #jid{luser =Username, lserver = Server} ->
+ case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of
+ {ok, {_Ctx,Authorization}} ->
+ {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]),
{ok, AccessToken} = oauth2_response:access_token(Response),
- {ok, Expires} = oauth2_response:expires_in(Response),
{ok, VerifiedScope} = oauth2_response:scope(Response),
- {AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"};
+ {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"};
{error, Error} ->
{error, Error}
+ end;
+ error ->
+ {error, "Invalid JID: " ++ Jid}
end.
oauth_list_tokens() ->
- Tokens = mnesia:dirty_match_object(#oauth_token{us = server_admin, _ = '_'}),
+ Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}),
{MegaSecs, Secs, _MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- [{Token, Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
- #oauth_token{token=Token, scope=Scope, expire=Expires} <- Tokens].
+ [{Token, jid:to_string(jid:make(U,S,<<>>)), Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
+ #oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens].
oauth_revoke_token(Token) ->
@@ -148,8 +163,7 @@ oauth_revoke_token(Token) ->
oauth_list_tokens().
oauth_list_scopes() ->
- get_cmd_scopes().
-
+ [ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")} || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())].
@@ -170,15 +184,8 @@ handle_cast(_Msg, State) -> {noreply, State}.
handle_info(clean, State) ->
{MegaSecs, Secs, MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- F = fun() ->
- Ts = mnesia:select(
- oauth_token,
- [{#oauth_token{expire = '$1', _ = '_'},
- [{'<', '$1', TS}],
- ['$_']}]),
- lists:foreach(fun mnesia:delete_object/1, Ts)
- end,
- mnesia:async_dirty(F),
+ DBMod = get_db_backend(),
+ DBMod:clean(TS),
erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)),
self(), clean),
{noreply, State};
@@ -189,21 +196,11 @@ terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
-init_db(mnesia, _Host) ->
- mnesia:create_table(oauth_token,
- [{disc_copies, [node()]},
- {attributes,
- record_info(fields, oauth_token)}]),
- mnesia:add_table_copy(oauth_token, node(), disc_copies);
-init_db(_, _) ->
- ok.
-
-
get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}.
-authenticate_user({User, Server}, {password, Password} = Ctx) ->
+authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server, <<"">>) of
#jid{} = JID ->
Access =
@@ -213,12 +210,17 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
none),
case acl:match_rule(JID#jid.lserver, Access, JID) of
allow ->
+ case Ctx of
+ {password, Password} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
true ->
{ok, {Ctx, {user, User, Server}}};
false ->
{error, badpass}
end;
+ admin_generated ->
+ {ok, {Ctx, {user, User, Server}}}
+ end;
deny ->
{error, badpass}
end;
@@ -229,8 +231,8 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
- Cmds = ejabberd_commands:get_commands(),
- Cmds1 = [sasl_auth | Cmds],
+ Cmds = ejabberd_commands:get_exposed_commands(),
+ Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds],
RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1],
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(RegisteredScope)) of
@@ -244,93 +246,156 @@ verify_resowner_scope(_, _, _) ->
get_cmd_scopes() ->
- Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of
- {ok, Policy} when Policy =/= restricted -> true;
- _ -> false
- end end,
- ejabberd_commands:get_commands()),
- [atom_to_binary(C, utf8) || C <- Cmds].
+ ScopeMap = lists:foldl(fun(Cmd, Accum) ->
+ case ejabberd_commands:get_command_policy_and_scope(Cmd) of
+ {ok, Policy, Scopes} when Policy =/= restricted ->
+ lists:foldl(fun(Scope, Accum2) ->
+ dict:append(Scope, Cmd, Accum2)
+ end, Accum, Scopes);
+ _ -> Accum
+ end end, dict:new(), ejabberd_commands:get_exposed_commands()),
+ ScopeMap.
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
-verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
- RegisteredScope = get_cmd_scopes(),
- case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
- oauth2_priv_set:new(RegisteredScope)) of
- true ->
- {ok, {Ctx, Scope}};
- false ->
- {error, badscope}
- end.
+%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
+% RegisteredScope = dict:fetch_keys(get_cmd_scopes()),
+% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
+% oauth2_priv_set:new(RegisteredScope)) of
+% true ->
+% {ok, {Ctx, Scope}};
+% false ->
+% {error, badscope}
+% end.
+-spec seconds_since_epoch(integer()) -> non_neg_integer().
+seconds_since_epoch(Diff) ->
+ {Mega, Secs, _} = os:timestamp(),
+ Mega * 1000000 + Secs + Diff.
+
associate_access_code(_AccessCode, _Context, AppContext) ->
%put(?ACCESS_CODE_TABLE, AccessCode, Context),
{ok, AppContext}.
associate_access_token(AccessToken, Context, AppContext) ->
- %% Tokens generated using the API/WEB belongs to users and always include the user, server pair.
- %% Tokens generated form command line aren't tied to an user, and instead belongs to the ejabberd sysadmin
- US = case proplists:get_value(<<"resource_owner">>, Context, <<"">>) of
- {user, User, Server} -> {jid:nodeprep(User), jid:nodeprep(Server)};
- undefined -> server_admin
+ {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
+ Expire = case proplists:get_value(expiry_time, AppContext, undefined) of
+ undefined ->
+ proplists:get_value(<<"expiry_time">>, Context, 0);
+ ExpiresIn ->
+ %% There is no clean way in oauth2 lib to actually override the TTL of the generated token.
+ %% It always pass the global configured value. Here we use the app context to pass the per-case
+ %% ttl if we want to override it.
+ seconds_since_epoch(ExpiresIn)
end,
+ {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Scope = proplists:get_value(<<"scope">>, Context, []),
- Expire = proplists:get_value(<<"expiry_time">>, Context, 0),
R = #oauth_token{
token = AccessToken,
- us = US,
+ us = {jid:nodeprep(User), jid:nodeprep(Server)},
scope = Scope,
expire = Expire
},
- mnesia:dirty_write(R),
+ store(R),
{ok, AppContext}.
associate_refresh_token(_RefreshToken, _Context, AppContext) ->
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
{ok, AppContext}.
+scope_in_scope_list(Scope, ScopeList) ->
+ TokenScopeSet = oauth2_priv_set:new(Scope),
+ lists:any(fun(Scope2) ->
+ oauth2_priv_set:is_member(Scope2, TokenScopeSet) end,
+ ScopeList).
+
+check_token(Token) ->
+ case lookup(Token) of
+ {ok, #oauth_token{us = US,
+ scope = TokenScope,
+ expire = Expire}} ->
+ {MegaSecs, Secs, _} = os:timestamp(),
+ TS = 1000000 * MegaSecs + Secs,
+ if
+ Expire > TS ->
+ {ok, US, TokenScope};
+ true ->
+ {false, expired}
+ end;
+ _ ->
+ {false, not_found}
+ end.
-check_token(User, Server, Scope, Token) ->
+check_token(User, Server, ScopeList, Token) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case catch mnesia:dirty_read(oauth_token, Token) of
- [#oauth_token{us = {LUser, LServer},
+ case lookup(Token) of
+ {ok, #oauth_token{us = {LUser, LServer},
scope = TokenScope,
- expire = Expire}] ->
+ expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- oauth2_priv_set:is_member(
- Scope, oauth2_priv_set:new(TokenScope)) andalso
- Expire > TS;
+ if
+ Expire > TS ->
+ TokenScopeSet = oauth2_priv_set:new(TokenScope),
+ lists:any(fun(Scope) ->
+ oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+ ScopeList);
+ true ->
+ {false, expired}
+ end;
_ ->
- false
+ {false, not_found}
end.
-check_token(Scope, Token) ->
- case catch mnesia:dirty_read(oauth_token, Token) of
- [#oauth_token{us = US,
+check_token(ScopeList, Token) ->
+ case lookup(Token) of
+ {ok, #oauth_token{us = US,
scope = TokenScope,
- expire = Expire}] ->
+ expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- case oauth2_priv_set:is_member(
- Scope, oauth2_priv_set:new(TokenScope)) andalso
- Expire > TS of
- true -> case US of
- {LUser, LServer} -> {ok, user, {LUser, LServer}};
- server_admin -> {ok, server_admin}
+ if
+ Expire > TS ->
+ TokenScopeSet = oauth2_priv_set:new(TokenScope),
+ case lists:any(fun(Scope) ->
+ oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+ ScopeList) of
+ true -> {ok, user, US};
+ false -> {false, no_matching_scope}
end;
- false -> false
+ true ->
+ {false, expired}
end;
_ ->
- false
+ {false, not_found}
end.
+store(R) ->
+ cache_tab:insert(
+ oauth_token, R#oauth_token.token, R,
+ fun() ->
+ DBMod = get_db_backend(),
+ DBMod:store(R)
+ end).
+
+lookup(Token) ->
+ cache_tab:lookup(
+ oauth_token, Token,
+ fun() ->
+ DBMod = get_db_backend(),
+ case DBMod:lookup(Token) of
+ #oauth_token{} = R -> {ok, R};
+ _ -> error
+ end
+ end).
+
+
expire() ->
ejabberd_config:get_option(
oauth_expire,
@@ -358,12 +423,9 @@ process(_Handlers,
?XAE(<<"form">>,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
- [?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]),
+ [?LABEL(<<"username">>, [?CT(<<"User (jid)">>), ?C(<<": ">>)]),
?INPUTID(<<"text">>, <<"username">>, <<"">>),
?BR,
- ?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]),
- ?INPUTID(<<"text">>, <<"server">>, <<"">>),
- ?BR,
?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
?INPUT(<<"hidden">>, <<"response_type">>, ResponseType),
@@ -372,6 +434,15 @@ process(_Handlers,
?INPUT(<<"hidden">>, <<"scope">>, Scope),
?INPUT(<<"hidden">>, <<"state">>, State),
?BR,
+ ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
+ ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
+ [
+ ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
+ ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
+ ?BR,
?INPUTT(<<"submit">>, <<"">>, <<"Accept">>)
]),
Top =
@@ -415,11 +486,16 @@ process(_Handlers,
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
- Username = proplists:get_value(<<"username">>, Q, <<"">>),
- Server = proplists:get_value(<<"server">>, Q, <<"">>),
+ StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
+ #jid{user = Username, server = Server} = jid:from_string(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
State = proplists:get_value(<<"state">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
+ TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
+ ExpiresIn = case TTL of
+ <<>> -> undefined;
+ _ -> jlib:binary_to_integer(TTL)
+ end,
case oauth2:authorize_password({Username, Server},
ClientId,
RedirectURI,
@@ -427,10 +503,18 @@ process(_Handlers,
{password, Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
- oauth2:issue_token(Authorization, none),
+ oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
- {ok, Expires} = oauth2_response:expires_in(Response),
+ %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
+ %%per-case expirity time.
+ Expires = case ExpiresIn of
+ undefined ->
+ {ok, Ex} = oauth2_response:expires_in(Response),
+ Ex;
+ _ ->
+ ExpiresIn
+ end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
%oauth2_wrq:redirected_access_token_response(ReqData,
% RedirectURI,
@@ -459,11 +543,82 @@ process(_Handlers,
}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end;
+process(_Handlers,
+ #request{method = 'POST', q = Q, lang = _Lang,
+ path = [_, <<"token">>]}) ->
+ case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
+ <<"password">> ->
+ SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
+ StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
+ #jid{user = Username, server = Server} = jid:from_string(StringJID),
+ Password = proplists:get_value(<<"password">>, Q, <<"">>),
+ Scope = str:tokens(SScope, <<" ">>),
+ TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
+ ExpiresIn = case TTL of
+ <<>> -> undefined;
+ _ -> jlib:binary_to_integer(TTL)
+ end,
+ case oauth2:authorize_password({Username, Server},
+ Scope,
+ {password, Password}) of
+ {ok, {_AppContext, Authorization}} ->
+ {ok, {_AppContext2, Response}} =
+ oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
+ {ok, AccessToken} = oauth2_response:access_token(Response),
+ {ok, Type} = oauth2_response:token_type(Response),
+ %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
+ %%per-case expirity time.
+ Expires = case ExpiresIn of
+ undefined ->
+ {ok, Ex} = oauth2_response:expires_in(Response),
+ Ex;
+ _ ->
+ ExpiresIn
+ end,
+ {ok, VerifiedScope} = oauth2_response:scope(Response),
+ json_response(200, {[
+ {<<"access_token">>, AccessToken},
+ {<<"token_type">>, Type},
+ {<<"scope">>, str:join(VerifiedScope, <<" ">>)},
+ {<<"expires_in">>, Expires}]});
+ {error, Error} when is_atom(Error) ->
+ json_error(400, <<"invalid_grant">>, Error)
+ end;
+ _OtherGrantType ->
+ json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
+ end;
+
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
+-spec get_db_backend() -> module().
+
+get_db_backend() ->
+ DBType = ejabberd_config:get_option(
+ oauth_db_type,
+ fun(T) -> ejabberd_config:v_db(?MODULE, T) end,
+ mnesia),
+ list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
+
+
+%% Headers as per RFC 6749
+json_response(Code, Body) ->
+ {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
+ {<<"Cache-Control">>, <<"no-store">>},
+ {<<"Pragma">>, <<"no-cache">>}],
+ jiffy:encode(Body)}.
+%% OAauth error are defined in:
+%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2
+json_error(Code, Error, Reason) ->
+ Desc = json_error_desc(Reason),
+ Body = {[{<<"error">>, Error},
+ {<<"error_description">>, Desc}]},
+ json_response(Code, Body).
+json_error_desc(access_denied) -> <<"Access denied">>;
+json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
+json_error_desc(invalid_scope) -> <<"Invalid scope">>.
web_head() ->
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
@@ -595,4 +750,10 @@ opt_type(oauth_expire) ->
fun(I) when is_integer(I), I >= 0 -> I end;
opt_type(oauth_access) ->
fun acl:access_rules_validator/1;
-opt_type(_) -> [oauth_expire, oauth_access].
+opt_type(oauth_db_type) ->
+ fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
+opt_type(oauth_cache_life_time) ->
+ fun (I) when is_integer(I), I > 0 -> I end;
+opt_type(oauth_cache_size) ->
+ fun (I) when is_integer(I), I > 0 -> I end;
+opt_type(_) -> [oauth_expire, oauth_access, oauth_db_type].
diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl
new file mode 100644
index 000000000..a23f443ed
--- /dev/null
+++ b/src/ejabberd_oauth_mnesia.erl
@@ -0,0 +1,65 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_mnesia.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 mnesia backend
+%%% Created : 20 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_mnesia).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1]).
+
+-include("ejabberd_oauth.hrl").
+
+init() ->
+ mnesia:create_table(oauth_token,
+ [{disc_copies, [node()]},
+ {attributes,
+ record_info(fields, oauth_token)}]),
+ mnesia:add_table_copy(oauth_token, node(), disc_copies),
+ ok.
+
+store(R) ->
+ mnesia:dirty_write(R).
+
+lookup(Token) ->
+ case catch mnesia:dirty_read(oauth_token, Token) of
+ [R] ->
+ R;
+ _ ->
+ false
+ end.
+
+clean(TS) ->
+ F = fun() ->
+ Ts = mnesia:select(
+ oauth_token,
+ [{#oauth_token{expire = '$1', _ = '_'},
+ [{'<', '$1', TS}],
+ ['$_']}]),
+ lists:foreach(fun mnesia:delete_object/1, Ts)
+ end,
+ mnesia:async_dirty(F).
+
diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl
new file mode 100644
index 000000000..aadb97084
--- /dev/null
+++ b/src/ejabberd_oauth_rest.erl
@@ -0,0 +1,98 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_rest.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 REST backend
+%%% Created : 26 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_rest).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1,
+ opt_type/1]).
+
+-include("ejabberd.hrl").
+-include("ejabberd_oauth.hrl").
+-include("logger.hrl").
+-include("jlib.hrl").
+
+init() ->
+ rest:start(?MYNAME),
+ ok.
+
+store(R) ->
+ Path = path(<<"store">>),
+ %% Retry 2 times, with a backoff of 500millisec
+ {User, Server} = R#oauth_token.us,
+ SJID = jid:to_string({User, Server, <<"">>}),
+ case rest:with_retry(
+ post,
+ [?MYNAME, Path, [],
+ {[{<<"token">>, R#oauth_token.token},
+ {<<"user">>, SJID},
+ {<<"scope">>, R#oauth_token.scope},
+ {<<"expire">>, R#oauth_token.expire}
+ ]}], 2, 500) of
+ {ok, Code, _} when Code == 200 orelse Code == 201 ->
+ ok;
+ Err ->
+ ?ERROR_MSG("failed to store oauth record ~p: ~p", [R, Err]),
+ {error, Err}
+ end.
+
+lookup(Token) ->
+ Path = path(<<"lookup">>),
+ case rest:with_retry(post, [?MYNAME, Path, [],
+ {[{<<"token">>, Token}]}],
+ 2, 500) of
+ {ok, 200, {Data}} ->
+ SJID = proplists:get_value(<<"user">>, Data, <<>>),
+ JID = jid:from_string(SJID),
+ US = {JID#jid.luser, JID#jid.lserver},
+ Scope = proplists:get_value(<<"scope">>, Data, []),
+ Expire = proplists:get_value(<<"expire">>, Data, 0),
+ #oauth_token{token = Token,
+ us = US,
+ scope = Scope,
+ expire = Expire};
+ {ok, 404, _Resp} ->
+ false;
+ Other ->
+ ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]),
+ {error, rest_failed}
+ end.
+
+clean(_TS) ->
+ ok.
+
+path(Path) ->
+ Base = ejabberd_config:get_option(ext_api_path_oauth,
+ fun(X) -> iolist_to_binary(X) end,
+ <<"/oauth">>),
+ <<Base/binary, "/", Path/binary>>.
+
+
+opt_type(ext_api_path_oauth) ->
+ fun (X) -> iolist_to_binary(X) end;
+opt_type(_) -> [ext_api_path_oauth].
diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl
new file mode 100644
index 000000000..9253335ff
--- /dev/null
+++ b/src/ejabberd_oauth_sql.erl
@@ -0,0 +1,78 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_sql.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 SQL backend
+%%% Created : 27 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_sql).
+
+-compile([{parse_transform, ejabberd_sql_pt}]).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1]).
+
+-include("ejabberd_oauth.hrl").
+-include("ejabberd.hrl").
+-include("ejabberd_sql_pt.hrl").
+-include("jlib.hrl").
+
+init() ->
+ ok.
+
+store(R) ->
+ Token = R#oauth_token.token,
+ {User, Server} = R#oauth_token.us,
+ SJID = jid:to_string({User, Server, <<"">>}),
+ Scope = str:join(R#oauth_token.scope, <<" ">>),
+ Expire = R#oauth_token.expire,
+ ?SQL_UPSERT(
+ ?MYNAME,
+ "oauth_token",
+ ["!token=%(Token)s",
+ "jid=%(SJID)s",
+ "scope=%(Scope)s",
+ "expire=%(Expire)d"]).
+
+lookup(Token) ->
+ case ejabberd_sql:sql_query(
+ ?MYNAME,
+ ?SQL("select @(jid)s, @(scope)s, @(expire)d"
+ " from oauth_token where token=%(Token)s")) of
+ {selected, [{SJID, Scope, Expire}]} ->
+ JID = jid:from_string(SJID),
+ US = {JID#jid.luser, JID#jid.lserver},
+ #oauth_token{token = Token,
+ us = US,
+ scope = str:tokens(Scope, <<" ">>),
+ expire = Expire};
+ _ ->
+ false
+ end.
+
+clean(TS) ->
+ ejabberd_sql:sql_query(
+ ?MYNAME,
+ ?SQL("delete from oauth_token where expire < %(TS)d")).
+
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 3c3e698ad..97aef3cab 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -466,19 +466,17 @@ send_element(Pid, El) ->
%%% ejabberd commands
get_commands_spec() ->
- [#ejabberd_commands{name = incoming_s2s_number,
+ [#ejabberd_commands{
+ name = incoming_s2s_number,
tags = [stats, s2s],
- desc =
- "Number of incoming s2s connections on "
- "the node",
+ desc = "Number of incoming s2s connections on the node",
policy = admin,
module = ?MODULE, function = incoming_s2s_number,
args = [], result = {s2s_incoming, integer}},
- #ejabberd_commands{name = outgoing_s2s_number,
+ #ejabberd_commands{
+ name = outgoing_s2s_number,
tags = [stats, s2s],
- desc =
- "Number of outgoing s2s connections on "
- "the node",
+ desc = "Number of outgoing s2s connections on the node",
policy = admin,
module = ?MODULE, function = outgoing_s2s_number,
args = [], result = {s2s_outgoing, integer}},
@@ -489,11 +487,19 @@ get_commands_spec() ->
module = ?MODULE, function = stop_all_connections,
args = [], result = {res, rescode}}].
+%% TODO Move those stats commands to ejabberd stats command ?
incoming_s2s_number() ->
- length(supervisor:which_children(ejabberd_s2s_in_sup)).
+ supervisor_count(ejabberd_s2s_in_sup).
outgoing_s2s_number() ->
- length(supervisor:which_children(ejabberd_s2s_out_sup)).
+ supervisor_count(ejabberd_s2s_out_sup).
+
+supervisor_count(Supervisor) ->
+ case catch supervisor:which_children(Supervisor) of
+ {'EXIT', _} -> 0;
+ Result ->
+ length(Result)
+ end.
stop_all_connections() ->
lists:foreach(
diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl
index 62c07b068..076ba2d3b 100644
--- a/src/ejabberd_s2s_out.erl
+++ b/src/ejabberd_s2s_out.erl
@@ -850,13 +850,12 @@ get_addr_port(Server) ->
?DEBUG("srv lookup of '~s': ~p~n",
[Server, HEnt#hostent.h_addr_list]),
AddrList = HEnt#hostent.h_addr_list,
- random:seed(p1_time_compat:timestamp()),
case catch lists:map(fun ({Priority, Weight, Port,
Host}) ->
N = case Weight of
0 -> 0;
_ ->
- (Weight + 1) * random:uniform()
+ (Weight + 1) * randoms:uniform()
end,
{Priority * 65536 - N, Host, Port}
end,
diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl
index b1a4b433e..94cd68ecf 100644
--- a/src/ejabberd_service.erl
+++ b/src/ejabberd_service.erl
@@ -98,13 +98,13 @@ init([{SockMod, Socket}, Opts]) ->
fun({H, Os}, D) ->
P = proplists:get_value(
password, Os,
- p1_sha:sha(crypto:rand_bytes(20))),
+ p1_sha:sha(randoms:bytes(20))),
dict:store(H, P, D)
end, dict:new(), HOpts);
false ->
Pass = proplists:get_value(
password, Opts,
- p1_sha:sha(crypto:rand_bytes(20))),
+ p1_sha:sha(randoms:bytes(20))),
dict:from_list([{global, Pass}])
end,
Shaper = case lists:keysearch(shaper_rule, 1, Opts) of
@@ -170,27 +170,37 @@ wait_for_stream(closed, StateData) ->
wait_for_handshake({xmlstreamelement, El}, StateData) ->
decode_element(El, wait_for_handshake, StateData);
wait_for_handshake(#handshake{data = Digest}, StateData) ->
- case dict:find(StateData#state.host, StateData#state.host_opts) of
- {ok, Password} ->
- case p1_sha:sha(<<(StateData#state.streamid)/binary,
- Password/binary>>) of
- Digest ->
- send_element(StateData, #handshake{}),
- lists:foreach(
- fun (H) ->
- ejabberd_router:register_route(H, ?MYNAME),
- ?INFO_MSG("Route registered for service ~p~n",
- [H])
- end, dict:fetch_keys(StateData#state.host_opts)),
- {next_state, stream_established, StateData};
- _ ->
- send_element(StateData, xmpp:serr_not_authorized()),
- {stop, normal, StateData}
- end;
- _ ->
- send_element(StateData, xmpp:serr_not_authorized()),
- {stop, normal, StateData}
- end;
+ send_element(StateData, #handshake{}),
+ lists:foreach(
+ fun (H) ->
+ ejabberd_router:register_route(H, ?MYNAME),
+ ?INFO_MSG("Route registered for service ~p~n",
+ [H]),
+ ejabberd_hooks:run(component_connected, [H])
+ end, dict:fetch_keys(StateData#state.host_opts)),
+ {next_state, stream_established, StateData};
+ %% case dict:find(StateData#state.host, StateData#state.host_opts) of
+ %% {ok, Password} ->
+ %% case p1_sha:sha(<<(StateData#state.streamid)/binary,
+ %% Password/binary>>) of
+ %% Digest ->
+ %% send_element(StateData, #handshake{}),
+ %% lists:foreach(
+ %% fun (H) ->
+ %% ejabberd_router:register_route(H, ?MYNAME),
+ %% ?INFO_MSG("Route registered for service ~p~n",
+ %% [H]),
+ %% ejabberd_hooks:run(component_connected, [H])
+ %% end, dict:fetch_keys(StateData#state.host_opts)),
+ %% {next_state, stream_established, StateData};
+ %% _ ->
+ %% send_element(StateData, xmpp:serr_not_authorized()),
+ %% {stop, normal, StateData}
+ %% end;
+ %% _ ->
+ %% send_element(StateData, xmpp:serr_not_authorized()),
+ %% {stop, normal, StateData}
+ %% end;
wait_for_handshake({xmlstreamend, _Name}, StateData) ->
{stop, normal, StateData};
wait_for_handshake({xmlstreamerror, _}, StateData) ->
@@ -211,24 +221,10 @@ stream_established(El, StateData) when ?is_stanza(El) ->
Txt = <<"Missing 'from' or 'to' attribute">>,
send_error(StateData, El, xmpp:err_jid_malformed(Txt, Lang));
true ->
- FromJID = case StateData#state.check_from of
- false ->
- %% If the admin does not want to check the from field
- %% when accept packets from any address.
- %% In this case, the component can send packet of
- %% behalf of the server users.
- From;
- _ ->
- %% The default is the standard behaviour in XEP-0114
- Server = From#jid.lserver,
- case dict:is_key(Server, StateData#state.host_opts) of
- true -> From;
- false -> error
- end
- end,
- if FromJID /= error ->
- ejabberd_router:route(FromJID, To, El);
- true ->
+ case check_from(From, StateData) of
+ true ->
+ ejabberd_router:route(From, To, El);
+ false ->
Txt = <<"Improper domain part of 'from' attribute">>,
send_error(StateData, El, xmpp:err_not_allowed(Txt, Lang))
end
@@ -281,7 +277,9 @@ terminate(Reason, StateName, StateData) ->
case StateName of
stream_established ->
lists:foreach(fun (H) ->
- ejabberd_router:unregister_route(H)
+ ejabberd_router:unregister_route(H),
+ ejabberd_hooks:run(component_disconnected,
+ [H, Reason])
end,
dict:fetch_keys(StateData#state.host_opts));
_ -> ok
@@ -350,6 +348,18 @@ decode_element(#xmlel{} = El, StateName, StateData) ->
{next_state, StateName, StateData}
end.
+-spec check_from(jid(), state()) -> boolean().
+check_from(_From, #state{check_from = false}) ->
+ %% If the admin does not want to check the from field
+ %% when accept packets from any address.
+ %% In this case, the component can send packet of
+ %% behalf of the server users.
+ true;
+check_from(From, StateData) ->
+ %% The default is the standard behaviour in XEP-0114
+ Server = From#jid.lserver,
+ dict:is_key(Server, StateData#state.host_opts).
+
-spec new_id() -> binary().
new_id() -> randoms:get_string().
diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl
index 0655bbcf3..18703dc9c 100644
--- a/src/ejabberd_sm.erl
+++ b/src/ejabberd_sm.erl
@@ -279,25 +279,28 @@ get_session_pid(User, Server, Resource) ->
-spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok.
-set_offline_info({Time, _Pid}, User, Server, Resource, Info) ->
- SID = {Time, undefined},
+set_offline_info(SID, User, Server, Resource, Info) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
- set_session(SID, LUser, LServer, LResource, undefined, Info).
+ set_session(SID, LUser, LServer, LResource, undefined, [offline | Info]).
-spec get_offline_info(erlang:timestamp(), binary(), binary(),
binary()) -> none | info().
get_offline_info(Time, User, Server, Resource) ->
- SID = {Time, undefined},
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
Mod = get_sm_backend(LServer),
case Mod:get_sessions(LUser, LServer, LResource) of
- [#session{sid = SID, info = Info}] ->
+ [#session{sid = {Time, _}, info = Info}] ->
+ case proplists:get_bool(offline, Info) of
+ true ->
Info;
+ false ->
+ none
+ end;
_ ->
none
end.
@@ -434,11 +437,12 @@ set_session(SID, User, Server, Resource, Priority, Info) ->
-spec online([#session{}]) -> [#session{}].
online(Sessions) ->
- lists:filter(fun(#session{sid = {_, undefined}}) ->
- false;
- (_) ->
- true
- end, Sessions).
+ lists:filter(fun is_online/1, Sessions).
+
+-spec is_online(#session{}) -> boolean().
+
+is_online(#session{info = Info}) ->
+ not proplists:get_bool(offline, Info).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec do_route(jid(), jid(), stanza() | broadcast()) -> any().
@@ -629,15 +633,17 @@ check_for_sessions_to_replace(User, Server, Resource) ->
-spec check_existing_resources(binary(), binary(), binary()) -> ok.
check_existing_resources(LUser, LServer, LResource) ->
- SIDs = get_resource_sessions(LUser, LServer, LResource),
- if SIDs == [] -> ok;
+ Mod = get_sm_backend(LServer),
+ Ss = Mod:get_sessions(LUser, LServer, LResource),
+ {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
+ lists:foreach(fun(#session{sid = S}) ->
+ Mod:delete_session(LUser, LServer, LResource, S)
+ end, OfflineSs),
+ if OnlineSs == [] -> ok;
true ->
+ SIDs = [SID || #session{sid = SID} <- OnlineSs],
MaxSID = lists:max(SIDs),
- lists:foreach(fun ({_, undefined} = S) ->
- Mod = get_sm_backend(LServer),
- Mod:delete_session(LUser, LServer, LResource,
- S);
- ({_, Pid} = S) when S /= MaxSID ->
+ lists:foreach(fun ({_, Pid} = S) when S /= MaxSID ->
Pid ! replaced;
(_) -> ok
end,
@@ -660,10 +666,18 @@ get_resource_sessions(User, Server, Resource) ->
-spec check_max_sessions(binary(), binary()) -> ok | replaced.
check_max_sessions(LUser, LServer) ->
Mod = get_sm_backend(LServer),
- SIDs = [S#session.sid || S <- online(Mod:get_sessions(LUser, LServer))],
+ Ss = Mod:get_sessions(LUser, LServer),
+ {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
MaxSessions = get_max_user_sessions(LUser, LServer),
- if length(SIDs) =< MaxSessions -> ok;
- true -> {_, Pid} = lists:min(SIDs), Pid ! replaced
+ if length(OnlineSs) =< MaxSessions -> ok;
+ true ->
+ #session{sid = {_, Pid}} = lists:min(OnlineSs),
+ Pid ! replaced
+ end,
+ if length(OfflineSs) =< MaxSessions -> ok;
+ true ->
+ #session{sid = SID, usr = {_, _, R}} = lists:min(OfflineSs),
+ Mod:delete_session(LUser, LServer, R, SID)
end.
%% Get the user_max_session setting
diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl
index 9c78acaf7..049f1de58 100644
--- a/src/ejabberd_sm_redis.erl
+++ b/src/ejabberd_sm_redis.erl
@@ -144,7 +144,10 @@ clean_table() ->
{_, SID} = binary_to_term(USSIDKey),
node(element(2, SID)) == node()
end, Vals),
- Q1 = ["HDEL", ServKey | Vals1],
+ Q1 = case Vals1 of
+ [] -> [];
+ _ -> ["HDEL", ServKey | Vals1]
+ end,
Q2 = lists:map(
fun(USSIDKey) ->
{US, SID} = binary_to_term(USSIDKey),
@@ -152,7 +155,7 @@ clean_table() ->
SIDKey = sid_to_key(SID),
["HDEL", USKey, SIDKey]
end, Vals1),
- Res = ejabberd_redis:qp([Q1|Q2]),
+ Res = ejabberd_redis:qp(lists:delete([], [Q1|Q2])),
case lists:filter(
fun({ok, _}) -> false;
(_) -> true
diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl
index 8116d617f..8db8b6c5f 100644
--- a/src/ejabberd_sql.erl
+++ b/src/ejabberd_sql.erl
@@ -629,7 +629,7 @@ generic_sql_query_format(SQLQuery) ->
generic_escape() ->
#sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end,
- integer = fun(X) -> integer_to_binary(X) end,
+ integer = fun(X) -> jlib:i2l(X) end,
boolean = fun(true) -> <<"1">>;
(false) -> <<"0">>
end
@@ -646,7 +646,7 @@ sqlite_sql_query_format(SQLQuery) ->
sqlite_escape() ->
#sql_escape{string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end,
- integer = fun(X) -> integer_to_binary(X) end,
+ integer = fun(X) -> jlib:i2l(X) end,
boolean = fun(true) -> <<"1">>;
(false) -> <<"0">>
end
@@ -670,7 +670,7 @@ pgsql_prepare(SQLQuery, State) ->
pgsql_execute_escape() ->
#sql_escape{string = fun(X) -> X end,
- integer = fun(X) -> [integer_to_binary(X)] end,
+ integer = fun(X) -> [jlib:i2l(X)] end,
boolean = fun(true) -> "1";
(false) -> "0"
end
@@ -790,7 +790,7 @@ pgsql_connect(Server, Port, DB, Username, Password) ->
{port, Port},
{as_binary, true}]) of
{ok, Ref} ->
- pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>,
+ pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>,
<<"standard_conforming_strings='off';">>]),
pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]),
{ok, Ref};
diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl
index 498fcf9b0..bf17c8ab1 100644
--- a/src/ejabberd_web_admin.erl
+++ b/src/ejabberd_web_admin.erl
@@ -74,20 +74,27 @@ get_acl_rule([<<"vhosts">>], _) ->
%% The pages of a vhost are only accesible if the user is admin of that vhost:
get_acl_rule([<<"server">>, VHost | _RPath], Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
- {VHost, [configure, webadmin_view]};
+ AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ ACR = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access_readonly, fun(A) -> A end, webadmin_view),
+ {VHost, [AC, ACR]};
get_acl_rule([<<"server">>, VHost | _RPath], 'POST') ->
- {VHost, [configure]};
+ AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ {VHost, [AC]};
%% Default rule: only global admins can access any other random page
get_acl_rule(_RPath, Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
- {global, [configure, webadmin_view]};
-get_acl_rule(_RPath, 'POST') -> {global, [configure]}.
-
-is_acl_match(Host, Rules, Jid) ->
- lists:any(fun (Rule) ->
- allow == acl:match_rule(Host, Rule, Jid)
- end,
- Rules).
+ AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ ACR = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access_readonly, fun(A) -> A end, webadmin_view),
+ {global, [AC, ACR]};
+get_acl_rule(_RPath, 'POST') ->
+ AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ {global, [AC]}.
%%%==================================
%%%% Menu Items Access
@@ -138,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) ->
is_allowed_path(Path, JID);
is_allowed_path(Path, JID) ->
{HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'),
- is_acl_match(HostOfRule, AccessRule, JID).
+ acl:any_rules_allowed(HostOfRule, AccessRule, JID).
%% @spec(Path) -> URL
%% where Path = [string()]
@@ -266,7 +273,7 @@ get_auth_account(HostOfRule, AccessRule, User, Server,
Pass) ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
true ->
- case is_acl_match(HostOfRule, AccessRule,
+ case acl:any_rules_allowed(HostOfRule, AccessRule,
jid:make(User, Server, <<"">>))
of
false -> {unauthorized, <<"unprivileged-account">>};
@@ -740,7 +747,7 @@ process_admin(Host,
_ -> nothing
end,
ACLs = lists:keysort(2,
- ets:select(acl,
+ mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}])),
{NumLines, ACLsP} = term_to_paragraph(ACLs, 80),
@@ -777,7 +784,7 @@ process_admin(Host,
_ -> nothing
end,
ACLs = lists:keysort(2,
- ets:select(acl,
+ mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}])),
make_xhtml((?H1GL((?T(<<"Access Control Lists">>)),
@@ -842,7 +849,7 @@ process_admin(Host,
end;
_ -> nothing
end,
- Access = ets:select(access,
+ Access = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
{NumLines, AccessP} = term_to_paragraph(lists:keysort(2,Access), 80),
@@ -876,7 +883,7 @@ process_admin(Host,
end;
_ -> nothing
end,
- AccessRules = ets:select(access,
+ AccessRules = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
make_xhtml((?H1GL((?T(<<"Access Rules">>)),
@@ -1150,7 +1157,7 @@ term_to_paragraph(T, Cols) ->
term_to_id(T) -> jlib:encode_base64((term_to_binary(T))).
acl_parse_query(Host, Query) ->
- ACLs = ets:select(acl,
+ ACLs = mnesia:dirty_select(acl,
[{{acl, {'$1', Host}, '$2'}, [],
[{{acl, '$1', '$2'}}]}]),
case lists:keysearch(<<"submit">>, 1, Query) of
@@ -1264,7 +1271,7 @@ access_rules_to_xhtml(AccessRules, Lang) ->
<<"Add New">>)])])]))]).
access_parse_query(Host, Query) ->
- AccessRules = ets:select(access,
+ AccessRules = mnesia:dirty_select(access,
[{{access, {'$1', Host}, '$2'}, [],
[{{access, '$1', '$2'}}]}]),
case lists:keysearch(<<"addnew">>, 1, Query) of
@@ -1337,7 +1344,7 @@ parse_access_rule(Text) ->
list_vhosts(Lang, JID) ->
Hosts = (?MYHOSTS),
HostsAllowed = lists:filter(fun (Host) ->
- is_acl_match(Host,
+ acl:any_rules_allowed(Host,
[configure, webadmin_view],
JID)
end,
@@ -1549,7 +1556,7 @@ su_to_list({Server, User}) ->
%%%% get_stats
get_stats(global, Lang) ->
- OnlineUsers = mnesia:table_info(session, size),
+ OnlineUsers = ejabberd_sm:connected_users_number(),
RegisteredUsers = lists:foldl(fun (Host, Total) ->
ejabberd_auth:get_vh_registered_users_number(Host)
+ Total
@@ -2175,7 +2182,7 @@ get_node(global, Node, [<<"stats">>], _Query, Lang) ->
CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]),
CPUTimeS = list_to_binary(io_lib:format("~.3f",
[element(1, CPUTime) / 1000])),
- OnlineUsers = mnesia:table_info(session, size),
+ OnlineUsers = ejabberd_sm:connected_users_number(),
TransactionsCommitted = ejabberd_cluster:call(Node, mnesia,
system_info, [transaction_commits]),
TransactionsAborted = ejabberd_cluster:call(Node, mnesia,
@@ -2970,7 +2977,8 @@ make_menu_item(item, 3, URI, Name, Lang) ->
%%%==================================
-opt_type(access) -> fun (V) -> V end;
-opt_type(_) -> [access].
+opt_type(access) -> fun acl:access_rules_validator/1;
+opt_type(access_readonly) -> fun acl:access_rules_validator/1;
+opt_type(_) -> [access, access_readonly].
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl
index 6259b4efd..1b795d3fd 100644
--- a/src/ejabberd_xmlrpc.erl
+++ b/src/ejabberd_xmlrpc.erl
@@ -47,7 +47,8 @@
-record(state,
{access_commands = [] :: list(),
auth = noauth :: noauth | {binary(), binary(), binary()},
- get_auth = true :: boolean()}).
+ get_auth = true :: boolean(),
+ ip :: inet:ip_address()}).
%% Test:
@@ -195,7 +196,7 @@ socket_type() -> raw.
%% -----------------------------
%% HTTP interface
%% -----------------------------
-process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
+process(_, #request{method = 'POST', data = Data, opts = Opts, ip = {IP, _}}) ->
AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts,
fun(L) when is_list(L) -> L end,
undefined),
@@ -206,7 +207,7 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
lists:flatmap(
fun({Ac, AcOpts}) ->
Commands = gen_mod:get_opt(
- commands, AcOpts,
+ commands, lists:flatten(AcOpts),
fun(A) when is_atom(A) ->
A;
(L) when is_list(L) ->
@@ -219,15 +220,15 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
options, AcOpts,
fun(L) when is_list(L) -> L end,
[]),
- [{Ac, Commands, CommOpts}];
+ [{<<"ejabberd_xmlrpc compatibility shim">>, {[?MODULE], [{access, Ac}], Commands}}];
(Wrong) ->
?WARNING_MSG("wrong options format for ~p: ~p",
[?MODULE, Wrong]),
[]
- end, AccessCommandsOpts)
+ end, lists:flatten(AccessCommandsOpts))
end,
GetAuth = true,
- State = #state{access_commands = AccessCommands, get_auth = GetAuth},
+ State = #state{access_commands = AccessCommands, get_auth = GetAuth, ip = IP},
case fxml_stream:parse_element(Data) of
{error, _} ->
{400, [],
@@ -258,21 +259,35 @@ process(_, _) ->
%% Access verification
%% -----------------------------
-get_auth(AuthList) ->
- Admin =
- case lists:keysearch(admin, 1, AuthList) of
- {value, {admin, true}} -> true;
- _ -> false
- end,
+extract_auth(AuthList) ->
+ ?DEBUG("AUTHLIST ~p", [AuthList]),
try get_attrs([user, server, token], AuthList) of
- [U, S, T] -> {U, S, {oauth, T}, Admin}
+ [U0, S0, T] ->
+ U = jid:nodeprep(U0),
+ S = jid:nameprep(S0),
+ case ejabberd_oauth:check_token(T) of
+ {ok, {U, S}, Scope} ->
+ #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
+ {false, Reason} ->
+ {error, Reason};
+ _ ->
+ {error, not_found}
+ end
catch
exit:{attribute_not_found, _Attr, _} ->
try get_attrs([user, server, password], AuthList) of
- [U, S, P] -> {U, S, P, Admin}
+ [U0, S0, P] ->
+ U = jid:nodeprep(U0),
+ S = jid:nameprep(S0),
+ case ejabberd_auth:check_password(U, <<"">>, S, P) of
+ true ->
+ #{usr => {U, S, <<"">>}, caller_server => S};
+ false ->
+ {error, invalid_auth}
+ end
catch
- exit:{attribute_not_found, Attr, _} ->
- throw({error, missing_auth_arguments, Attr})
+ exit:{attribute_not_found, _Attr, _} ->
+ #{}
end
end.
@@ -300,12 +315,28 @@ get_auth(AuthList) ->
%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}).
%% {ok,{response,[152]}}
-handler(#state{get_auth = true, auth = noauth} = State,
+handler(#state{get_auth = true, auth = noauth, ip = IP} = State,
{call, Method,
[{struct, AuthList} | Arguments] = AllArgs}) ->
- try get_auth(AuthList) of
+ try extract_auth(AuthList) of
+ {error, invalid_auth} ->
+ build_fault_response(-118,
+ "Invalid authentication data",
+ []);
+ {error, not_found} ->
+ build_fault_response(-118,
+ "Invalid oauth token",
+ []);
+ {error, expired} ->
+ build_fault_response(-118,
+ "Invalid oauth token",
+ []);
+ {error, Value} ->
+ build_fault_response(-118,
+ "Invalid authentication data: ~p",
+ [Value]);
Auth ->
- handler(State#state{get_auth = false, auth = Auth},
+ handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}},
{call, Method, Arguments})
catch
{error, missing_auth_arguments, _Attr} ->
@@ -393,9 +424,14 @@ build_fault_response(Code, ParseString, ParseArgs) ->
do_command(AccessCommands, Auth, Command, AttrL, ArgsF,
ResultF) ->
ArgsFormatted = format_args(AttrL, ArgsF),
+ Auth2 = case AccessCommands of
+ V when is_list(V) ->
+ Auth#{extra_permissions => AccessCommands};
+ _ ->
+ Auth
+ end,
Result =
- ejabberd_commands:execute_command(AccessCommands, Auth,
- Command, ArgsFormatted),
+ ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth2),
ResultFormatted = format_result(Result, ResultF),
{command_result, ResultFormatted}.
@@ -489,6 +525,8 @@ process_unicode_codepoints(Str) ->
format_result({error, Error}, _) ->
throw({error, Error});
+format_result({error, _Type, _Code, Error}, _) ->
+ throw({error, Error});
format_result(String, string) -> lists:flatten(String);
format_result(Atom, {Name, atom}) ->
{struct,
diff --git a/src/ext_mod.erl b/src/ext_mod.erl
index 332d2c5e2..842bb09fc 100644
--- a/src/ext_mod.erl
+++ b/src/ext_mod.erl
@@ -484,17 +484,28 @@ compile_deps(_Module, _Spec, DestDir) ->
filelib:ensure_dir(filename:join(Ebin, ".")),
Result = lists:foldl(fun(Dep, Acc) ->
Inc = filename:join(Dep, "include"),
+ Lib = filename:join(Dep, "lib"),
Src = filename:join(Dep, "src"),
Options = [{outdir, Ebin}, {i, Inc}],
[file:copy(App, Ebin) || App <- filelib:wildcard(Src++"/*.app")],
- Acc++[case compile:file(File, Options) of
+
+ %% Compile erlang files
+ Acc1 = Acc ++ [case compile:file(File, Options) of
{ok, _} -> ok;
{ok, _, _} -> ok;
{ok, _, _, _} -> ok;
error -> {error, {compilation_failed, File}};
Error -> Error
end
- || File <- filelib:wildcard(Src++"/*.erl")]
+ || File <- filelib:wildcard(Src++"/*.erl")],
+
+ %% Compile elixir files
+ Acc1 ++ [case compile_elixir_file(Ebin, File) of
+ {ok, _} -> ok;
+ {error, File} -> {error, {compilation_failed, File}}
+ end
+ || File <- filelib:wildcard(Lib ++ "/*.ex")]
+
end, [], filelib:wildcard("deps/*")),
case lists:dropwhile(
fun(ok) -> true;
@@ -515,6 +526,8 @@ compile(_Module, _Spec, DestDir) ->
verbose, report_errors, report_warnings]
++ ExtLib,
[file:copy(App, Ebin) || App <- filelib:wildcard("src/*.app")],
+
+ %% Compile erlang files
Result = [case compile:file(File, Options) of
{ok, _} -> ok;
{ok, _, _} -> ok;
@@ -523,14 +536,32 @@ compile(_Module, _Spec, DestDir) ->
Error -> Error
end
|| File <- filelib:wildcard("src/*.erl")],
+
+ %% Compile elixir files
+ Result1 = Result ++ [case compile_elixir_file(Ebin, File) of
+ {ok, _} -> ok;
+ {error, File} -> {error, {compilation_failed, File}}
+ end
+ || File <- filelib:wildcard("lib/*.ex")],
+
case lists:dropwhile(
fun(ok) -> true;
(_) -> false
- end, Result) of
+ end, Result1) of
[] -> ok;
[Error|_] -> Error
end.
+compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) ->
+ compile_elixir_file(list_to_binary(Dest), list_to_binary(File));
+
+compile_elixir_file(Dest, File) ->
+ try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of
+ [Module] -> {ok, Module}
+ catch
+ _ -> {error, File}
+ end.
+
install(Module, Spec, DestDir) ->
Errors = lists:dropwhile(fun({_, {ok, _}}) -> true;
(_) -> false
diff --git a/src/extauth.erl b/src/extauth.erl
index 50330b47b..6063d3670 100644
--- a/src/extauth.erl
+++ b/src/extauth.erl
@@ -102,8 +102,7 @@ call_port(Server, Msg) ->
receive {eauth, Result} -> Result end.
random_instance(MaxNum) ->
- random:seed(p1_time_compat:timestamp()),
- random:uniform(MaxNum) - 1.
+ randoms:uniform(MaxNum) - 1.
get_instances(Server) ->
ejabberd_config:get_option(
diff --git a/src/gen_mod.erl b/src/gen_mod.erl
index 476e19e9d..aaf452aeb 100644
--- a/src/gen_mod.erl
+++ b/src/gen_mod.erl
@@ -48,7 +48,7 @@
opts = [] :: opts() | '_' | '$2'}).
-type opts() :: [{atom(), any()}].
--type db_type() :: sql | mnesia | riak | ldap.
+-type db_type() :: sql | mnesia | riak.
-callback start(binary(), opts()) -> any().
-callback stop(binary()) -> any().
@@ -147,7 +147,7 @@ start_module(Host, Module) ->
-spec start_module(binary(), atom(), opts()) -> any().
start_module(Host, Module, Opts0) ->
- Opts = validate_opts(Host, Module, Opts0),
+ Opts = validate_opts(Module, Opts0),
ets:insert(ejabberd_modules,
#ejabberd_module{module_host = {Module, Host},
opts = Opts}),
@@ -308,10 +308,47 @@ get_opt_host(Host, Opts, Default) ->
Val = get_opt(host, Opts, fun iolist_to_binary/1, Default),
ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host).
-validate_opts(Host, Module, Opts) ->
+
+get_module_mod_opt_type_fun(Module) ->
+ DBSubMods = ejabberd_config:v_dbs_mods(Module),
+ fun(Opt) ->
+ Res = lists:foldl(fun(Mod, {Funs, ArgsList, _} = Acc) ->
+ case catch Mod:mod_opt_type(Opt) of
+ Fun when is_function(Fun) ->
+ {[Fun | Funs], ArgsList, true};
+ L when is_list(L) ->
+ {Funs, L ++ ArgsList, true};
+ _ ->
+ Acc
+ end
+ end, {[], [], false}, [Module | DBSubMods]),
+ case Res of
+ {[], [], false} ->
+ throw({'EXIT', {undef, mod_opt_type}});
+ {[], Args, _} -> Args;
+ {Funs, _, _} ->
+ fun(Val) ->
+ lists:any(fun(F) ->
+ try F(Val) of
+ _ ->
+ true
+ catch {replace_with, _NewVal} = E ->
+ throw(E);
+ {invalid_syntax, _Error} = E2 ->
+ throw(E2);
+ _:_ ->
+ false
+ end
+ end, Funs)
+ end
+ end
+ end.
+
+validate_opts(Module, Opts) ->
+ ModOptFun = get_module_mod_opt_type_fun(Module),
lists:filtermap(
fun({Opt, Val}) ->
- case catch validate_opt(Host, Module, Opt, Opts) of
+ case catch ModOptFun(Opt) of
VFun when is_function(VFun) ->
try VFun(Val) of
_ ->
@@ -346,22 +383,6 @@ validate_opts(Host, Module, Opts) ->
false
end, Opts).
-validate_opt(Host, Module, Opt, Opts) ->
- case Module:mod_opt_type(Opt) of
- VFun1 when is_function(VFun1) ->
- VFun1;
- L1 when is_list(L1) ->
- DBModule = db_mod(Host, Opts, Module),
- try DBModule:mod_opt_type(Opt) of
- VFun2 when is_function(VFun2) ->
- VFun2;
- L2 when is_list(L2) ->
- lists:usort(L1 ++ L2)
- catch _:undef ->
- L1
- end
- end.
-
-spec db_type(binary() | global, module()) -> db_type();
(opts(), module()) -> db_type().
@@ -378,7 +399,7 @@ db_type(Host, Module) when is_atom(Module) ->
undefined
end.
--spec db_type(global | binary(), opts(), module()) -> db_type().
+-spec db_type(binary(), opts(), module()) -> db_type().
db_type(Host, Opts, Module) ->
case catch Module:mod_opt_type(db_type) of
diff --git a/src/http_p1.erl b/src/http_p1.erl
new file mode 100644
index 000000000..f430bbe11
--- /dev/null
+++ b/src/http_p1.erl
@@ -0,0 +1,358 @@
+%%%----------------------------------------------------------------------
+%%% File : http_p1.erl
+%%% Author : Emilio Bustos <ebustos@process-one.net>
+%%% Purpose : Provide a common API for inets / lhttpc / ibrowse
+%%% Created : 29 Jul 2010 by Emilio Bustos <ebustos@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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(http_p1).
+
+-author('ebustos@process-one.net').
+
+-export([start/0, stop/0, get/1, get/2, post/2, post/3,
+ request/3, request/4, request/5,
+ get_pool_size/0, set_pool_size/1]).
+
+-include("logger.hrl").
+
+-define(USE_INETS, 1).
+% -define(USE_LHTTPC, 1).
+% -define(USE_IBROWSE, 1).
+% inets used as default if none specified
+
+-ifdef(USE_IBROWSE).
+
+start() ->
+ ejabberd:start_app(ibrowse).
+
+stop() ->
+ application:stop(ibrowse).
+
+request(Method, URL, Hdrs, Body, Opts) ->
+ TimeOut = proplists:get_value(timeout, Opts, infinity),
+ Options = [{inactivity_timeout, TimeOut}
+ | proplists:delete(timeout, Opts)],
+ case ibrowse:send_req(URL, Hdrs, Method, Body, Options)
+ of
+ {ok, Status, Headers, Response} ->
+ {ok, jlib:binary_to_integer(Status), Headers,
+ Response};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ application:get_env(ibrowse, default_max_sessions, 10).
+
+set_pool_size(Size) ->
+ application:set_env(ibrowse, default_max_sessions, Size).
+
+-else.
+
+-ifdef(USE_LHTTPC).
+
+start() ->
+ ejabberd:start_app(lhttpc).
+
+stop() ->
+ application:stop(lhttpc).
+
+request(Method, URL, Hdrs, Body, Opts) ->
+ {[TO, SO], Rest} = proplists:split(Opts, [timeout, socket_options]),
+ TimeOut = proplists:get_value(timeout, TO, infinity),
+ SockOpt = proplists:get_value(socket_options, SO, []),
+ Options = [{connect_options, SockOpt} | Rest],
+ Result = lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options),
+ ?DEBUG("HTTP request -> response:~n"
+ "** Method = ~p~n"
+ "** URI = ~s~n"
+ "** Body = ~s~n"
+ "** Hdrs = ~p~n"
+ "** Timeout = ~p~n"
+ "** Options = ~p~n"
+ "** Response = ~p",
+ [Method, URL, Body, Hdrs, TimeOut, Options, Result]),
+ case Result of
+ {ok, {{Status, _Reason}, Headers, Response}} ->
+ {ok, Status, Headers, (Response)};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ Opts = proplists:get_value(lhttpc_manager, lhttpc_manager:list_pools()),
+ proplists:get_value(max_pool_size,Opts).
+
+set_pool_size(Size) ->
+ lhttpc_manager:set_max_pool_size(lhttpc_manager, Size).
+
+-else.
+
+start() ->
+ ejabberd:start_app(inets).
+
+stop() ->
+ application:stop(inets).
+
+to_list(Str) when is_binary(Str) ->
+ binary_to_list(Str);
+to_list(Str) ->
+ Str.
+
+request(Method, URLRaw, HdrsRaw, Body, Opts) ->
+ Hdrs = lists:map(fun({N, V}) ->
+ {to_list(N), to_list(V)}
+ end, HdrsRaw),
+ URL = to_list(URLRaw),
+
+ Request = case Method of
+ get -> {URL, Hdrs};
+ head -> {URL, Hdrs};
+ delete -> {URL, Hdrs};
+ _ -> % post, etc.
+ {URL, Hdrs,
+ to_list(proplists:get_value(<<"content-type">>, HdrsRaw, [])),
+ Body}
+ end,
+ Options = case proplists:get_value(timeout, Opts,
+ infinity)
+ of
+ infinity -> proplists:delete(timeout, Opts);
+ _ -> Opts
+ end,
+ case httpc:request(Method, Request, Options, []) of
+ {ok, {{_, Status, _}, Headers, Response}} ->
+ {ok, Status, Headers, Response};
+ {error, Reason} -> {error, Reason}
+ end.
+
+get_pool_size() ->
+ {ok, Size} = httpc:get_option(max_sessions),
+ Size.
+
+set_pool_size(Size) ->
+ httpc:set_option(max_sessions, Size).
+
+-endif.
+
+-endif.
+
+-type({header,
+ {type, 63, tuple,
+ [{type, 63, union,
+ [{type, 63, string, []}, {type, 63, atom, []}]},
+ {type, 63, string, []}]},
+ []}).
+
+-type({headers,
+ {type, 64, list, [{type, 64, header, []}]}, []}).
+
+-type({option,
+ {type, 67, union,
+ [{type, 67, tuple,
+ [{atom, 67, connect_timeout}, {type, 67, timeout, []}]},
+ {type, 68, tuple,
+ [{atom, 68, timeout}, {type, 68, timeout, []}]},
+ {type, 70, tuple,
+ [{atom, 70, send_retry},
+ {type, 70, non_neg_integer, []}]},
+ {type, 71, tuple,
+ [{atom, 71, partial_upload},
+ {type, 71, union,
+ [{type, 71, non_neg_integer, []},
+ {atom, 71, infinity}]}]},
+ {type, 72, tuple,
+ [{atom, 72, partial_download}, {type, 72, pid, []},
+ {type, 72, union,
+ [{type, 72, non_neg_integer, []},
+ {atom, 72, infinity}]}]}]},
+ []}).
+
+-type({options,
+ {type, 74, list, [{type, 74, option, []}]}, []}).
+
+-type({result,
+ {type, 76, union,
+ [{type, 76, tuple,
+ [{atom, 76, ok},
+ {type, 76, tuple,
+ [{type, 76, tuple,
+ [{type, 76, pos_integer, []}, {type, 76, string, []}]},
+ {type, 76, headers, []}, {type, 76, string, []}]}]},
+ {type, 77, tuple,
+ [{atom, 77, error}, {type, 77, atom, []}]}]},
+ []}).
+
+%% @spec (URL) -> Result
+%% URL = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, [])',
+%% that is {@link request/3} with an empty header list.
+%% @end
+%% @see request/3
+-spec get(string()) -> result().
+get(URL) -> request(get, URL, []).
+
+%% @spec (URL, Hdrs) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a GET request.
+%% Would be the same as calling `request(get, URL, Hdrs)'.
+%% @end
+%% @see request/3
+-spec get(string(), headers()) -> result().
+get(URL, Hdrs) -> request(get, URL, Hdrs).
+
+%% @spec (URL, RequestBody) -> Result
+%% URL = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request with form data.
+%% Would be the same as calling
+%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), string()) -> result().
+post(URL, Body) ->
+ request(post, URL,
+ [{<<"content-type">>, <<"x-www-form-urlencoded">>}],
+ Body).
+
+%% @spec (URL, Hdrs, RequestBody) -> Result
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a POST request.
+%% Would be the same as calling
+%% `request(post, URL, Hdrs, Body)'.
+%% @end
+%% @see request/4
+-spec post(string(), headers(), string()) -> result().
+post(URL, Hdrs, Body) ->
+ NewHdrs = case [X
+ || {X, _} <- Hdrs,
+ str:to_lower(X) == <<"content-type">>]
+ of
+ [] ->
+ [{<<"content-type">>, <<"x-www-form-urlencoded">>}
+ | Hdrs];
+ _ -> Hdrs
+ end,
+ request(post, URL, NewHdrs, Body).
+
+%% @spec (Method, URL, Hdrs) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request without a body.
+%% Would be the same as calling `request(Method, URL, Hdrs, [], [])',
+%% that is {@link request/5} with an empty body.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers()) -> result().
+request(Method, URL, Hdrs) ->
+ request(Method, URL, Hdrs, [], []).
+
+%% @spec (Method, URL, Hdrs, RequestBody) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string()) -> result().
+request(Method, URL, Hdrs, Body) ->
+ request(Method, URL, Hdrs, Body, []).
+
+%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result
+%% Method = atom()
+%% URL = string()
+%% Hdrs = [{Header, Value}]
+%% Header = string()
+%% Value = string()
+%% RequestBody = string()
+%% Options = [Option]
+%% Option = {timeout, Milliseconds | infinity} |
+%% {connect_timeout, Milliseconds | infinity} |
+%% {socket_options, [term()]} |
+
+%% Milliseconds = integer()
+%% Result = {ok, StatusCode, Hdrs, ResponseBody}
+%% | {error, Reason}
+%% StatusCode = integer()
+%% ResponseBody = string()
+%% Reason = connection_closed | connect_timeout | timeout
+%% @doc Sends a request with a body.
+%% Would be the same as calling
+%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5}
+%% with no options.
+%% @end
+%% @see request/5
+-spec request(atom(), string(), headers(), string(), options()) -> result().
+
+% ibrowse {response_format, response_format()} |
+% Options - [option()]
+% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result,
+% boolean()} | {headers_as_is, boolean()}
+%body_format() = string() | binary()
+% The body_format option is only valid for the synchronous request and the default is string.
+% When making an asynchronous request the body will always be received as a binary.
+% lhttpc: always binary
+
diff --git a/src/jid.erl b/src/jid.erl
index 9e8ea9d23..287a0642b 100644
--- a/src/jid.erl
+++ b/src/jid.erl
@@ -52,11 +52,35 @@
-spec start() -> ok.
start() ->
+ {ok, Owner} = ets_owner(),
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
- catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]),
+ %% Table is public to allow ETS insert to fix / update the table even if table already exist
+ %% with another owner.
+ catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]),
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
ok.
+ets_owner() ->
+ case whereis(jlib_ets) of
+ undefined ->
+ Pid = spawn(fun() -> ets_keepalive() end),
+ case catch register(jlib_ets, Pid) of
+ true ->
+ {ok, Pid};
+ Error -> Error
+ end;
+ Pid ->
+ {ok,Pid}
+ end.
+
+%% Process used to keep jlib ETS table alive in case the original owner dies.
+%% The table need to be public, otherwise subsequent inserts would fail.
+ets_keepalive() ->
+ receive
+ _ ->
+ ets_keepalive()
+ end.
+
-spec make(binary(), binary(), binary()) -> jid() | error.
make(User, Server, Resource) ->
diff --git a/src/jlib.erl b/src/jlib.erl
index b79b8fa7c..aca3b0ee8 100644
--- a/src/jlib.erl
+++ b/src/jlib.erl
@@ -373,15 +373,20 @@ iq_type_to_string(error) -> <<"error">>.
-spec iq_to_xml(IQ :: iq()) -> xmlel().
iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) ->
+ Children =
+ if
+ is_list(SubEl) -> SubEl;
+ true -> [SubEl]
+ end,
if ID /= <<"">> ->
#xmlel{name = <<"iq">>,
attrs =
[{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}],
- children = SubEl};
+ children = Children};
true ->
#xmlel{name = <<"iq">>,
attrs = [{<<"type">>, iq_type_to_string(Type)}],
- children = SubEl}
+ children = Children}
end.
-spec parse_xdata_submit(El :: xmlel()) ->
@@ -579,33 +584,8 @@ add_delay_info(El, From, Time) ->
binary()) -> xmlel().
add_delay_info(El, From, Time, Desc) ->
- case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of
- false ->
- %% Add new tag
DelayTag = create_delay_tag(Time, From, Desc),
- fxml:append_subtags(El, [DelayTag]);
- DelayTag ->
- %% Update existing tag
- NewDelayTag =
- case {fxml:get_tag_cdata(DelayTag), Desc} of
- {<<"">>, <<"">>} ->
- DelayTag;
- {OldDesc, <<"">>} ->
- DelayTag#xmlel{children = [{xmlcdata, OldDesc}]};
- {<<"">>, NewDesc} ->
- DelayTag#xmlel{children = [{xmlcdata, NewDesc}]};
- {OldDesc, NewDesc} ->
- case binary:match(OldDesc, NewDesc) of
- nomatch ->
- FinalDesc = <<OldDesc/binary, ", ", NewDesc/binary>>,
- DelayTag#xmlel{children = [{xmlcdata, FinalDesc}]};
- _ ->
- DelayTag#xmlel{children = [{xmlcdata, OldDesc}]}
- end
- end,
- NewEl = fxml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}),
- fxml:append_subtags(NewEl, [NewDelayTag])
- end.
+ fxml:append_subtags(El, [DelayTag]).
-spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary())
-> xmlel() | error.
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index 4598805c2..69fffbd7c 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -378,6 +378,7 @@ get_commands_spec() ->
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
+ longdesc = "Group can be several groups separated by ; for example: \"g1;g2;g3\"",
module = ?MODULE, function = add_rosteritem,
args = [{localuser, binary}, {localserver, binary},
{user, binary}, {server, binary},
@@ -536,7 +537,7 @@ get_commands_spec() ->
policy = user,
module = mod_offline, function = count_offline_messages,
args = [],
- result = {res, integer}},
+ result = {value, integer}},
#ejabberd_commands{name = send_message, tags = [stanza],
desc = "Send a message to a local or remote bare of full JID",
module = ?MODULE, function = send_message,
@@ -864,12 +865,15 @@ connected_users_vhost(Host) ->
%% Code copied from ejabberd_sm.erl and customized
dirty_get_sessions_list2() ->
- mnesia:dirty_select(
+ Ss = mnesia:dirty_select(
session,
- [{#session{usr = '$1', sid = {'$2', '$3'}, priority = '$4', info = '$5',
+ [{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4',
_ = '_'},
- [{is_pid, '$3'}],
- [['$1', {{'$2', '$3'}}, '$4', '$5']]}]).
+ [],
+ [['$1', '$2', '$3', '$4']]}]),
+ lists:filter(fun([_USR, _SID, _Priority, Info]) ->
+ not proplists:get_bool(offline, Info)
+ end, Ss).
%% Make string more print-friendly
stringize(String) ->
@@ -906,8 +910,8 @@ user_sessions_info(User, Host) ->
{'EXIT', _Reason} ->
[];
Ss ->
- lists:filter(fun(#session{sid = {_, Pid}}) ->
- is_pid(Pid)
+ lists:filter(fun(#session{info = Info}) ->
+ not proplists:get_bool(offline, Info)
end, Ss)
end,
lists:map(
@@ -1140,8 +1144,8 @@ subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -
subscribe_roster({Name, Server, Group, Nick}, Roster);
%% Subscribe Name2 to Name1
subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) ->
- subscribe(Name1, Server1, list_to_binary(Name2), list_to_binary(Server2),
- list_to_binary(Nick2), list_to_binary(Group2), <<"both">>, []),
+ subscribe(Name1, Server1, iolist_to_binary(Name2), iolist_to_binary(Server2),
+ iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []),
subscribe_roster({Name1, Server1, Group1, Nick1}, Roster).
push_alltoall(S, G) ->
@@ -1173,10 +1177,11 @@ push_roster_item(LU, LS, R, U, S, Action) ->
ejabberd_router:route(jid:remove_resource(LJID), LJID, ResIQ).
build_roster_item(U, S, {add, Nick, Subs, Group}) ->
+ Groups = binary:split(Group,<<";">>, [global]),
#roster_item{jid = jid:make(U, S),
name = Nick,
subscription = jlib:binary_to_atom(Subs),
- groups = [Group]};
+ groups = Groups};
build_roster_item(U, S, remove) ->
#roster_item{jid = jid:make(U, S), subscription = remove}.
@@ -1260,11 +1265,11 @@ srg_create(Group, Host, Name, Description, Display) ->
Opts = [{name, Name},
{displayed_groups, DisplayList},
{description, Description}],
- {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts),
+ {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
srg_delete(Group, Host) ->
- {atomic, ok} = mod_shared_roster:delete_group(Host, Group),
+ {atomic, _} = mod_shared_roster:delete_group(Host, Group),
ok.
srg_list(Host) ->
@@ -1287,11 +1292,11 @@ srg_get_members(Group, Host) ->
|| {MUser, MServer} <- Members].
srg_user_add(User, Host, Group, GroupHost) ->
- {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
+ {atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
ok.
srg_user_del(User, Host, Group, GroupHost) ->
- {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
+ {atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
ok.
@@ -1302,44 +1307,9 @@ srg_user_del(User, Host, Group, GroupHost) ->
%% @doc Send a message to a Jabber account.
%% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok
send_message(Type, From, To, Subject, Body) ->
+ FromJID = jid:from_string(From),
+ ToJID = jid:from_string(To),
Packet = build_packet(Type, Subject, Body),
- send_packet_all_resources(From, To, Packet).
-
-%% @doc Send a packet to a Jabber account.
-%% If a resource was specified in the JID,
-%% the packet is sent only to that specific resource.
-%% If no resource was specified in the JID,
-%% and the user is remote or local but offline,
-%% the packet is sent to the bare JID.
-%% If the user is local and is online in several resources,
-%% the packet is sent to all its resources.
-send_packet_all_resources(FromJIDString, ToJIDString, Packet) ->
- FromJID = jid:from_string(FromJIDString),
- ToJID = jid:from_string(ToJIDString),
- ToUser = ToJID#jid.user,
- ToServer = ToJID#jid.server,
- case ToJID#jid.resource of
- <<>> ->
- send_packet_all_resources(FromJID, ToUser, ToServer, Packet);
- Res ->
- send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet)
- end.
-
-send_packet_all_resources(FromJID, ToUser, ToServer, Packet) ->
- case ejabberd_sm:get_user_resources(ToUser, ToServer) of
- [] ->
- send_packet_all_resources(FromJID, ToUser, ToServer, <<>>, Packet);
- ToResources ->
- lists:foreach(
- fun(ToResource) ->
- send_packet_all_resources(FromJID, ToUser, ToServer,
- ToResource, Packet)
- end,
- ToResources)
- end.
-
-send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) ->
- ToJID = jid:make(ToU, ToS, ToR),
ejabberd_router:route(FromJID, ToJID, Packet).
build_packet(Type, Subject, Body) ->
diff --git a/src/mod_announce.erl b/src/mod_announce.erl
index 495cbf946..1d93cbe65 100644
--- a/src/mod_announce.erl
+++ b/src/mod_announce.erl
@@ -609,8 +609,8 @@ announce_all(From, To, Packet) ->
Local = jid:make(To#jid.server),
lists:foreach(
fun({User, Server}) ->
- Dest = jid:make(User, Server),
- ejabberd_router:route(Local, Dest, Packet)
+ Dest = jid:make(User, Server, <<>>),
+ ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, ejabberd_auth:get_vh_registered_users(Host))
end.
@@ -626,8 +626,8 @@ announce_all_hosts_all(From, To, Packet) ->
Local = jid:make(To#jid.server),
lists:foreach(
fun({User, Server}) ->
- Dest = jid:make(User, Server),
- ejabberd_router:route(Local, Dest, Packet)
+ Dest = jid:make(User, Server, <<>>),
+ ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, ejabberd_auth:dirty_get_registered_users())
end.
@@ -813,7 +813,7 @@ send_announcement_to_all(Host, SubjectS, BodyS) ->
lists:foreach(
fun({U, S, R}) ->
Dest = jid:make(U, S, R),
- ejabberd_router:route(Local, Dest, Packet)
+ ejabberd_router:route(Local, Dest, add_store_hint(Packet))
end, Sessions).
-spec get_access(global | binary()) -> atom().
@@ -823,6 +823,10 @@ get_access(Host) ->
fun(A) -> A end,
none).
+-spec add_store_hint(stanza()) -> stanza().
+add_store_hint(El) ->
+ xmpp:set_subtag(El, #hint{type = store}).
+
%%-------------------------------------------------------------------------
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl
index e35caa1c7..023e8dc6f 100644
--- a/src/mod_carboncopy.erl
+++ b/src/mod_carboncopy.erl
@@ -123,6 +123,7 @@ user_receive_packet(Packet, _C2SState, JID, _From, To) ->
stanza() | {stop, stanza()}.
check_and_forward(JID, To, Packet, Direction)->
case is_chat_message(Packet) andalso
+ not is_muc_pm(To, Packet) andalso
xmpp:has_subtag(Packet, #carbons_private{}) == false andalso
xmpp:has_subtag(Packet, #hint{type = 'no-copy'}) == false of
true ->
@@ -232,6 +233,11 @@ is_chat_message(#message{type = normal, body = Body}) ->
is_chat_message(_) ->
false.
+is_muc_pm(#jid{lresource = <<>>}, _Packet) ->
+ false;
+is_muc_pm(_To, Packet) ->
+ xmpp:has_subtag(Packet, #muc_user{}).
+
-spec list(binary(), binary()) -> [{binary(), binary()}].
%% list {resource, cc_version} with carbons enabled for given user and host
list(User, Server) ->
diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl
index 7f0658eff..2bae7a4f8 100644
--- a/src/mod_client_state.erl
+++ b/src/mod_client_state.erl
@@ -34,8 +34,8 @@
-export([start/2, stop/1, mod_opt_type/1, depends/2]).
%% ejabberd_hooks callbacks.
--export([filter_presence/3, filter_chat_states/3, filter_pep/3, filter_other/3,
- flush_queue/2, add_stream_feature/2]).
+-export([filter_presence/4, filter_chat_states/4, filter_pep/4, filter_other/4,
+ flush_queue/3, add_stream_feature/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -151,26 +151,27 @@ depends(_Host, _Opts) ->
%% ejabberd_hooks callbacks.
%%--------------------------------------------------------------------
--spec filter_presence({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
+-spec filter_presence({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
-filter_presence({C2SState, _OutStanzas} = Acc, Host,
+filter_presence({C2SState, _OutStanzas} = Acc, Host, To,
#presence{type = Type} = Stanza) ->
if Type == available; Type == unavailable ->
- ?DEBUG("Got availability presence stanza", []),
+ ?DEBUG("Got availability presence stanza for ~s",
+ [jid:to_string(To)]),
queue_add(presence, Stanza, Host, C2SState);
true ->
Acc
end;
-filter_presence(Acc, _Host, _Stanza) -> Acc.
+filter_presence(Acc, _Host, _To, _Stanza) -> Acc.
--spec filter_chat_states({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
+-spec filter_chat_states({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
-filter_chat_states({C2SState, _OutStanzas} = Acc, Host,
- #message{from = From, to = To} = Stanza) ->
+filter_chat_states({C2SState, _OutStanzas} = Acc, Host, To,
+ #message{from = From} = Stanza) ->
case xmpp_util:is_standalone_chat_state(Stanza) of
true ->
case {From, To} of
@@ -180,40 +181,41 @@ filter_chat_states({C2SState, _OutStanzas} = Acc, Host,
%% conversations across clients.
Acc;
_ ->
- ?DEBUG("Got standalone chat state notification", []),
+ ?DEBUG("Got standalone chat state notification for ~s",
+ [jid:to_string(To)]),
queue_add(chatstate, Stanza, Host, C2SState)
end;
false ->
Acc
end;
-filter_chat_states(Acc, _Host, _Stanza) -> Acc.
+filter_chat_states(Acc, _Host, _To, _Stanza) -> Acc.
--spec filter_pep({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
+-spec filter_pep({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
-> {ejabberd_c2s:state(), [stanza()]} |
{stop, {ejabberd_c2s:state(), [stanza()]}}.
-filter_pep({C2SState, _OutStanzas} = Acc, Host, #message{} = Stanza) ->
+filter_pep({C2SState, _OutStanzas} = Acc, Host, To, #message{} = Stanza) ->
case get_pep_node(Stanza) of
undefined ->
Acc;
Node ->
- ?DEBUG("Got PEP notification", []),
+ ?DEBUG("Got PEP notification for ~s", [jid:to_string(To)]),
queue_add({pep, Node}, Stanza, Host, C2SState)
end;
-filter_pep(Acc, _Host, _Stanza) -> Acc.
+filter_pep(Acc, _Host, _To, _Stanza) -> Acc.
--spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), stanza())
- -> {stop, {ejabberd_c2s:state(), [stanza()]}}.
+-spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza())
+ -> {ejabberd_c2s:state(), [stanza()]}.
-filter_other({C2SState, _OutStanzas}, Host, Stanza) ->
- ?DEBUG("Won't add stanza to CSI queue", []),
+filter_other({C2SState, _OutStanzas}, Host, To, Stanza) ->
+ ?DEBUG("Won't add stanza for ~s to CSI queue", [jid:to_string(To)]),
queue_take(Stanza, Host, C2SState).
--spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary())
+-spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary(), jid())
-> {ejabberd_c2s:state(), [stanza()]}.
-flush_queue({C2SState, _OutStanzas}, Host) ->
- ?DEBUG("Going to flush CSI queue", []),
+flush_queue({C2SState, _OutStanzas}, Host, JID) ->
+ ?DEBUG("Going to flush CSI queue of ~s", [jid:to_string(JID)]),
Queue = get_queue(C2SState),
NewState = set_queue([], C2SState),
{NewState, get_stanzas(Queue, Host)}.
@@ -246,7 +248,7 @@ queue_add(Type, Stanza, Host, C2SState) ->
{stop, {NewState, []}}
end.
--spec queue_take(stanza(), binary(), term()) -> {stop, {term(), [stanza()]}}.
+-spec queue_take(stanza(), binary(), term()) -> {term(), [stanza()]}.
queue_take(Stanza, Host, C2SState) ->
From = xmpp:get_from(Stanza),
@@ -256,7 +258,7 @@ queue_take(Stanza, Host, C2SState) ->
U == LUser andalso S == LServer
end, get_queue(C2SState)),
NewState = set_queue(Rest, C2SState),
- {stop, {NewState, get_stanzas(Selected, Host) ++ [Stanza]}}.
+ {NewState, get_stanzas(Selected, Host) ++ [Stanza]}.
-spec set_queue(csi_queue(), term()) -> term().
diff --git a/src/mod_configure.erl b/src/mod_configure.erl
index 5e2ff351c..fc274dc03 100644
--- a/src/mod_configure.erl
+++ b/src/mod_configure.erl
@@ -1084,7 +1084,7 @@ get_form(Host, [<<"config">>, <<"acls">>], Lang) ->
ACLs = str:tokens(
iolist_to_binary(
io_lib:format("~p.",
- [ets:select(
+ [mnesia:dirty_select(
acl,
ets:fun2ms(
fun({acl, {Name, H}, Spec}) when H == Host ->
@@ -1103,7 +1103,7 @@ get_form(Host, [<<"config">>, <<"access">>], Lang) ->
Accs = str:tokens(
iolist_to_binary(
io_lib:format("~p.",
- [ets:select(
+ [mnesia:dirty_select(
access,
ets:fun2ms(
fun({access, {Name, H}, Acc}) when H == Host ->
@@ -1568,21 +1568,29 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>),
Xmlelement = xmpp:serr_policy_violation(<<"has been kicked">>, Lang),
case JID#jid.lresource of
<<>> ->
- SIDs = mnesia:dirty_select(session,
- [{#session{sid = {'$1', '$2'},
- usr = {LUser, LServer, '_'},
+ SIs = mnesia:dirty_select(session,
+ [{#session{usr = {LUser, LServer, '_'},
+ sid = '$1',
+ info = '$2',
_ = '_'},
- [{is_pid, '$2'}],
- [{{'$1', '$2'}}]}]),
- [Pid ! {kick, kicked_by_admin, Xmlelement} || {_, Pid} <- SIDs];
+ [], [{{'$1', '$2'}}]}]),
+ Pids = [P || {{_, P}, Info} <- SIs,
+ not proplists:get_bool(offline, Info)],
+ lists:foreach(fun(Pid) ->
+ Pid ! {kick, kicked_by_admin, Xmlelement}
+ end, Pids);
R ->
- [{_, Pid}] = mnesia:dirty_select(session,
- [{#session{sid = {'$1', '$2'},
- usr = {LUser, LServer, R},
+ [{{_, Pid}, Info}] = mnesia:dirty_select(
+ session,
+ [{#session{usr = {LUser, LServer, R},
+ sid = '$1',
+ info = '$2',
_ = '_'},
- [{is_pid, '$2'}],
- [{{'$1', '$2'}}]}]),
- Pid ! {kick, kicked_by_admin, Xmlelement}
+ [], [{{'$1', '$2'}}]}]),
+ case proplists:get_bool(offline, Info) of
+ true -> ok;
+ false -> Pid ! {kick, kicked_by_admin, Xmlelement}
+ end
end,
{result, undefined};
set_form(From, Host,
diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl
new file mode 100644
index 000000000..7fec01dcb
--- /dev/null
+++ b/src/mod_delegation.erl
@@ -0,0 +1,325 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 10 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(mod_delegation).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2]).
+-export([start/2, stop/1, mod_opt_type/1, depends/2]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+-export([component_connected/1, component_disconnected/2,
+ ejabberd_local/1, ejabberd_sm/1,
+ disco_local_features/5, disco_sm_features/5,
+ disco_local_identity/5, disco_sm_identity/5]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("xmpp.hrl").
+
+-type disco_acc() :: {error, stanza_error()} | {result, [binary()]} | empty.
+-record(state, {server_host = <<"">> :: binary(),
+ delegations = dict:new() :: ?TDICT}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(ejabberd_sup, PingSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
+mod_opt_type(namespaces) -> validate_fun();
+mod_opt_type(_) ->
+ [namespaces, iqdisc].
+
+depends(_, _) ->
+ [].
+
+-spec component_connected(binary()) -> ok.
+component_connected(Host) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_connected, Host})
+ end, ?MYHOSTS).
+
+-spec component_disconnected(binary(), binary()) -> ok.
+component_disconnected(Host, _Reason) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_disconnected, Host})
+ end, ?MYHOSTS).
+
+-spec ejabberd_local(iq()) -> iq().
+ejabberd_local(IQ) ->
+ process_iq(IQ, ejabberd_local).
+
+-spec ejabberd_sm(iq()) -> iq().
+ejabberd_sm(IQ) ->
+ process_iq(IQ, ejabberd_sm).
+
+-spec disco_local_features(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_local_features(Acc, From, To, Node, Lang) ->
+ disco_features(Acc, From, To, Node, Lang, ejabberd_local).
+
+-spec disco_sm_features(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_sm_features(Acc, From, To, Node, Lang) ->
+ disco_features(Acc, From, To, Node, Lang, ejabberd_sm).
+
+-spec disco_local_identity(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_local_identity(Acc, From, To, Node, Lang) ->
+ disco_identity(Acc, From, To, Node, Lang, ejabberd_local).
+
+-spec disco_sm_identity(disco_acc(), jid(), jid(), binary(), binary()) -> disco_acc().
+disco_sm_identity(Acc, From, To, Node, Lang) ->
+ disco_identity(Acc, From, To, Node, Lang, ejabberd_sm).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+init([Host, _Opts]) ->
+ ejabberd_hooks:add(component_connected, ?MODULE,
+ component_connected, 50),
+ ejabberd_hooks:add(component_disconnected, ?MODULE,
+ component_disconnected, 50),
+ ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
+ disco_local_features, 50),
+ ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
+ disco_sm_features, 50),
+ ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
+ disco_local_identity, 50),
+ ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE,
+ disco_sm_identity, 50),
+ {ok, #state{server_host = Host}}.
+
+handle_call(get_delegations, _From, State) ->
+ {reply, {ok, State#state.delegations}, State};
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast({component_connected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ To = jid:make(Host),
+ NSAttrsAccessList = gen_mod:get_module_opt(
+ ServerHost, ?MODULE, namespaces,
+ validate_fun(), []),
+ lists:foreach(
+ fun({NS, _Attrs, Access}) ->
+ case acl:match_rule(ServerHost, Access, To) of
+ allow ->
+ send_disco_queries(ServerHost, Host, NS);
+ deny ->
+ ok
+ end
+ end, NSAttrsAccessList),
+ {noreply, State};
+handle_cast({disco_info, Type, Host, NS, Info}, State) ->
+ From = jid:make(State#state.server_host),
+ To = jid:make(Host),
+ case dict:find({NS, Type}, State#state.delegations) of
+ error ->
+ Msg = #message{from = From, to = To,
+ sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]},
+ Delegations = dict:store({NS, Type}, {Host, Info}, State#state.delegations),
+ gen_iq_handler:add_iq_handler(Type, State#state.server_host, NS,
+ ?MODULE, Type, one_queue),
+ ejabberd_router:route(From, To, Msg),
+ ?INFO_MSG("Namespace '~s' is delegated to external component '~s'",
+ [NS, Host]),
+ {noreply, State#state{delegations = Delegations}};
+ {ok, {AnotherHost, _}} ->
+ ?WARNING_MSG("Failed to delegate namespace '~s' to "
+ "external component '~s' because it's already "
+ "delegated to '~s'",
+ [NS, Host, AnotherHost]),
+ {noreply, State}
+ end;
+handle_cast({component_disconnected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ Delegations =
+ dict:filter(
+ fun({NS, Type}, {H, _}) when H == Host ->
+ ?INFO_MSG("Remove delegation of namespace '~s' "
+ "from external component '~s'",
+ [NS, Host]),
+ gen_iq_handler:remove_iq_handler(Type, ServerHost, NS),
+ false;
+ (_, _) ->
+ true
+ end, State#state.delegations),
+ {noreply, State#state{delegations = Delegations}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ %% Note: we don't remove component_* hooks because they are global
+ %% and might be registered within a module on another virtual host
+ ServerHost = State#state.server_host,
+ ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE,
+ disco_local_features, 50),
+ ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE,
+ disco_sm_features, 50),
+ ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE,
+ disco_local_identity, 50),
+ ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE,
+ disco_sm_identity, 50),
+ lists:foreach(
+ fun({NS, Type}) ->
+ gen_iq_handler:remove_iq_handler(Type, ServerHost, NS)
+ end, dict:fetch_keys(State#state.delegations)).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+-spec get_delegations(binary()) -> ?TDICT.
+get_delegations(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ try gen_server:call(Proc, get_delegations) of
+ {ok, Delegations} -> Delegations
+ catch exit:{noproc, _} ->
+ %% No module is loaded for this virtual host
+ dict:new()
+ end.
+
+-spec process_iq(iq(), ejabberd_local | ejabberd_sm) -> ignore | iq().
+process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) ->
+ LServer = To#jid.lserver,
+ NS = xmpp:get_ns(SubEl),
+ Delegations = get_delegations(LServer),
+ case dict:find({NS, Type}, Delegations) of
+ {ok, {Host, _}} ->
+ Delegation = #delegation{forwarded = #forwarded{sub_els = [IQ]}},
+ NewFrom = jid:make(LServer),
+ NewTo = jid:make(Host),
+ ejabberd_local:route_iq(
+ NewFrom, NewTo,
+ #iq{type = set,
+ from = NewFrom,
+ to = NewTo,
+ sub_els = [Delegation]},
+ fun(Result) -> process_iq_result(IQ, Result) end),
+ ignore;
+ error ->
+ Txt = <<"Failed to map delegated namespace to external component">>,
+ xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
+ end.
+
+-spec process_iq_result(iq(), iq()) -> ok.
+process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ,
+ #iq{type = result} = ResIQ) ->
+ case xmpp:get_subtag(ResIQ, #delegation{}) of
+ #delegation{
+ forwarded = #forwarded{
+ sub_els = [#iq{from = To, to = From,
+ type = Type, id = ID} = Reply]}}
+ when Type == error; Type == result ->
+ ejabberd_router:route(From, To, Reply);
+ _ ->
+ ?ERROR_MSG("got iq-result with invalid delegated "
+ "payload:~n~s", [xmpp:pp(ResIQ)]),
+ Txt = <<"External component failure">>,
+ Err = xmpp:err_internal_server_error(Txt, Lang),
+ ejabberd_router:route_error(To, From, IQ, Err)
+ end;
+process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) ->
+ Err = xmpp:set_from_to(ResIQ, To, From),
+ ejabberd_router:route(To, From, Err);
+process_iq_result(#iq{from = From, to = To, lang = Lang} = IQ, timeout) ->
+ Txt = <<"External component timeout">>,
+ Err = xmpp:err_internal_server_error(Txt, Lang),
+ ejabberd_router:route_error(To, From, IQ, Err).
+
+-spec send_disco_queries(binary(), binary(), binary()) -> ok.
+send_disco_queries(LServer, Host, NS) ->
+ From = jid:make(LServer),
+ To = jid:make(Host),
+ lists:foreach(
+ fun({Type, Node}) ->
+ ejabberd_local:route_iq(
+ From, To, #iq{type = get, from = From, to = To,
+ sub_els = [#disco_info{node = Node}]},
+ fun(#iq{type = result, sub_els = [#disco_info{} = Info]}) ->
+ Proc = gen_mod:get_module_proc(LServer, ?MODULE),
+ gen_server:cast(Proc, {disco_info, Type, Host, NS, Info});
+ (_) ->
+ ok
+ end)
+ end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>},
+ {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]).
+
+-spec disco_features(disco_acc(), jid(), jid(), binary(), binary(),
+ ejabberd_local | ejabberd_sm) -> disco_acc().
+disco_features(Acc, _From, To, <<"">>, _Lang, Type) ->
+ Delegations = get_delegations(To#jid.lserver),
+ Features = my_features(Type) ++
+ lists:flatmap(
+ fun({{_, T}, {_, Info}}) when T == Type ->
+ Info#disco_info.features;
+ (_) ->
+ []
+ end, dict:to_list(Delegations)),
+ case Acc of
+ empty when Features /= [] -> {result, Features};
+ {result, Fs} -> {result, Fs ++ Features};
+ _ -> Acc
+ end;
+disco_features(Acc, _, _, _, _, _) ->
+ Acc.
+
+-spec disco_identity(disco_acc(), jid(), jid(), binary(), binary(),
+ ejabberd_local | ejabberd_sm) -> disco_acc().
+disco_identity(Acc, _From, To, <<"">>, _Lang, Type) ->
+ Delegations = get_delegations(To#jid.lserver),
+ Identities = lists:flatmap(
+ fun({{_, T}, {_, Info}}) when T == Type ->
+ Info#disco_info.identities;
+ (_) ->
+ []
+ end, dict:to_list(Delegations)),
+ case Acc of
+ empty when Identities /= [] -> {result, Identities};
+ {result, Ids} -> {result, Ids ++ Identities};
+ Acc -> Acc
+ end.
+
+my_features(ejabberd_local) -> [?NS_DELEGATION];
+my_features(ejabberd_sm) -> [].
+
+validate_fun() ->
+ fun(L) ->
+ lists:map(
+ fun({NS, Opts}) ->
+ Attrs = proplists:get_value(filtering, Opts, []),
+ Access = proplists:get_value(access, Opts, none),
+ {NS, Attrs, Access}
+ end, L)
+ end.
diff --git a/src/mod_echo.erl b/src/mod_echo.erl
index fe4b8d90d..e7d64dd67 100644
--- a/src/mod_echo.erl
+++ b/src/mod_echo.erl
@@ -63,7 +63,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 1578be964..881587ede 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -101,7 +101,7 @@
-define(AC_ALLOW_HEADERS,
{<<"Access-Control-Allow-Headers">>,
- <<"Content-Type">>}).
+ <<"Content-Type, Authorization, X-Admin">>}).
-define(AC_MAX_AGE,
{<<"Access-Control-Max-Age">>, <<"86400">>}).
@@ -118,9 +118,11 @@
%% -------------------
start(_Host, _Opts) ->
+ ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0),
ok.
stop(_Host) ->
+ ejabberd_access_permissions:unregister_permission_addon(?MODULE),
ok.
depends(_Host, _Opts) ->
@@ -130,79 +132,39 @@ depends(_Host, _Opts) ->
%% basic auth
%% ----------
-check_permissions(Request, Command) ->
- case catch binary_to_existing_atom(Command, utf8) of
- Call when is_atom(Call) ->
- {ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
- check_permissions2(Request, Call, CommandPolicy);
- _ ->
- unauthorized_response()
- end.
-
-check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
- when HTTPAuth /= undefined ->
- Admin =
- case lists:keysearch(<<"X-Admin">>, 1, Headers) of
- {value, {_, <<"true">>}} -> true;
- _ -> false
- end,
- Auth =
- case HTTPAuth of
+extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) ->
+ Info = case HTTPAuth of
{SJID, Pass} ->
case jid:from_string(SJID) of
- #jid{user = User, server = Server} ->
+ #jid{luser = User, lserver = Server} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
- true -> {ok, {User, Server, Pass, Admin}};
- false -> false
+ true ->
+ #{usr => {User, Server, <<"">>}, caller_server => Server};
+ false ->
+ {error, invalid_auth}
end;
_ ->
- false
+ {error, invalid_auth}
end;
{oauth, Token, _} ->
- case oauth_check_token(Call, Token) of
- {ok, user, {User, Server}} ->
- {ok, {User, Server, {oauth, Token}, Admin}};
- {ok, server_admin} -> %% token whas generated using issue_token command line
- {ok, admin};
- false ->
- false
+ case ejabberd_oauth:check_token(Token) of
+ {ok, {U, S}, Scope} ->
+ #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
+ {false, Reason} ->
+ {error, Reason}
end;
_ ->
- false
+ #{}
end,
- case Auth of
- {ok, A} -> {allowed, Call, A};
- _ -> unauthorized_response()
- end;
-check_permissions2(_Request, Call, open) ->
- {allowed, Call, noauth};
-check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
- Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
- fun(V) -> V end,
- none),
- Res = acl:match_rule(global, Access, IP),
- case Res of
- all ->
- {allowed, Call, admin};
- [all] ->
- {allowed, Call, admin};
- allow ->
- {allowed, Call, admin};
- Commands when is_list(Commands) ->
- case lists:member(Call, Commands) of
- true -> {allowed, Call, admin};
- _ -> unauthorized_response()
- end;
- _E ->
- {allowed, Call, noauth}
+ case Info of
+ Map when is_map(Map) ->
+ Map#{caller_module => ?MODULE, ip => IP};
+ _ ->
+ ?DEBUG("Invalid auth data: ~p", [Info]),
+ Info
end;
-check_permissions2(_Request, _Call, _Policy) ->
- unauthorized_response().
-
-oauth_check_token(Scope, Token) when is_atom(Scope) ->
- oauth_check_token(atom_to_binary(Scope, utf8), Token);
-oauth_check_token(Scope, Token) ->
- ejabberd_oauth:check_token(Scope, Token).
+extract_auth(#request{ip = IP}) ->
+ #{ip => IP, caller_module => ?MODULE}.
%% ------------------
%% command processing
@@ -213,31 +175,24 @@ oauth_check_token(Scope, Token) ->
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
badrequest_response(<<"Missing POST data">>);
-process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) ->
+process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
Version = get_api_version(Req),
try
- Args = case jiffy:decode(Data) of
- List when is_list(List) -> List;
- {List} when is_list(List) -> List;
- Other -> [Other]
- end,
+ Args = extract_args(Data),
log(Call, Args, IPPort),
- case check_permissions(Req, Call) of
- {allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args, Version, IP),
- json_response(Code, jiffy:encode(Result));
- %% Warning: check_permission direcly formats 401 reply if not authorized
- ErrorResponse ->
- ErrorResponse
- end
- catch _:{error,{_,invalid_json}} = _Err ->
+ perform_call(Call, Args, Req, Version)
+ catch
+ %% TODO We need to refactor to remove redundant error return formatting
+ throw:{error, unknown_command} ->
+ json_format({404, 44, <<"Command not found.">>});
+ _:{error,{_,invalid_json}} = _Err ->
?DEBUG("Bad Request: ~p", [_Err]),
badrequest_response(<<"Invalid JSON input">>);
_:_Error ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
-process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
+process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
Version = get_api_version(Req),
try
Args = case Data of
@@ -245,23 +200,48 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
_ -> Data
end,
log(Call, Args, IP),
- case check_permissions(Req, Call) of
- {allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args, Version, IP),
- json_response(Code, jiffy:encode(Result));
- %% Warning: check_permission direcly formats 401 reply if not authorized
- ErrorResponse ->
- ErrorResponse
- end
- catch _:_Error ->
+ perform_call(Call, Args, Req, Version)
+ catch
+ %% TODO We need to refactor to remove redundant error return formatting
+ throw:{error, unknown_command} ->
+ json_format({404, 44, <<"Command not found.">>});
+ _:_Error ->
+
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
-process([], #request{method = 'OPTIONS', data = <<>>}) ->
+process([_Call], #request{method = 'OPTIONS', data = <<>>}) ->
{200, ?OPTIONS_HEADER, []};
+process(_, #request{method = 'OPTIONS'}) ->
+ {400, ?OPTIONS_HEADER, []};
process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
- badrequest_response().
+ json_error(400, 40, <<"Missing command name.">>).
+
+perform_call(Command, Args, Req, Version) ->
+ case catch binary_to_existing_atom(Command, utf8) of
+ Call when is_atom(Call) ->
+ case extract_auth(Req) of
+ {error, expired} -> invalid_token_response();
+ {error, not_found} -> invalid_token_response();
+ {error, invalid_auth} -> unauthorized_response();
+ {error, _} -> unauthorized_response();
+ Auth when is_map(Auth) ->
+ Result = handle(Call, Auth, Args, Version),
+ json_format(Result)
+ end;
+ _ ->
+ json_error(404, 40, <<"Endpoint not found.">>)
+ end.
+
+%% Be tolerant to make API more easily usable from command-line pipe.
+extract_args(<<"\n">>) -> [];
+extract_args(Data) ->
+ case jiffy:decode(Data) of
+ List when is_list(List) -> List;
+ {List} when is_list(List) -> List;
+ Other -> [Other]
+ end.
% get API version N from last "vN" element in URL path
get_api_version(#request{path = Path}) ->
@@ -282,8 +262,10 @@ get_api_version([]) ->
%% command handlers
%% ----------------
+%% TODO Check accept types of request before decided format of reply.
+
% generic ejabberd command handler
-handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
+handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
@@ -300,7 +282,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
[{Key, undefined}|Acc]
end, [], ArgsSpec),
try
- handle2(Call, Auth, match(Args2, Spec), Version, IP)
+ handle2(Call, Auth, match(Args2, Spec), Version)
catch throw:not_found ->
{404, <<"not_found">>};
throw:{not_found, Why} when is_atom(Why) ->
@@ -314,7 +296,9 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
throw:{not_allowed, Msg} ->
{401, iolist_to_binary(Msg)};
throw:{error, account_unprivileged} ->
- {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
+ {403, 31, <<"Command need to be run with admin priviledge.">>};
+ throw:{error, access_rules_unauthorized} ->
+ {403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>};
throw:{invalid_parameter, Msg} ->
{400, iolist_to_binary(Msg)};
throw:{error, Why} when is_atom(Why) ->
@@ -337,10 +321,15 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
{400, <<"Error">>}
end.
-handle2(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
+handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF),
- ejabberd_command(Auth, Call, ArgsFormatted, Version, IP).
+ case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
+ {error, Error} ->
+ throw(Error);
+ Res ->
+ format_command_result(Call, Auth, Res, Version)
+ end.
get_elem_delete(A, L) ->
case proplists:get_all_values(A, L) of
@@ -370,28 +359,47 @@ format_args(Args, ArgsFormat) ->
L when is_list(L) -> exit({additional_unused_args, L})
end.
-format_arg({array, Elements},
- {list, {ElementDefName, ElementDefFormat}})
+format_arg({Elements},
+ {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}})
+ when is_list(Elements) andalso
+ (Tuple1S == binary orelse Tuple1S == string) ->
+ lists:map(fun({F1, F2}) ->
+ {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)};
+ ({Val}) when is_list(Val) ->
+ format_arg({Val}, Tuple)
+ end, Elements);
+format_arg(Elements,
+ {list, {_ElementDefName, {list, _} = ElementDefFormat}})
when is_list(Elements) ->
- lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
- ElementDefName == ElementName ->
- format_arg(ElementValue, ElementDefFormat)
- end,
- Elements);
-format_arg({array, [{struct, Elements}]},
- {list, {ElementDefName, ElementDefFormat}})
+ [{format_arg(Element, ElementDefFormat)}
+ || Element <- Elements];
+format_arg(Elements,
+ {list, {_ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
- lists:map(fun ({ElementName, ElementValue}) ->
- true = ElementDefName == ElementName,
- format_arg(ElementValue, ElementDefFormat)
- end,
- Elements);
-format_arg({array, [{struct, Elements}]},
+ [format_arg(Element, ElementDefFormat)
+ || Element <- Elements];
+format_arg({[{Name, Value}]},
+ {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]})
+ when Tuple1S == binary;
+ Tuple1S == string ->
+ {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)};
+format_arg({Elements},
{tuple, ElementsDef})
when is_list(Elements) ->
- FormattedList = format_args(Elements, ElementsDef),
- list_to_tuple(FormattedList);
-format_arg({array, Elements}, {list, ElementsDef})
+ F = lists:map(fun({TElName, TElDef}) ->
+ case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of
+ {_, Value} ->
+ format_arg(Value, TElDef);
+ _ when TElDef == binary; TElDef == string ->
+ <<"">>;
+ _ ->
+ ?ERROR_MSG("missing field ~p in tuple ~p", [TElName, Elements]),
+ throw({invalid_parameter,
+ io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])})
+ end
+ end, ElementsDef),
+ list_to_tuple(F);
+format_arg(Elements, {list, ElementsDef})
when is_list(Elements) and is_atom(ElementsDef) ->
[format_arg(Element, ElementsDef)
|| Element <- Elements];
@@ -405,7 +413,7 @@ format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
throw({invalid_parameter,
- io_lib:format("Arg ~p is not in format ~p",
+ io_lib:format("Arg ~w is not in format ~w",
[Arg, Format])}).
process_unicode_codepoints(Str) ->
@@ -420,18 +428,6 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
-ejabberd_command(Auth, Cmd, Args, Version, IP) ->
- Access = case Auth of
- admin -> [];
- _ -> undefined
- end,
- case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version, #{ip => IP}) of
- {error, Error} ->
- throw(Error);
- Res ->
- format_command_result(Cmd, Auth, Res, Version)
- end.
-
format_command_result(Cmd, Auth, Result, Version) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of
@@ -439,10 +435,12 @@ format_command_result(Cmd, Auth, Result, Version) ->
{200, 0};
{{_, rescode}, _} ->
{200, 1};
- {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
- {200, iolist_to_binary(Text1)};
- {{_, restuple}, {_, Text2}} ->
- {500, iolist_to_binary(Text2)};
+ {_, {error, ErrorAtom, Code, Msg}} ->
+ format_error_result(ErrorAtom, Code, Msg);
+ {{_, restuple}, {V, Text}} when V == true; V == ok ->
+ {200, iolist_to_binary(Text)};
+ {{_, restuple}, {ErrorAtom, Msg}} ->
+ format_error_result(ErrorAtom, 0, Msg);
{{_, {list, _}}, _V} ->
{_, L} = format_result(Result, ResultFormat),
{200, L};
@@ -470,6 +468,11 @@ format_result({Code, Text}, {Name, restuple}) ->
{[{<<"res">>, Code == true orelse Code == ok},
{<<"text">>, iolist_to_binary(Text)}]}};
+format_result(Code, {Name, restuple}) ->
+ {jlib:atom_to_binary(Name),
+ {[{<<"res">>, Code == true orelse Code == ok},
+ {<<"text">>, <<"">>}]}};
+
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
{jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
@@ -488,24 +491,74 @@ format_result(Tuple, {Name, {tuple, Def}}) ->
format_result(404, {_Name, _}) ->
"not_found".
+
+format_error_result(conflict, Code, Msg) ->
+ {409, Code, iolist_to_binary(Msg)};
+format_error_result(_ErrorAtom, Code, Msg) ->
+ {500, Code, iolist_to_binary(Msg)}.
+
unauthorized_response() ->
- unauthorized_response(<<"401 Unauthorized">>).
-unauthorized_response(Body) ->
- json_response(401, jiffy:encode(Body)).
+ json_error(401, 10, <<"You are not authorized to call this command.">>).
+
+invalid_token_response() ->
+ json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
+
+outofscope_response() ->
+ json_error(401, 11, <<"Token does not grant usage to command required scope.">>).
badrequest_response() ->
badrequest_response(<<"400 Bad Request">>).
badrequest_response(Body) ->
json_response(400, jiffy:encode(Body)).
+json_format({Code, Result}) ->
+ json_response(Code, jiffy:encode(Result));
+json_format({HTMLCode, JSONErrorCode, Message}) ->
+ json_error(HTMLCode, JSONErrorCode, Message).
+
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
+%% HTTPCode, JSONCode = integers
+%% message is binary
+json_error(HTTPCode, JSONCode, Message) ->
+ {HTTPCode, ?HEADER(?CT_JSON),
+ jiffy:encode({[{<<"status">>, <<"error">>},
+ {<<"code">>, JSONCode},
+ {<<"message">>, Message}]})
+ }.
+
log(Call, Args, {Addr, Port}) ->
AddrS = jlib:ip_to_list({Addr, Port}),
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
log(Call, Args, IP) ->
?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]).
+permission_addon() ->
+ Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
+ fun(V) -> V end,
+ none),
+ Rules = acl:resolve_access(Access, global),
+ R = lists:filtermap(
+ fun({V, AclRules}) when V == all; V == [all]; V == [allow]; V == allow ->
+ {true, {[{allow, AclRules}], {[<<"*">>], []}}};
+ ({List, AclRules}) when is_list(List) ->
+ {true, {[{allow, AclRules}], {List, []}}};
+ (_) ->
+ false
+ end, Rules),
+ case R of
+ [] ->
+ none;
+ _ ->
+ {_, Res} = lists:foldl(
+ fun({R2, L2}, {Idx, Acc}) ->
+ {Idx+1, [{<<"'mod_http_api admin_ip_access' option compatibility shim ",
+ (integer_to_binary(Idx))/binary>>,
+ {[?MODULE], [{access, R2}], L2}} | Acc]}
+ end, {1, []}, R),
+ Res
+ end.
+
mod_opt_type(admin_ip_access) -> fun acl:access_rules_validator/1;
mod_opt_type(_) -> [admin_ip_access].
diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl
index fa37b801f..9522cd3d4 100644
--- a/src/mod_http_upload_quota.erl
+++ b/src/mod_http_upload_quota.erl
@@ -251,7 +251,7 @@ terminate(Reason, #state{server_host = ServerHost, timers = Timers}) ->
?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]),
ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
- lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers).
+ lists:foreach(fun timer:cancel/1, Timers).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
@@ -299,7 +299,7 @@ enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) ->
{[Path | AccFiles], AccSize + Size, NewSize}
end, {[], 0, 0}, Files),
if OldSize + SlotSize > MaxSize ->
- lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles),
+ lists:foreach(fun del_file_and_dir/1, DelFiles),
file:del_dir(UserDir), % In case it's empty, now.
NewSize + SlotSize;
true ->
@@ -314,7 +314,7 @@ delete_old_files(UserDir, CutOff) ->
[] ->
ok;
OldFiles ->
- lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles),
+ lists:foreach(fun del_file_and_dir/1, OldFiles),
file:del_dir(UserDir) % In case it's empty, now.
end.
diff --git a/src/mod_irc.erl b/src/mod_irc.erl
index fefebcfa3..2fb35414d 100644
--- a/src/mod_irc.erl
+++ b/src/mod_irc.erl
@@ -85,7 +85,7 @@ start(Host, Opts) ->
start_supervisor(Host),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 4c3050df1..3d5c8f64d 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -106,15 +106,12 @@ start(Host, Opts) ->
ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE,
remove_user, 50),
case gen_mod:get_opt(assume_mam_usage, Opts,
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end, never) of
- never ->
- ok;
- _ ->
+ fun(B) when is_boolean(B) -> B end, false) of
+ true ->
ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
- message_is_archived, 50)
+ message_is_archived, 50);
+ false ->
+ ok
end,
ejabberd_commands:register_commands(get_commands_spec()),
ok.
@@ -159,15 +156,12 @@ stop(Host) ->
ejabberd_hooks:delete(anonymous_purge_hook, Host,
?MODULE, remove_user, 50),
case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage,
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end, never) of
- never ->
- ok;
- _ ->
+ fun(B) when is_boolean(B) -> B end, false) of
+ true ->
ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
- message_is_archived, 50)
+ message_is_archived, 50);
+ false ->
+ ok
end,
ejabberd_commands:unregister_commands(get_commands_spec()),
ok.
@@ -367,32 +361,13 @@ message_is_archived(true, _C2SState, _Peer, _JID, _Pkt) ->
true;
message_is_archived(false, C2SState, Peer,
#jid{luser = LUser, lserver = LServer}, Pkt) ->
- Res = case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end, never) of
- if_enabled ->
- case get_prefs(LUser, LServer) of
- #archive_prefs{} = P ->
- {ok, P};
- error ->
- error
- end;
- on_request ->
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- cache_tab:lookup(archive_prefs, {LUser, LServer},
- fun() ->
- Mod:get_prefs(LUser, LServer)
- end);
- never ->
- error
- end,
- case Res of
- {ok, Prefs} ->
+ case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage,
+ fun(B) when is_boolean(B) -> B end, false) of
+ true ->
should_archive(strip_my_archived_tag(Pkt, LServer), LServer)
- andalso should_archive_peer(C2SState, Prefs, Peer);
- error ->
+ andalso should_archive_peer(C2SState, get_prefs(LUser, LServer),
+ Peer);
+ false ->
false
end.
@@ -493,9 +468,10 @@ process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang,
xmpp:make_error(IQ, Err)
end.
-should_archive(#message{type = T}, _LServer) when T == error; T == result ->
+should_archive(#message{type = error}, _LServer) ->
false;
-should_archive(#message{body = Body} = Pkt, LServer) ->
+should_archive(#message{body = Body, subject = Subject,
+ type = Type} = Pkt, LServer) ->
case is_resent(Pkt, LServer) of
true ->
false;
@@ -505,14 +481,11 @@ should_archive(#message{body = Body} = Pkt, LServer) ->
true;
no_store ->
false;
+ none when Type == groupchat; Type == headline ->
+ false;
none ->
- case xmpp:get_text(Body) of
- <<>> ->
- %% Empty body
- false;
- _ ->
- true
- end
+ xmpp:get_text(Body) /= <<>> orelse
+ xmpp:get_text(Subject) /= <<>>
end
end;
should_archive(_, _LServer) ->
@@ -669,9 +642,15 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) ->
case should_archive_peer(C2SState, Prefs, Peer) of
true ->
US = {LUser, LServer},
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- El = xmpp:encode(Pkt),
- Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir);
+ case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
+ [LUser, LServer, Peer, chat, Dir]) of
+ drop ->
+ pass;
+ NewPkt ->
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ El = xmpp:encode(NewPkt),
+ Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir)
+ end;
false ->
pass
end.
@@ -679,11 +658,17 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) ->
store_muc(MUCState, Pkt, RoomJID, Peer, Nick) ->
case should_archive_muc(Pkt) of
true ->
- LServer = MUCState#state.server_host,
{U, S, _} = jid:tolower(RoomJID),
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- El = xmpp:encode(Pkt),
- Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv);
+ LServer = MUCState#state.server_host,
+ case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
+ [U, S, Peer, groupchat, recv]) of
+ drop ->
+ pass;
+ NewPkt ->
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ El = xmpp:encode(NewPkt),
+ Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv)
+ end;
false ->
pass
end.
@@ -879,6 +864,7 @@ is_bare_copy(#jid{luser = U, lserver = S, lresource = R}, To) ->
send(Msgs, Count, IsComplete,
#iq{from = From, to = To,
sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) ->
+ Hint = #hint{type = 'no-store'},
Els = lists:map(
fun({ID, _IDInt, El}) ->
#message{sub_els = [#mam_result{xmlns = NS,
@@ -905,7 +891,7 @@ send(Msgs, Count, IsComplete,
fun(El) ->
ejabberd_router:route(To, From, El)
end, Els),
- ejabberd_router:route(To, From, #message{sub_els = [Result]}),
+ ejabberd_router:route(To, From, #message{sub_els = [Result, Hint]}),
ignore
end.
@@ -926,6 +912,8 @@ filter_by_max(_Msgs, _Junk) ->
-spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined.
limit_max(RSM, ?NS_MAM_TMP) ->
RSM; % XEP-0313 v0.2 doesn't require clients to support RSM.
+limit_max(undefined, _NS) ->
+ #rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) ->
RSM#rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE ->
@@ -972,10 +960,7 @@ get_commands_spec() ->
result = {res, rescode}}].
mod_opt_type(assume_mam_usage) ->
- fun(if_enabled) -> if_enabled;
- (on_request) -> on_request;
- (never) -> never
- end;
+ fun (B) when is_boolean(B) -> B end;
mod_opt_type(cache_life_time) ->
fun (I) when is_integer(I), I > 0 -> I end;
mod_opt_type(cache_size) ->
diff --git a/src/mod_mix.erl b/src/mod_mix.erl
index 7ca09f4db..f7bd0ec9a 100644
--- a/src/mod_mix.erl
+++ b/src/mod_mix.erl
@@ -43,7 +43,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 5000, worker, [?MODULE]},
+ transient, 5000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 9c17643b8..ea8bff5e3 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -71,13 +71,12 @@
server_host = <<"">> :: binary(),
access = {none, none, none, none} :: {atom(), atom(), atom(), atom()},
history_size = 20 :: non_neg_integer(),
+ max_rooms_discoitems = 100 :: non_neg_integer(),
default_room_opts = [] :: list(),
room_shaper = none :: shaper:shaper()}).
-define(PROCNAME, ejabberd_mod_muc).
--define(MAX_ROOMS_DISCOITEMS, 100).
-
-type muc_room_opts() :: [{atom(), any()}].
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass.
@@ -100,7 +99,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
@@ -196,6 +195,9 @@ init([Host, Opts]) ->
HistorySize = gen_mod:get_opt(history_size, Opts,
fun(I) when is_integer(I), I>=0 -> I end,
20),
+ MaxRoomsDiscoItems = gen_mod:get_opt(max_rooms_discoitems, Opts,
+ fun(I) when is_integer(I), I>=0 -> I end,
+ 100),
DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts,
fun(L) when is_list(L) -> L end,
[]),
@@ -221,6 +223,7 @@ init([Host, Opts]) ->
public -> Bool;
public_list -> Bool;
mam -> Bool;
+ allow_subscription -> Bool;
password -> fun iolist_to_binary/1;
title -> fun iolist_to_binary/1;
allow_private_messages_from_visitors ->
@@ -272,6 +275,7 @@ init([Host, Opts]) ->
access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
default_room_opts = DefRoomOpts,
history_size = HistorySize,
+ max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper}}.
handle_call(stop, _From, State) ->
@@ -300,9 +304,10 @@ handle_info({route, From, To, Packet},
#state{host = Host, server_host = ServerHost,
access = Access, default_room_opts = DefRoomOpts,
history_size = HistorySize,
+ max_rooms_discoitems = MaxRoomsDiscoItems,
room_shaper = RoomShaper} = State) ->
case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts) of
+ From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) of
{'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]);
_ ->
@@ -339,12 +344,12 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%--------------------------------------------------------------------
do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts) ->
+ From, To, Packet, DefRoomOpts, _MaxRoomsDiscoItems) ->
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
case acl:match_rule(ServerHost, AccessRoute, From) of
allow ->
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
- From, To, Packet, DefRoomOpts);
+ From, To, Packet, DefRoomOpts);
deny ->
Lang = xmpp:get_lang(Packet),
ErrText = <<"Access denied by service policy">>,
@@ -487,9 +492,13 @@ process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
process_disco_items(#iq{type = get, from = From, to = To, lang = Lang,
sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) ->
Host = To#jid.lserver,
- xmpp:make_iq_result(
- IQ, #disco_items{node = Node,
- items = iq_disco_items(Host, From, Lang, Node, RSM)});
+ ServerHost = ejabberd_router:host_of_route(Host),
+ MaxRoomsDiscoItems = gen_mod:get_module_opt(
+ ServerHost, ?MODULE, max_rooms_discoitems,
+ fun(I) when is_integer(I), I>=0 -> I end,
+ 100),
+ Items = iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, Node, RSM),
+ xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items});
process_disco_items(#iq{lang = Lang} = IQ) ->
Txt = <<"No module is handling this query">>,
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
@@ -588,23 +597,23 @@ register_room(Host, Room, Pid) ->
end,
mnesia:transaction(F).
-iq_disco_items(Host, From, Lang, <<"">>, undefined) ->
+iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"">>, undefined) ->
Rooms = get_vh_rooms(Host),
- case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of
+ case erlang:length(Rooms) < MaxRoomsDiscoItems of
true ->
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang});
false ->
- iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined)
+ iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"nonemptyrooms">>, undefined)
end;
-iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, undefined) ->
+iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"nonemptyrooms">>, undefined) ->
Empty = #disco_item{jid = jid:make(<<"conference.localhost">>),
node = <<"emptyrooms">>,
name = translate:translate(Lang, <<"Empty Rooms">>)},
Query = {get_disco_item, only_non_empty, From, Lang},
[Empty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)];
-iq_disco_items(Host, From, Lang, <<"emptyrooms">>, undefined) ->
+iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"emptyrooms">>, undefined) ->
iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang});
-iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) ->
+iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, _DiscoNode, Rsm) ->
{Rooms, RsmO} = get_vh_rooms(Host, Rsm),
RsmOut = jlib:rsm_encode(RsmO),
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut.
@@ -624,47 +633,47 @@ iq_disco_items_list(Host, Rooms, Query) ->
get_vh_rooms(_, _) ->
todo.
-%% get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
-%% AllRooms = lists:sort(get_vh_rooms(Host)),
-%% Count = erlang:length(AllRooms),
-%% Guard = case Direction of
-%% _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}];
-%% aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}];
-%% before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}];
-%% _ -> [{'==', {element, 2, '$1'}, Host}]
-%% end,
-%% L = lists:sort(
-%% mnesia:dirty_select(muc_online_room,
-%% [{#muc_online_room{name_host = '$1', _ = '_'},
-%% Guard,
-%% ['$_']}])),
-%% L2 = if
-%% Index == undefined andalso Direction == before ->
-%% lists:reverse(lists:sublist(lists:reverse(L), 1, M));
-%% Index == undefined ->
-%% lists:sublist(L, 1, M);
-%% Index > Count orelse Index < 0 ->
-%% [];
-%% true ->
-%% lists:sublist(L, Index+1, M)
-%% end,
-%% if L2 == [] -> {L2, #rsm_out{count = Count}};
-%% true ->
-%% H = hd(L2),
-%% NewIndex = get_room_pos(H, AllRooms),
-%% T = lists:last(L2),
-%% {F, _} = H#muc_online_room.name_host,
-%% {Last, _} = T#muc_online_room.name_host,
-%% {L2,
-%% #rsm_out{first = F, last = Last, count = Count,
-%% index = NewIndex}}
-%% end.
+ %% AllRooms = lists:sort(get_vh_rooms(Host)),
+ %% Count = erlang:length(AllRooms),
+ %% Guard = case Direction of
+ %% _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}];
+ %% aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}];
+ %% before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}];
+ %% _ -> [{'==', {element, 2, '$1'}, Host}]
+ %% end,
+ %% L = lists:sort(
+ %% mnesia:dirty_select(muc_online_room,
+ %% [{#muc_online_room{name_host = '$1', _ = '_'},
+ %% Guard,
+ %% ['$_']}])),
+ %% L2 = if
+ %% Index == undefined andalso Direction == before ->
+ %% lists:reverse(lists:sublist(lists:reverse(L), 1, M));
+ %% Index == undefined ->
+ %% lists:sublist(L, 1, M);
+ %% Index > Count orelse Index < 0 ->
+ %% [];
+ %% true ->
+ %% lists:sublist(L, Index+1, M)
+ %% end,
+ %% if L2 == [] -> {L2, #rsm_out{count = Count}};
+ %% true ->
+ %% H = hd(L2),
+ %% NewIndex = get_room_pos(H, AllRooms),
+ %% T = lists:last(L2),
+ %% {F, _} = H#muc_online_room.name_host,
+ %% {Last, _} = T#muc_online_room.name_host,
+ %% {L2,
+ %% #rsm_out{first = F, last = Last, count = Count,
+ %% index = NewIndex}}
+ %% end.
get_subscribed_rooms(_ServerHost, Host, From) ->
Rooms = get_vh_rooms(Host),
+ BareFrom = jid:remove_resource(From),
lists:flatmap(
fun(#muc_online_room{name_host = {Name, _}, pid = Pid}) ->
- case gen_fsm:sync_send_all_state_event(Pid, {is_subscriber, From}) of
+ case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of
true -> [jid:make(Name, Host)];
false -> []
end;
@@ -874,6 +883,8 @@ mod_opt_type(max_room_id) ->
fun (infinity) -> infinity;
(I) when is_integer(I), I > 0 -> I
end;
+mod_opt_type(max_rooms_discoitems) ->
+ fun (I) when is_integer(I), I >= 0 -> I end;
mod_opt_type(regexp_room_id) ->
fun iolist_to_binary/1;
mod_opt_type(max_room_name) ->
@@ -901,8 +912,8 @@ mod_opt_type(user_presence_shaper) ->
mod_opt_type(_) ->
[access, access_admin, access_create, access_persistent,
db_type, default_room_options, history_size, host,
- max_room_desc, max_room_id, max_room_name, regexp_room_id,
- max_user_conferences, max_users,
+ max_room_desc, max_room_id, max_room_name,
+ max_rooms_discoitems, max_user_conferences, max_users,
max_users_admin_threshold, max_users_presence,
min_message_interval, min_presence_interval,
- room_shaper, user_message_shaper, user_presence_shaper].
+ regexp_room_id, room_shaper, user_message_shaper, user_presence_shaper].
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 4d56093e1..8f1f649d2 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -13,6 +13,7 @@
-export([start/2, stop/1, depends/2, muc_online_rooms/1,
muc_unregister_nick/1, create_room/3, destroy_room/2,
+ create_room_with_opts/4,
create_rooms_file/1, destroy_rooms_file/1,
rooms_unused_list/2, rooms_unused_destroy/2,
get_user_rooms/2, get_room_occupants/2,
@@ -20,6 +21,7 @@
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2,
web_menu_main/2, web_page_main/2, web_menu_host/3,
+ subscribe_room/4, unsubscribe_room/2, get_subscribers/2,
web_page_host/3, mod_opt_type/1, get_commands_spec/0]).
-include("ejabberd.hrl").
@@ -87,6 +89,18 @@ get_commands_spec() ->
module = ?MODULE, function = create_rooms_file,
args = [{file, string}],
result = {res, rescode}},
+ #ejabberd_commands{name = create_room_with_opts, tags = [muc_room],
+ desc = "Create a MUC room name@service in host with given options",
+ module = ?MODULE, function = create_room_with_opts,
+ args = [{name, binary}, {service, binary},
+ {host, binary},
+ {options, {list,
+ {option, {tuple,
+ [{name, binary},
+ {value, binary}
+ ]}}
+ }}],
+ result = {res, rescode}},
#ejabberd_commands{name = destroy_rooms_file, tags = [muc],
desc = "Destroy the rooms indicated in file",
longdesc = "Provide one room JID per line.",
@@ -151,7 +165,22 @@ get_commands_spec() ->
{value, string}
]}}
}}},
-
+ #ejabberd_commands{name = subscribe_room, tags = [muc_room],
+ desc = "Subscribe to a MUC conference",
+ module = ?MODULE, function = subscribe_room,
+ args = [{user, binary}, {nick, binary}, {room, binary},
+ {nodes, binary}],
+ result = {nodes, {list, {node, string}}}},
+ #ejabberd_commands{name = unsubscribe_room, tags = [muc_room],
+ desc = "Unsubscribe from a MUC conference",
+ module = ?MODULE, function = unsubscribe_room,
+ args = [{user, binary}, {room, binary}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = get_subscribers, tags = [muc_room],
+ desc = "List subscribers of a MUC conference",
+ module = ?MODULE, function = get_subscribers,
+ args = [{name, binary}, {service, binary}],
+ result = {subscribers, {list, {jid, string}}}},
#ejabberd_commands{name = set_room_affiliation, tags = [muc_room],
desc = "Change an affiliation in a MUC room",
module = ?MODULE, function = set_room_affiliation,
@@ -400,15 +429,23 @@ prepare_room_info(Room_info) ->
%% ok | error
%% @doc Create a room immediately with the default options.
create_room(Name1, Host1, ServerHost) ->
- Name = jid:nodeprep(Name1),
- Host = jid:nodeprep(Host1),
+ create_room_with_opts(Name1, Host1, ServerHost, []).
+
+create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) ->
+ true = (error /= (Name = jid:nodeprep(Name1))),
+ true = (error /= (Host = jid:nodeprep(Host1))),
%% Get the default room options from the muc configuration
DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc,
default_room_options, fun(X) -> X end, []),
+ %% Change default room options as required
+ FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts],
+ RoomOpts = lists:ukeymerge(1,
+ lists:keysort(1, FormattedRoomOpts),
+ lists:keysort(1, DefRoomOpts)),
%% Store the room on the server, it is not started yet though at this point
- mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts),
+ mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
%% Get all remaining mod_muc parameters that might be utilized
Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all),
@@ -429,7 +466,7 @@ create_room(Name1, Host1, ServerHost) ->
Name,
HistorySize,
RoomShaper,
- DefRoomOpts),
+ RoomOpts),
{atomic, ok} = register_room(Host, Name, Pid),
ok;
_ ->
@@ -477,7 +514,7 @@ destroy_room({N, H, SH}) ->
%% The file encoding must be UTF-8
destroy_rooms_file(Filename) ->
- {ok, F} = file:open(Filename, [read, binary]),
+ {ok, F} = file:open(Filename, [read]),
RJID = read_room(F),
Rooms = read_rooms(F, RJID, []),
file:close(F),
@@ -496,7 +533,7 @@ read_room(F) ->
eof -> eof;
String ->
case io_lib:fread("~s", String) of
- {ok, [RoomJID], _} -> split_roomjid(RoomJID);
+ {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID));
{error, What} ->
io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String])
end
@@ -514,7 +551,7 @@ split_roomjid(RoomJID) ->
%%----------------------------
create_rooms_file(Filename) ->
- {ok, F} = file:open(Filename, [read, binary]),
+ {ok, F} = file:open(Filename, [read]),
RJID = read_room(F),
Rooms = read_rooms(F, RJID, []),
file:close(F),
@@ -748,12 +785,20 @@ send_direct_invitation(FromJid, UserJid, XmlEl) ->
%% the option to change (for example title or max_users),
%% and the value to assign to the new option.
%% For example:
-%% change_room_option("testroom", "conference.localhost", "title", "Test Room")
-change_room_option(Name, Service, Option, Value) when is_atom(Option) ->
- Pid = get_room_pid(Name, Service),
- {ok, _} = change_room_option(Pid, Option, Value),
- ok;
+%% change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)
change_room_option(Name, Service, OptionString, ValueString) ->
+ case get_room_pid(Name, Service) of
+ room_not_found ->
+ room_not_found;
+ Pid ->
+ {Option, Value} = format_room_option(OptionString, ValueString),
+ Config = get_room_config(Pid),
+ Config2 = change_option(Option, Value, Config),
+ {ok, _} = gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}),
+ ok
+ end.
+
+format_room_option(OptionString, ValueString) ->
Option = jlib:binary_to_atom(OptionString),
Value = case Option of
title -> ValueString;
@@ -764,12 +809,7 @@ change_room_option(Name, Service, OptionString, ValueString) ->
max_users -> binary_to_integer(ValueString);
_ -> jlib:binary_to_atom(ValueString)
end,
- change_room_option(Name, Service, Option, Value).
-
-change_room_option(Pid, Option, Value) ->
- Config = get_room_config(Pid),
- Config2 = change_option(Option, Value, Config),
- gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}).
+ {Option, Value}.
%% @doc Get the Pid of an existing MUC room, or 'room_not_found'.
get_room_pid(Name, Service) ->
@@ -789,6 +829,7 @@ change_option(Option, Value, Config) ->
allow_private_messages -> Config#config{allow_private_messages = Value};
allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value};
allow_query_users -> Config#config{allow_query_users = Value};
+ allow_subscription -> Config#config{allow_subscription = Value};
allow_user_invites -> Config#config{allow_user_invites = Value};
allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value};
allow_visitor_status -> Config#config{allow_visitor_status = Value};
@@ -884,6 +925,74 @@ set_room_affiliation(Name, Service, JID, AffiliationString) ->
error
end.
+%%%
+%%% MUC Subscription
+%%%
+
+subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> ->
+ throw({error, "Nickname must be set"});
+subscribe_room(User, Nick, Room, Nodes) ->
+ NodeList = re:split(Nodes, "\\h*,\\h*"),
+ case jid:from_string(Room) of
+ #jid{luser = Name, lserver = Host} when Name /= <<"">> ->
+ case jid:from_string(User) of
+ error ->
+ throw({error, "Malformed user JID"});
+ #jid{lresource = <<"">>} ->
+ throw({error, "User's JID should have a resource"});
+ UserJID ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ case gen_fsm:sync_send_all_state_event(
+ Pid,
+ {muc_subscribe, UserJID, Nick, NodeList}) of
+ {ok, SubscribedNodes} ->
+ SubscribedNodes;
+ {error, Reason} ->
+ throw({error, binary_to_list(Reason)})
+ end;
+ _ ->
+ throw({error, "The room does not exist"})
+ end
+ end;
+ _ ->
+ throw({error, "Malformed room JID"})
+ end.
+
+unsubscribe_room(User, Room) ->
+ case jid:from_string(Room) of
+ #jid{luser = Name, lserver = Host} when Name /= <<"">> ->
+ case jid:from_string(User) of
+ error ->
+ throw({error, "Malformed user JID"});
+ UserJID ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ case gen_fsm:sync_send_all_state_event(
+ Pid,
+ {muc_unsubscribe, UserJID}) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ throw({error, binary_to_list(Reason)})
+ end;
+ _ ->
+ throw({error, "The room does not exist"})
+ end
+ end;
+ _ ->
+ throw({error, "Malformed room JID"})
+ end.
+
+get_subscribers(Name, Host) ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ {ok, JIDList} = gen_fsm:sync_send_all_state_event(Pid, get_subscribers),
+ [jid:to_string(jid:remove_resource(J)) || J <- JIDList];
+ _ ->
+ throw({error, "The room does not exist"})
+ end.
+
make_opts(StateData) ->
Config = StateData#state.config,
[
diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl
index 4b129ce81..5cf52e60f 100644
--- a/src/mod_muc_log.erl
+++ b/src/mod_muc_log.erl
@@ -81,7 +81,7 @@ start_link(Host, Opts) ->
start(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
- temporary, 1000, worker, [?MODULE]},
+ transient, 1000, worker, [?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index ce6851bc5..c83565734 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -142,6 +142,7 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) ->
normal_state({route, From, <<"">>,
#message{type = Type, lang = Lang} = Packet}, StateData) ->
case is_user_online(From, StateData) orelse
+ is_subscriber(From, StateData) orelse
is_user_allowed_message_nonparticipant(From, StateData) of
true when Type == groupchat ->
Activity = get_user_activity(From, StateData),
@@ -372,7 +373,8 @@ normal_state({route, From, ToNick,
{next_state, normal_state, StateData};
continue_delivery ->
case {(StateData#state.config)#config.allow_private_messages,
- is_user_online(From, StateData)} of
+ is_user_online(From, StateData) orelse
+ is_subscriber(From, StateData)} of
{true, true} when Type == groupchat ->
ErrText = <<"It is not allowed to send private messages "
"of type \"groupchat\"">>,
@@ -397,9 +399,7 @@ normal_state({route, From, ToNick,
PmFromVisitors == anyone;
(PmFromVisitors == moderators) and
DstIsModerator ->
- {ok, #user{nick = FromNick}} =
- (?DICT):find(jid:tolower(From),
- StateData#state.users),
+ {FromNick, _} = get_participant_data(From, StateData),
FromNickJID =
jid:replace_resource(StateData#state.jid,
FromNick),
@@ -476,7 +476,7 @@ handle_event({service_message, Msg}, _StateName,
MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)},
send_wrapped_multiple(
StateData#state.jid,
- StateData#state.users,
+ get_users_and_subscribers(StateData),
MessagePkt,
?NS_MUCSUB_NODES_MESSAGES,
StateData),
@@ -538,8 +538,59 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
NSD ->
{reply, {ok, NSD}, StateName, NSD}
end;
-handle_sync_event({is_subscriber, From}, _From, StateName, StateData) ->
- {reply, is_subscriber(From, StateData), StateName, StateData};
+handle_sync_event(get_subscribers, _From, StateName, StateData) ->
+ JIDs = lists:map(fun jid:make/1,
+ ?DICT:fetch_keys(StateData#state.subscribers)),
+ {reply, {ok, JIDs}, StateName, StateData};
+handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
+ StateName, StateData) ->
+ IQ = #iq{type = set, id = randoms:get_string(),
+ from = From, sub_els = [#muc_subscribe{nick = Nick,
+ events = Nodes}]},
+ Config = StateData#state.config,
+ CaptchaRequired = Config#config.captcha_protected,
+ PasswordProtected = Config#config.password_protected,
+ TmpConfig = Config#config{captcha_protected = false,
+ password_protected = false},
+ TmpState = StateData#state{config = TmpConfig},
+ case process_iq_mucsub(From, IQ, TmpState) of
+ {result, #muc_subscribe{events = NewNodes}, NewState} ->
+ NewConfig = (NewState#state.config)#config{
+ captcha_protected = CaptchaRequired,
+ password_protected = PasswordProtected},
+ {reply, {ok, NewNodes}, StateName,
+ NewState#state{config = NewConfig}};
+ {ignore, NewState} ->
+ NewConfig = (NewState#state.config)#config{
+ captcha_protected = CaptchaRequired,
+ password_protected = PasswordProtected},
+ {reply, {error, <<"Requrest is ignored">>},
+ NewState#state{config = NewConfig}};
+ {error, Err, NewState} ->
+ NewConfig = (NewState#state.config)#config{
+ captcha_protected = CaptchaRequired,
+ password_protected = PasswordProtected},
+ {reply, {error, get_error_text(Err)}, StateName,
+ NewState#state{config = NewConfig}};
+ {error, Err} ->
+ {reply, {error, get_error_text(Err)}, StateName, StateData}
+ end;
+handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) ->
+ IQ = #iq{type = set, id = randoms:get_string(),
+ from = From, sub_els = [#muc_unsubscribe{}]},
+ case process_iq_mucsub(From, IQ, StateData) of
+ {result, _, NewState} ->
+ {reply, ok, StateName, NewState};
+ {ignore, NewState} ->
+ {reply, {error, <<"Requrest is ignored">>}, NewState};
+ {error, Err, NewState} ->
+ {reply, {error, get_error_text(Err)}, StateName, NewState};
+ {error, Err} ->
+ {reply, {error, get_error_text(Err)}, StateName, StateData}
+ end;
+handle_sync_event({is_subscribed, From}, _From, StateName, StateData) ->
+ IsSubs = ?DICT:is_key(jid:split(From), StateData#state.subscribers),
+ {reply, IsSubs, StateName, StateData};
handle_sync_event(_Event, _From, StateName,
StateData) ->
Reply = ok, {reply, Reply, StateName, StateData}.
@@ -654,7 +705,7 @@ terminate(Reason, _StateName, StateData) ->
end,
tab_remove_online_user(LJID, StateData)
end,
- [], StateData#state.users),
+ [], get_users_and_subscribers(StateData)),
add_to_log(room_existence, stopped, StateData),
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
StateData#state.server_host),
@@ -669,11 +720,12 @@ route(Pid, From, ToNick, Packet) ->
-spec process_groupchat_message(jid(), message(), state()) -> fsm_next().
process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
- case is_user_online(From, StateData) orelse
+ IsSubscriber = is_subscriber(From, StateData),
+ case is_user_online(From, StateData) orelse IsSubscriber orelse
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
- {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData),
+ {FromNick, Role} = get_participant_data(From, StateData),
if (Role == moderator) or (Role == participant) or IsSubscriber or
((StateData#state.config)#config.moderated == false) ->
Subject = check_subject(Packet),
@@ -682,6 +734,7 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
_ ->
case
can_change_subject(Role,
+ IsSubscriber,
StateData)
of
true ->
@@ -716,7 +769,7 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
end,
send_wrapped_multiple(
jid:replace_resource(StateData#state.jid, FromNick),
- StateData#state.users,
+ get_users_and_subscribers(StateData),
NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
@@ -907,14 +960,21 @@ is_user_allowed_message_nonparticipant(JID,
%% @doc Get information of this participant, or default values.
%% If the JID is not a participant, return values for a service message.
--spec get_participant_data(jid(), state()) -> {binary(), role(), boolean()}.
+-spec get_participant_data(jid(), state()) -> {binary(), role()}.
get_participant_data(From, StateData) ->
case (?DICT):find(jid:tolower(From),
StateData#state.users)
of
- {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} ->
- {FromNick, Role, IsSubscriber};
- error -> {<<"">>, moderator, false}
+ {ok, #user{nick = FromNick, role = Role}} ->
+ {FromNick, Role};
+ error ->
+ case ?DICT:find(jid:tolower(jid:remove_resource(From)),
+ StateData#state.subscribers) of
+ {ok, #subscriber{nick = FromNick}} ->
+ {FromNick, none};
+ error ->
+ {<<"">>, moderator}
+ end
end.
-spec process_presence(jid(), binary(), presence(), state()) -> fsm_transition().
@@ -979,32 +1039,19 @@ do_process_presence(From, Nick, #presence{type = available, lang = Lang} = Packe
From, Packet, Err),
StateData;
_ ->
- case is_initial_presence(From, StateData) of
- true ->
- subscriber_becomes_available(
- From, Nick, Packet, StateData);
- false ->
change_nick(From, Nick, StateData)
- end
end;
- _NotNickChange ->
- case is_initial_presence(From, StateData) of
- true ->
- subscriber_becomes_available(
- From, Nick, Packet, StateData);
- false ->
- Stanza = maybe_strip_status_from_presence(
- From, Packet, StateData),
- NewState = add_user_presence(From, Stanza,
- StateData),
- send_new_presence(From, NewState, StateData),
- NewState
- end
+ false ->
+ Stanza = maybe_strip_status_from_presence(
+ From, Packet, StateData),
+ NewState = add_user_presence(From, Stanza,
+ StateData),
+ send_new_presence(From, NewState, StateData),
+ NewState
end
end;
do_process_presence(From, Nick, #presence{type = unavailable} = Packet,
StateData) ->
- IsSubscriber = is_subscriber(From, StateData),
NewPacket = case {(StateData#state.config)#config.allow_visitor_status,
is_visitor(From, StateData)} of
{false, true} ->
@@ -1017,7 +1064,7 @@ do_process_presence(From, Nick, #presence{type = unavailable} = Packet,
_ -> send_new_presence(From, NewState, StateData)
end,
Reason = xmpp:get_text(NewPacket#presence.status),
- remove_online_user(From, NewState, IsSubscriber, Reason);
+ remove_online_user(From, NewState, Reason);
do_process_presence(From, _Nick, #presence{type = error, lang = Lang} = Packet,
StateData) ->
ErrorText = <<"It is not allowed to send error messages to the"
@@ -1036,24 +1083,11 @@ maybe_strip_status_from_presence(From, Packet, StateData) ->
_Allowed -> Packet
end.
--spec subscriber_becomes_available(jid(), binary(), presence(),
- state()) -> state().
-subscriber_becomes_available(From, Nick, Packet, StateData) ->
- Stanza = maybe_strip_status_from_presence(From, Packet, StateData),
- State1 = add_user_presence(From, Stanza, StateData),
- Aff = get_affiliation(From, State1),
- Role = get_default_role(Aff, State1),
- State2 = set_role(From, Role, State1),
- State3 = set_nick(From, Nick, State2),
- send_existing_presences(From, State3),
- send_initial_presence(From, State3, StateData),
- State3.
-
-spec close_room_if_temporary_and_empty(state()) -> fsm_transition().
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
- andalso (?DICT):size(StateData1#state.users) == 0
- of
+ andalso (?DICT):size(StateData1#state.users) == 0
+ andalso (?DICT):size(StateData1#state.subscribers) == 0 of
true ->
?INFO_MSG("Destroyed MUC room ~s because it's temporary "
"and empty",
@@ -1063,6 +1097,32 @@ close_room_if_temporary_and_empty(StateData1) ->
_ -> {next_state, normal_state, StateData1}
end.
+get_users_and_subscribers(StateData) ->
+ OnlineSubscribers = ?DICT:fold(
+ fun(LJID, _, Acc) ->
+ LBareJID = jid:remove_resource(LJID),
+ case is_subscriber(LBareJID, StateData) of
+ true ->
+ ?SETS:add_element(LBareJID, Acc);
+ false ->
+ Acc
+ end
+ end, ?SETS:new(), StateData#state.users),
+ ?DICT:fold(
+ fun(LBareJID, #subscriber{nick = Nick}, Acc) ->
+ case ?SETS:is_element(LBareJID, OnlineSubscribers) of
+ false ->
+ ?DICT:store(LBareJID,
+ #user{jid = jid:make(LBareJID),
+ nick = Nick,
+ role = none,
+ last_presence = undefined},
+ Acc);
+ true ->
+ Acc
+ end
+ end, StateData#state.users, StateData#state.subscribers).
+
-spec is_user_online(jid(), state()) -> boolean().
is_user_online(JID, StateData) ->
LJID = jid:tolower(JID),
@@ -1070,13 +1130,8 @@ is_user_online(JID, StateData) ->
-spec is_subscriber(jid(), state()) -> boolean().
is_subscriber(JID, StateData) ->
- LJID = jid:tolower(JID),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{is_subscriber = IsSubscriber}} ->
- IsSubscriber;
- _ ->
- false
- end.
+ LJID = jid:tolower(jid:remove_resource(JID)),
+ (?DICT):is_key(LJID, StateData#state.subscribers).
%% Check if the user is occupant of the room, or at least is an admin or owner.
-spec is_occupant_or_admin(jid(), state()) -> boolean().
@@ -1201,6 +1256,14 @@ get_error_condition(#stanza_error{reason = Reason}) ->
get_error_condition(undefined) ->
"undefined".
+get_error_text(Error) ->
+ case fxml:get_subtag_with_xmlns(Error, <<"text">>, ?NS_STANZAS) of
+ #xmlel{} = Tag ->
+ fxml:get_tag_cdata(Tag);
+ false ->
+ <<"">>
+ end.
+
-spec make_reason(stanza(), jid(), state(), binary()) -> binary().
make_reason(Packet, From, StateData, Reason1) ->
{ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users),
@@ -1210,14 +1273,13 @@ make_reason(Packet, From, StateData, Reason1) ->
-spec expulse_participant(stanza(), jid(), state(), binary()) ->
state().
expulse_participant(Packet, From, StateData, Reason1) ->
- IsSubscriber = is_subscriber(From, StateData),
Reason2 = make_reason(Packet, From, StateData, Reason1),
NewState = add_user_presence_un(From,
#presence{type = unavailable,
status = xmpp:mk_text(Reason2)},
StateData),
send_new_presence(From, NewState, StateData),
- remove_online_user(From, NewState, IsSubscriber).
+ remove_online_user(From, NewState).
-spec set_affiliation(jid(), affiliation(), state()) -> state().
set_affiliation(JID, Affiliation, StateData) ->
@@ -1514,8 +1576,7 @@ prepare_room_queue(StateData) ->
end.
-spec update_online_user(jid(), #user{}, state()) -> state().
-update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
- is_subscriber = IsSubscriber} = User, StateData) ->
+update_online_user(JID, #user{nick = Nick} = User, StateData) ->
LJID = jid:tolower(JID),
Nicks1 = case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{nick = OldNick}} ->
@@ -1534,9 +1595,7 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
[LJID], Nicks1),
Users = (?DICT):update(LJID,
fun(U) ->
- U#user{nick = Nick,
- subscriptions = Nodes,
- is_subscriber = IsSubscriber}
+ U#user{nick = Nick}
end, User, StateData#state.users),
NewStateData = StateData#state{users = Users, nicks = Nicks},
case {?DICT:find(LJID, StateData#state.users),
@@ -1548,35 +1607,32 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
end,
NewStateData.
--spec add_online_user(jid(), binary(), role(), boolean(), [binary()], state()) -> state().
-add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) ->
+set_subscriber(JID, Nick, Nodes, StateData) ->
+ BareJID = jid:remove_resource(JID),
+ LBareJID = jid:tolower(BareJID),
+ Subscribers = ?DICT:store(LBareJID,
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ StateData#state.subscribers),
+ Nicks = ?DICT:store(Nick, [LBareJID], StateData#state.subscriber_nicks),
+ NewStateData = StateData#state{subscribers = Subscribers,
+ subscriber_nicks = Nicks},
+ store_room(NewStateData),
+ NewStateData.
+
+-spec add_online_user(jid(), binary(), role(), state()) -> state().
+add_online_user(JID, Nick, Role, StateData) ->
tab_add_online_user(JID, StateData),
- User = #user{jid = JID, nick = Nick, role = Role,
- is_subscriber = IsSubscriber, subscriptions = Nodes},
- StateData1 = update_online_user(JID, User, StateData),
- if IsSubscriber ->
- store_room(StateData1);
- true ->
- ok
- end,
- StateData1.
+ User = #user{jid = JID, nick = Nick, role = Role},
+ update_online_user(JID, User, StateData).
--spec remove_online_user(jid(), state(), boolean()) -> state().
-remove_online_user(JID, StateData, IsSubscriber) ->
- remove_online_user(JID, StateData, IsSubscriber, <<"">>).
+-spec remove_online_user(jid(), state()) -> state().
+remove_online_user(JID, StateData) ->
+ remove_online_user(JID, StateData, <<"">>).
--spec remove_online_user(jid(), state(), boolean(), binary()) -> state().
-remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) ->
- LJID = jid:tolower(JID),
- Users = case (?DICT):find(LJID, StateData#state.users) of
- {ok, U} ->
- (?DICT):store(LJID, U#user{last_presence = undefined},
- StateData#state.users);
- error ->
- StateData#state.users
- end,
- StateData#state{users = Users};
-remove_online_user(JID, StateData, _IsSubscriber, Reason) ->
+-spec remove_online_user(jid(), state(), binary()) -> state().
+remove_online_user(JID, StateData, Reason) ->
LJID = jid:tolower(JID),
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
StateData#state.users),
@@ -1636,7 +1692,10 @@ add_user_presence_un(JID, Presence, StateData) ->
%% Return jid record.
-spec find_jids_by_nick(binary(), state()) -> [jid()].
find_jids_by_nick(Nick, StateData) ->
- case (?DICT):find(Nick, StateData#state.nicks) of
+ Nicks = ?DICT:merge(fun(_, Val, _) -> Val end,
+ StateData#state.nicks,
+ StateData#state.subscriber_nicks),
+ case (?DICT):find(Nick, Nicks) of
{ok, [User]} -> [jid:make(User)];
{ok, Users} -> [jid:make(LJID) || LJID <- Users];
error -> []
@@ -1704,7 +1763,14 @@ is_nick_change(JID, Nick, StateData) ->
-spec nick_collision(jid(), binary(), state()) -> boolean().
nick_collision(User, Nick, StateData) ->
- UserOfNick = find_jid_by_nick(Nick, StateData),
+ UserOfNick = case find_jid_by_nick(Nick, StateData) of
+ false ->
+ case ?DICT:find(Nick, StateData#state.subscriber_nicks) of
+ {ok, [J]} -> J;
+ error -> false
+ end;
+ J -> J
+ end,
(UserOfNick /= false andalso
jid:remove_resource(jid:tolower(UserOfNick))
/= jid:remove_resource(jid:tolower(User))).
@@ -1819,8 +1885,7 @@ add_new_user(From, Nick, Packet, StateData) ->
NewState = add_user_presence(
From, Packet,
add_online_user(From, Nick, Role,
- IsSubscribeRequest,
- Nodes, StateData)),
+ StateData)),
send_existing_presences(From, NewState),
send_initial_presence(From, NewState, StateData),
History = get_history(Nick, Packet, NewState),
@@ -1828,9 +1893,7 @@ add_new_user(From, Nick, Packet, StateData) ->
send_subject(From, StateData),
NewState;
true ->
- add_online_user(From, Nick, none,
- IsSubscribeRequest,
- Nodes, StateData)
+ set_subscriber(From, Nick, Nodes, StateData)
end,
ResultState =
case NewStateData#state.just_created of
@@ -2020,16 +2083,6 @@ presence_broadcast_allowed(JID, StateData) ->
Role = get_role(JID, StateData),
lists:member(Role, (StateData#state.config)#config.presence_broadcast).
--spec is_initial_presence(jid(), state()) -> boolean().
-is_initial_presence(From, StateData) ->
- LJID = jid:tolower(From),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{last_presence = Pres}} when Pres /= undefined ->
- false;
- _ ->
- true
- end.
-
-spec send_initial_presence(jid(), state(), state()) -> ok.
send_initial_presence(NJID, StateData, OldStateData) ->
send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@@ -2126,7 +2179,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
true ->
[{LNJID, UserInfo}];
false ->
- (?DICT):to_list(StateData#state.users)
+ (?DICT):to_list(get_users_and_subscribers(StateData))
end,
lists:foreach(
fun({LUJID, Info}) ->
@@ -2158,7 +2211,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
- IsSubscriber = Info#user.is_subscriber,
+ IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) and
(IsInitialPresence or (Type == unavailable)) ->
@@ -2306,20 +2359,21 @@ send_nick_changing(JID, OldNick, StateData,
(_) ->
ok
end,
- (?DICT):to_list(StateData#state.users)).
+ ?DICT:to_list(get_users_and_subscribers(StateData))).
-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok.
maybe_send_affiliation(JID, Affiliation, StateData) ->
LJID = jid:tolower(JID),
+ Users = get_users_and_subscribers(StateData),
IsOccupant = case LJID of
{LUser, LServer, <<"">>} ->
not (?DICT):is_empty(
(?DICT):filter(fun({U, S, _}, _) ->
U == LUser andalso
S == LServer
- end, StateData#state.users));
+ end, Users));
{_LUser, _LServer, _LResource} ->
- (?DICT):is_key(LJID, StateData#state.users)
+ (?DICT):is_key(LJID, Users)
end,
case IsOccupant of
true ->
@@ -2335,19 +2389,19 @@ send_affiliation(JID, Affiliation, StateData) ->
role = none},
Message = #message{id = randoms:get_string(),
sub_els = [#muc_user{items = [Item]}]},
+ Users = get_users_and_subscribers(StateData),
Recipients = case (StateData#state.config)#config.anonymous of
true ->
(?DICT):filter(fun(_, #user{role = moderator}) ->
true;
(_, _) ->
false
- end, StateData#state.users);
+ end, Users);
false ->
- StateData#state.users
+ Users
end,
- send_multiple(StateData#state.jid,
- StateData#state.server_host,
- Recipients, Message).
+ send_wrapped_multiple(StateData#state.jid, Recipients, Message,
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData).
-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()].
status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) ->
@@ -2452,11 +2506,11 @@ check_subject(#message{subject = [_|_] = Subj, body = [],
check_subject(_) ->
false.
--spec can_change_subject(role(), state()) -> boolean().
-can_change_subject(Role, StateData) ->
+-spec can_change_subject(role(), boolean(), state()) -> boolean().
+can_change_subject(Role, IsSubscriber, StateData) ->
case (StateData#state.config)#config.allow_change_subj
of
- true -> Role == moderator orelse Role == participant;
+ true -> Role == moderator orelse Role == participant orelse IsSubscriber == true;
_ -> Role == moderator
end.
@@ -2925,7 +2979,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick),
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
- IsSubscriber = Info#user.is_subscriber,
+ IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) ->
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
@@ -2934,7 +2988,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
ok
end
end,
- (?DICT):to_list(StateData#state.users)).
+ (?DICT):to_list(get_users_and_subscribers(StateData))).
-spec get_actor_nick(binary() | jid(), state()) -> binary().
get_actor_nick(<<"">>, _StateData) ->
@@ -3301,7 +3355,7 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
id = randoms:get_string(),
sub_els = [#muc_user{status_codes = Codes}]},
send_wrapped_multiple(StateData#state.jid,
- StateData#state.users,
+ get_users_and_subscribers(StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData);
@@ -3321,7 +3375,7 @@ remove_nonmembers(StateData) ->
_ -> SD
end
end,
- StateData, (?DICT):to_list(StateData#state.users)).
+ StateData, (?DICT):to_list(get_users_and_subscribers(StateData))).
-spec set_opts([{atom(), any()}], state()) -> state().
set_opts([], StateData) -> StateData;
@@ -3443,14 +3497,17 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
subscribers ->
- lists:foldl(
- fun({JID, Nick, Nodes}, State) ->
- User = #user{jid = JID, nick = Nick,
- subscriptions = Nodes,
- is_subscriber = true,
- role = none},
- update_online_user(JID, User, State)
- end, StateData, Val);
+ Subscribers = lists:foldl(
+ fun({JID, Nick, Nodes}, Acc) ->
+ BareJID = jid:remove_resource(JID),
+ ?DICT:store(
+ jid:tolower(BareJID),
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ Acc)
+ end, ?DICT:new(), Val),
+ StateData#state{subscribers = Subscribers};
affiliations ->
StateData#state{affiliations = (?DICT):from_list(Val)};
subject -> StateData#state{subject = Val};
@@ -3466,12 +3523,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
make_opts(StateData) ->
Config = StateData#state.config,
Subscribers = (?DICT):fold(
- fun(_LJID, #user{is_subscriber = true} = User, Acc) ->
- [{User#user.jid, User#user.nick,
- User#user.subscriptions}|Acc];
- (_, _, Acc) ->
- Acc
- end, [], StateData#state.users),
+ fun(_LJID, Sub, Acc) ->
+ [{Sub#subscriber.jid,
+ Sub#subscriber.nick,
+ Sub#subscriber.nodes}|Acc]
+ end, [], StateData#state.subscribers),
[?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description),
?MAKE_CONFIG_OPT(#config.allow_change_subj),
?MAKE_CONFIG_OPT(#config.allow_query_users),
@@ -3490,6 +3546,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous),
?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users),
?MAKE_CONFIG_OPT(#config.allow_voice_requests),
+ ?MAKE_CONFIG_OPT(#config.allow_subscription),
?MAKE_CONFIG_OPT(#config.mam),
?MAKE_CONFIG_OPT(#config.voice_request_min_interval),
?MAKE_CONFIG_OPT(#config.vcard),
@@ -3517,7 +3574,7 @@ destroy_room(DEl, StateData) ->
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
end,
- (?DICT):to_list(StateData#state.users)),
+ (?DICT):to_list(get_users_and_subscribers(StateData))),
case (StateData#state.config)#config.persistent of
true ->
mod_muc:forget_room(StateData#state.server_host,
@@ -3655,9 +3712,9 @@ process_iq_mucsub(From,
#iq{type = set, lang = Lang,
sub_els = [#muc_subscribe{nick = Nick}]} = Packet,
StateData) ->
- LJID = jid:tolower(From),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick ->
+ LBareJID = jid:tolower(jid:remove_resource(From)),
+ case (?DICT):find(LBareJID, StateData#state.subscribers) of
+ {ok, #subscriber{nick = Nick1}} when Nick1 /= Nick ->
Nodes = get_subscription_nodes(Packet),
case {nick_collision(From, Nick, StateData),
mod_muc:can_use_nick(StateData#state.server_host,
@@ -3670,54 +3727,53 @@ process_iq_mucsub(From,
ErrText = <<"That nickname is registered by another person">>,
{error, xmpp:err_conflict(ErrText, Lang)};
_ ->
- NewStateData = add_online_user(
- From, Nick, Role, true, Nodes, StateData),
+ NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscribe_result(Packet), NewStateData}
end;
- {ok, #user{role = Role}} ->
+ {ok, #subscriber{}} ->
Nodes = get_subscription_nodes(Packet),
- NewStateData = add_online_user(
- From, Nick, Role, true, Nodes, StateData),
+ NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscribe_result(Packet), NewStateData};
error ->
add_new_user(From, Nick, Packet, StateData)
end;
process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]},
StateData) ->
- LJID = jid:tolower(From),
- case ?DICT:find(LJID, StateData#state.users) of
- {ok, #user{is_subscriber = true} = User} ->
- NewStateData = remove_subscription(From, User, StateData),
+ LBareJID = jid:tolower(jid:remove_resource(From)),
+ case ?DICT:find(LBareJID, StateData#state.subscribers) of
+ {ok, #subscriber{nick = Nick}} ->
+ Nicks = ?DICT:erase(Nick, StateData#state.subscriber_nicks),
+ Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers),
+ NewStateData = StateData#state{subscribers = Subscribers,
+ subscriber_nicks = Nicks},
store_room(NewStateData),
{result, undefined, NewStateData};
_ ->
{result, undefined, StateData}
end;
+process_iq_mucsub(From, #iq{type = get, lang = Lang,
+ sub_els = [#muc_subscriptions{}]},
+ StateData) ->
+ FAffiliation = get_affiliation(From, StateData),
+ FRole = get_role(From, StateData),
+ if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
+ JIDs = dict:fold(
+ fun(_, #subscriber{jid = J}, Acc) ->
+ [J|Acc]
+ end, [], StateData#state.subscribers),
+ {result, #muc_subscriptions{list = JIDs}, StateData};
+ true ->
+ Txt = <<"Moderator privileges required">>,
+ {error, xmpp:err_forbidden(Txt, Lang)}
+ end;
process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) ->
Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
{error, xmpp:err_bad_request(Txt, Lang)}.
--spec remove_subscription(jid(), #user{}, state()) -> state().
-remove_subscription(JID, #user{is_subscriber = true} = User, StateData) ->
- case User#user.last_presence of
- undefined ->
- remove_online_user(JID, StateData, false);
- _ ->
- LJID = jid:tolower(JID),
- Users = ?DICT:store(LJID, User#user{is_subscriber = false},
- StateData#state.users),
- StateData#state{users = Users}
- end;
-remove_subscription(_JID, #user{}, StateData) ->
- StateData.
-
--spec remove_subscriptions(state()) -> state().
remove_subscriptions(StateData) ->
if not (StateData#state.config)#config.allow_subscription ->
- dict:fold(
- fun(_LJID, User, State) ->
- remove_subscription(User#user.jid, User, State)
- end, StateData, StateData#state.users);
+ StateData#state{subscribers = ?DICT:new(),
+ subscriber_nicks = ?DICT:new()};
true ->
StateData
end.
@@ -3957,18 +4013,26 @@ store_room(StateData) ->
-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok.
send_wrapped(From, To, Packet, Node, State) ->
LTo = jid:tolower(To),
- case ?DICT:find(LTo, State#state.users) of
- {ok, #user{is_subscriber = true,
- subscriptions = Nodes,
- last_presence = undefined}} ->
+ LBareTo = jid:tolower(jid:remove_resource(To)),
+ IsOffline = case ?DICT:find(LTo, State#state.users) of
+ {ok, #user{last_presence = undefined}} -> true;
+ error -> true;
+ _ -> false
+ end,
+ if IsOffline ->
+ case ?DICT:find(LBareTo, State#state.subscribers) of
+ {ok, #subscriber{nodes = Nodes, jid = JID}} ->
case lists:member(Node, Nodes) of
true ->
- NewPacket = wrap(From, To, Packet, Node),
- ejabberd_router:route(State#state.jid, To, NewPacket);
+ NewPacket = wrap(From, JID, Packet, Node),
+ ejabberd_router:route(State#state.jid, JID, NewPacket);
false ->
ok
end;
_ ->
+ ok
+ end;
+ true ->
ejabberd_router:route(From, To, Packet)
end.
@@ -3983,13 +4047,10 @@ wrap(From, To, Packet, Node) ->
id = randoms:get_string(),
xml_els = [El]}]}}]}.
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-%% Multicast
-
--spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok.
-send_multiple(From, Server, Users, Packet) ->
- JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
- ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
+%% -spec send_multiple(jid(), binary(), [#user{}], stanza()) -> ok.
+%% send_multiple(From, Server, Users, Packet) ->
+%% JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
+%% ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
-spec send_wrapped_multiple(jid(), [#user{}], stanza(), binary(), state()) -> ok.
send_wrapped_multiple(From, Users, Packet, Node, State) ->
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index 6134823c1..dfe3c9e8e 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -450,12 +450,12 @@ need_to_store(LServer, #message{type = Type} = Packet) ->
(unless_chat_state) -> unless_chat_state
end,
unless_chat_state) of
+ true ->
+ true;
false ->
Packet#message.body /= [];
unless_chat_state ->
- not xmpp_util:is_standalone_chat_state(Packet);
- true ->
- true
+ not xmpp_util:is_standalone_chat_state(Packet)
end
end;
true ->
@@ -469,14 +469,20 @@ store_packet(From, To, Packet) ->
case check_event(From, To, Packet) of
true ->
#jid{luser = LUser, lserver = LServer} = To,
- TimeStamp = p1_time_compat:timestamp(),
- Expire = find_x_expire(TimeStamp, Packet),
- El = xmpp:encode(Packet),
- gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) !
- #offline_msg{us = {LUser, LServer},
- timestamp = TimeStamp, expire = Expire,
- from = From, to = To, packet = El},
- stop;
+ case ejabberd_hooks:run_fold(store_offline_message, LServer,
+ Packet, [From, To]) of
+ drop ->
+ ok;
+ NewPacket ->
+ TimeStamp = p1_time_compat:timestamp(),
+ Expire = find_x_expire(TimeStamp, NewPacket),
+ El = xmpp:encode(NewPacket),
+ gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) !
+ #offline_msg{us = {LUser, LServer},
+ timestamp = TimeStamp, expire = Expire,
+ from = From, to = To, packet = El},
+ stop
+ end;
_ -> ok
end;
false -> ok
diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl
index b5033c710..9459753bc 100644
--- a/src/mod_offline_sql.erl
+++ b/src/mod_offline_sql.erl
@@ -81,7 +81,7 @@ remove_old_messages(Days, LServer) ->
[<<"DELETE FROM spool"
" WHERE created_at < "
"NOW() - INTERVAL '">>,
- integer_to_list(Days), <<"';">>]) of
+ integer_to_list(Days), <<"' DAY;">>]) of
{updated, N} ->
?INFO_MSG("~p message(s) deleted from offline spool", [N]);
_Error ->
diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl
index 7ca19b5e9..10f3cddc8 100644
--- a/src/mod_privacy_sql.erl
+++ b/src/mod_privacy_sql.erl
@@ -233,7 +233,7 @@ export(Server) ->
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
" %(MatchMessage)b, %(MatchPresenceIn)b,"
- " %(MatchPresenceOut)b)")
+ " %(MatchPresenceOut)b);")
|| {SType, SValue, SAction, Order,
MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn,
diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl
new file mode 100644
index 000000000..50212b7ae
--- /dev/null
+++ b/src/mod_privilege.erl
@@ -0,0 +1,348 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 11 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(mod_privilege).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2]).
+-export([start/2, stop/1, mod_opt_type/1, depends/2]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+-export([component_connected/1, component_disconnected/2,
+ roster_access/2, process_message/3,
+ process_presence_out/4, process_presence_in/5]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("xmpp.hrl").
+
+-record(state, {server_host = <<"">> :: binary(),
+ permissions = dict:new() :: ?TDICT}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(ejabberd_sup, PingSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+mod_opt_type(roster) -> v_roster();
+mod_opt_type(message) -> v_message();
+mod_opt_type(presence) -> v_presence();
+mod_opt_type(_) ->
+ [roster, message, presence].
+
+depends(_, _) ->
+ [].
+
+-spec component_connected(binary()) -> ok.
+component_connected(Host) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_connected, Host})
+ end, ?MYHOSTS).
+
+-spec component_disconnected(binary(), binary()) -> ok.
+component_disconnected(Host, _Reason) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_disconnected, Host})
+ end, ?MYHOSTS).
+
+-spec process_message(jid(), jid(), stanza()) -> stop | ok.
+process_message(#jid{luser = <<"">>, lresource = <<"">>} = From,
+ #jid{lresource = <<"">>} = To,
+ #message{lang = Lang, type = T} = Msg) when T /= error ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ Permissions = get_permissions(ServerHost),
+ case dict:find(Host, Permissions) of
+ {ok, Access} ->
+ case proplists:get_value(message, Access, none) of
+ outgoing ->
+ forward_message(From, To, Msg);
+ none ->
+ Txt = <<"Insufficient privilege">>,
+ Err = xmpp:err_forbidden(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end,
+ stop;
+ error ->
+ %% Component is disconnected
+ ok
+ end;
+process_message(_From, _To, _Stanza) ->
+ ok.
+
+-spec roster_access(boolean(), iq()) -> boolean().
+roster_access(true, _) ->
+ true;
+roster_access(false, #iq{from = From, to = To, type = Type}) ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ Permissions = get_permissions(ServerHost),
+ case dict:find(Host, Permissions) of
+ {ok, Access} ->
+ Permission = proplists:get_value(roster, Access, none),
+ (Permission == both)
+ orelse (Permission == get andalso Type == get)
+ orelse (Permission == set andalso Type == set);
+ error ->
+ %% Component is disconnected
+ false
+ end.
+
+-spec process_presence_out(stanza(), ejabberd_c2s:state(), jid(), jid()) -> stanza().
+process_presence_out(#presence{type = Type} = Pres, _C2SState,
+ #jid{luser = LUser, lserver = LServer} = From,
+ #jid{luser = LUser, lserver = LServer, lresource = <<"">>})
+ when Type == available; Type == unavailable ->
+ %% Self-presence processing
+ Permissions = get_permissions(LServer),
+ lists:foreach(
+ fun({Host, Access}) ->
+ Permission = proplists:get_value(presence, Access, none),
+ if Permission == roster; Permission == managed_entity ->
+ To = jid:make(Host),
+ ejabberd_router:route(
+ From, To, xmpp:set_from_to(Pres, From, To));
+ true ->
+ ok
+ end
+ end, dict:to_list(Permissions)),
+ Pres;
+process_presence_out(Acc, _, _, _) ->
+ Acc.
+
+-spec process_presence_in(stanza(), ejabberd_c2s:state(),
+ jid(), jid(), jid()) -> stanza().
+process_presence_in(#presence{type = Type} = Pres, _C2SState, _,
+ #jid{luser = U, lserver = S} = From,
+ #jid{luser = LUser, lserver = LServer})
+ when {U, S} /= {LUser, LServer} andalso
+ (Type == available orelse Type == unavailable) ->
+ Permissions = get_permissions(LServer),
+ lists:foreach(
+ fun({Host, Access}) ->
+ case proplists:get_value(presence, Access, none) of
+ roster ->
+ Permission = proplists:get_value(roster, Access, none),
+ if Permission == both; Permission == get ->
+ To = jid:make(Host),
+ ejabberd_router:route(
+ From, To, xmpp:set_from_to(Pres, From, To));
+ true ->
+ ok
+ end;
+ true ->
+ ok
+ end
+ end, dict:to_list(Permissions)),
+ Pres;
+process_presence_in(Acc, _, _, _, _) ->
+ Acc.
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+init([Host, _Opts]) ->
+ ejabberd_hooks:add(component_connected, ?MODULE,
+ component_connected, 50),
+ ejabberd_hooks:add(component_disconnected, ?MODULE,
+ component_disconnected, 50),
+ ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE,
+ process_message, 50),
+ ejabberd_hooks:add(roster_remote_access, Host, ?MODULE,
+ roster_access, 50),
+ ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
+ process_presence_out, 50),
+ ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
+ process_presence_in, 50),
+ {ok, #state{server_host = Host}}.
+
+handle_call(get_permissions, _From, State) ->
+ {reply, {ok, State#state.permissions}, State};
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast({component_connected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ From = jid:make(ServerHost),
+ To = jid:make(Host),
+ RosterPerm = get_roster_permission(ServerHost, Host),
+ PresencePerm = get_presence_permission(ServerHost, Host),
+ MessagePerm = get_message_permission(ServerHost, Host),
+ if RosterPerm /= none, PresencePerm /= none, MessagePerm /= none ->
+ Priv = #privilege{perms = [#privilege_perm{access = message,
+ type = MessagePerm},
+ #privilege_perm{access = roster,
+ type = RosterPerm},
+ #privilege_perm{access = presence,
+ type = PresencePerm}]},
+ ?INFO_MSG("Granting permissions to external "
+ "component '~s': roster = ~s, presence = ~s, "
+ "message = ~s",
+ [Host, RosterPerm, PresencePerm, MessagePerm]),
+ Msg = #message{from = From, to = To, sub_els = [Priv]},
+ ejabberd_router:route(From, To, Msg),
+ Permissions = dict:store(Host, [{roster, RosterPerm},
+ {presence, PresencePerm},
+ {message, MessagePerm}],
+ State#state.permissions),
+ {noreply, State#state{permissions = Permissions}};
+ true ->
+ ?INFO_MSG("Granting no permissions to external component '~s'",
+ [Host]),
+ {noreply, State}
+ end;
+handle_cast({component_disconnected, Host}, State) ->
+ Permissions = dict:erase(Host, State#state.permissions),
+ {noreply, State#state{permissions = Permissions}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ %% Note: we don't remove component_* hooks because they are global
+ %% and might be registered within a module on another virtual host
+ Host = State#state.server_host,
+ ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE,
+ process_message, 50),
+ ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE,
+ roster_access, 50),
+ ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
+ process_presence_out, 50),
+ ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
+ process_presence_in, 50).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+get_permissions(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ try gen_server:call(Proc, get_permissions) of
+ {ok, Permissions} ->
+ Permissions
+ catch exit:{noproc, _} ->
+ %% No module is loaded for this virtual host
+ dict:new()
+ end.
+
+forward_message(From, To, Msg) ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ case xmpp:get_subtag(Msg, #privilege{}) of
+ #privilege{forwarded = #forwarded{sub_els = [#message{} = SubEl]}} ->
+ case SubEl#message.from of
+ #jid{lresource = <<"">>, lserver = ServerHost} ->
+ ejabberd_router:route(
+ xmpp:get_from(SubEl), xmpp:get_to(SubEl), SubEl);
+ _ ->
+ Lang = xmpp:get_lang(Msg),
+ Txt = <<"Invalid 'from' attribute">>,
+ Err = xmpp:err_forbidden(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end;
+ _ ->
+ ?ERROR_MSG("got invalid forwarded payload from external "
+ "component '~s':~n~s", [Host, xmpp:pp(Msg)]),
+ Lang = xmpp:get_lang(Msg),
+ Txt = <<"Invalid forwarded payload">>,
+ Err = xmpp:err_bad_request(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end.
+
+get_roster_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, roster,
+ v_roster(), []),
+ case match_rule(ServerHost, Host, Perms, both) of
+ allow ->
+ both;
+ deny ->
+ Get = match_rule(ServerHost, Host, Perms, get),
+ Set = match_rule(ServerHost, Host, Perms, set),
+ if Get == allow, Set == allow -> both;
+ Get == allow -> get;
+ Set == allow -> set;
+ true -> none
+ end
+ end.
+
+get_message_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, message,
+ v_message(), []),
+ case match_rule(ServerHost, Host, Perms, outgoing) of
+ allow -> outgoing;
+ deny -> none
+ end.
+
+get_presence_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, presence,
+ v_presence(), []),
+ case match_rule(ServerHost, Host, Perms, roster) of
+ allow ->
+ roster;
+ deny ->
+ case match_rule(ServerHost, Host, Perms, managed_entity) of
+ allow -> managed_entity;
+ deny -> none
+ end
+ end.
+
+match_rule(ServerHost, Host, Perms, Type) ->
+ Access = proplists:get_value(Type, Perms, none),
+ acl:match_rule(ServerHost, Access, jid:make(Host)).
+
+v_roster() ->
+ fun(Props) ->
+ lists:map(
+ fun({both, ACL}) -> {both, acl:access_rules_validator(ACL)};
+ ({get, ACL}) -> {get, acl:access_rules_validator(ACL)};
+ ({set, ACL}) -> {set, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
+
+v_message() ->
+ fun(Props) ->
+ lists:map(
+ fun({outgoing, ACL}) -> {outgoing, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
+
+v_presence() ->
+ fun(Props) ->
+ lists:map(
+ fun({managed_entity, ACL}) ->
+ {managed_entity, acl:access_rules_validator(ACL)};
+ ({roster, ACL}) ->
+ {roster, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index e3fe64c16..a586935b8 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -385,8 +385,6 @@ depends(ServerHost, Opts) ->
%% The default plugin module is implicit.
%% <p>The Erlang code for the plugin is located in a module called
%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p>
-%% <p>The modules are initialized in alphetical order and the list is checked
-%% and sorted to ensure that each module is initialized only once.</p>
%% <p>See {@link node_hometree:init/1} for an example implementation.</p>
init_plugins(Host, ServerHost, Opts) ->
TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts,
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index fa27f866c..c344213f3 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -139,15 +139,18 @@ stop(Host) ->
depends(_Host, _Opts) ->
[].
-process_iq(#iq{from = #jid{luser = <<"">>},
- to = #jid{resource = <<"">>}} = IQ) ->
- process_iq_manager(IQ);
process_iq(#iq{from = #jid{luser = U, lserver = S},
to = #jid{luser = U, lserver = S}} = IQ) ->
process_local_iq(IQ);
-process_iq(#iq{lang = Lang} = IQ) ->
- Txt = <<"Query to another users is forbidden">>,
- xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)).
+process_iq(#iq{lang = Lang, to = To} = IQ) ->
+ case ejabberd_hooks:run_fold(roster_remote_access,
+ To#jid.lserver, false, [IQ]) of
+ false ->
+ Txt = <<"Query to another users is forbidden">>,
+ xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
+ true ->
+ process_local_iq(IQ)
+ end.
process_local_iq(#iq{type = set,lang = Lang,
sub_els = [#roster_query{
@@ -251,10 +254,10 @@ write_roster_version(LUser, LServer, InTransaction) ->
%% - roster versioning is not used by the client OR
%% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR
%% - the roster version from client don't match current version.
-process_iq_get(#iq{from = From, to = To, lang = Lang,
+process_iq_get(#iq{to = To, lang = Lang,
sub_els = [#roster_query{ver = RequestedVersion}]} = IQ) ->
- LUser = From#jid.luser,
- LServer = From#jid.lserver,
+ LUser = To#jid.luser,
+ LServer = To#jid.lserver,
US = {LUser, LServer},
try {ItemsToSend, VersionToSend} =
case {roster_versioning_enabled(LServer),
@@ -303,7 +306,7 @@ process_iq_get(#iq{from = From, to = To, lang = Lang,
end)
catch E:R ->
?ERROR_MSG("failed to process roster get for ~s: ~p",
- [jid:to_string(From), {E, {R, erlang:get_stacktrace()}}]),
+ [jid:to_string(To), {E, {R, erlang:get_stacktrace()}}]),
Txt = <<"Roster module has failed">>,
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end.
@@ -369,10 +372,10 @@ get_roster_by_jid_t(LUser, LServer, LJID) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_roster_by_jid(LUser, LServer, LJID).
-process_iq_set(#iq{from = From, to = To, id = Id,
+process_iq_set(#iq{from = From, to = To,
sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
- Managed = is_managed_from_id(Id),
- #jid{user = User, luser = LUser, lserver = LServer} = From,
+ #jid{user = User, luser = LUser, lserver = LServer} = To,
+ Managed = {From#jid.luser, From#jid.lserver} /= {LUser, LServer},
F = fun () ->
lists:map(
fun(#roster_item{jid = JID1} = QueryItem) ->
@@ -397,11 +400,10 @@ process_iq_set(#iq{from = From, to = To, id = Id,
{atomic, ItemPairs} ->
lists:foreach(
fun({OldItem, Item}) ->
- send_itemset_to_managers(From, Item, Managed),
push_item(User, LServer, To, Item),
case Item#roster.subscription of
remove ->
- send_unsubscribing_presence(From, OldItem);
+ send_unsubscribing_presence(To, OldItem);
_ ->
ok
end
@@ -1012,66 +1014,6 @@ webadmin_user(Acc, _User, _Server, Lang) ->
[?XE(<<"h3">>, [?ACT(<<"roster/">>, <<"Roster">>)])].
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-
-%% Implement XEP-0321 Remote Roster Management
-
-process_iq_manager(#iq{from = From, to = To, lang = Lang} = IQ) ->
- %% Check what access is allowed for From to To
- MatchDomain = From#jid.lserver,
- case is_domain_managed(MatchDomain, To#jid.lserver) of
- true ->
- process_iq_manager2(MatchDomain, IQ);
- false ->
- Txt = <<"Roster management is not allowed from this domain">>,
- xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
- end.
-
-process_iq_manager2(MatchDomain, #iq{to = To} = IQ) ->
- %% If IQ is SET, filter the input IQ
- IQFiltered = maybe_filter_request(MatchDomain, IQ),
- %% Call the standard function with reversed JIDs
- IdInitial = IQFiltered#iq.id,
- ResIQ = process_iq(IQFiltered#iq{from = To, to = To,
- id = <<"roster-remotely-managed">>}),
- %% Filter the output IQ
- filter_stanza(MatchDomain, ResIQ#iq{id = IdInitial}).
-
-is_domain_managed(ContactHost, UserHost) ->
- Managers = gen_mod:get_module_opt(UserHost, ?MODULE, managers,
- fun(B) when is_list(B) -> B end,
- []),
- lists:member(ContactHost, Managers).
-
-maybe_filter_request(MatchDomain, IQ) when IQ#iq.type == set ->
- filter_stanza(MatchDomain, IQ);
-maybe_filter_request(_MatchDomain, IQ) ->
- IQ.
-
-filter_stanza(MatchDomain,
- #iq{sub_els = [#roster_query{items = Items} = R]} = IQ) ->
- ItemsFiltered = lists:filter(
- fun(#roster_item{jid = #jid{lserver = S}}) ->
- S == MatchDomain
- end, Items),
- IQ#iq{sub_els = [R#roster_query{items = ItemsFiltered}]}.
-
-send_itemset_to_managers(_From, _Item, true) ->
- ok;
-send_itemset_to_managers(From, Item, false) ->
- {_, UserHost} = Item#roster.us,
- {_ContactUser, ContactHost, _ContactResource} = Item#roster.jid,
- %% Check if the component is an allowed manager
- IsManager = is_domain_managed(ContactHost, UserHost),
- case IsManager of
- true -> push_item(<<"">>, ContactHost, <<"">>, From, Item);
- false -> ok
- end.
-
-is_managed_from_id(<<"roster-remotely-managed">>) ->
- true;
-is_managed_from_id(_Id) ->
- false.
-
has_duplicated_groups(Groups) ->
GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
not (length(GroupsPrep) == length(Groups)).
diff --git a/src/mod_stats.erl b/src/mod_stats.erl
index c4b8ddb15..f146498c6 100644
--- a/src/mod_stats.erl
+++ b/src/mod_stats.erl
@@ -127,13 +127,8 @@ get_local_stat(Server, [], Name)
end;
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/online">> ->
- case catch mnesia:table_info(session, size) of
- {'EXIT', _Reason} ->
- ?STATERR(500, <<"Internal Server Error">>);
- Users ->
- ?STATVAL((iolist_to_binary(integer_to_list(Users))),
- <<"users">>)
- end;
+ Users = ejabberd_sm:connected_users_number(),
+ ?STATVAL((iolist_to_binary(integer_to_list(Users))), <<"users">>);
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/total">> ->
NumUsers = lists:foldl(fun (Host, Total) ->
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index c1dfd0e81..5adf1e559 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -664,7 +664,7 @@ get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex,
Before /= undefined -> {<<">">>, <<"asc">>};
true -> {<<"is not">>, <<"desc">>}
end,
- SNidx = integer_to_binary(Nidx),
+ SNidx = jlib:i2l(Nidx),
I = if After /= undefined -> After;
Before /= undefined -> Before;
true -> undefined
@@ -774,7 +774,7 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM
get_last_items(Nidx, _From, Count) ->
Limit = jlib:i2l(Count),
- SNidx = integer_to_binary(Nidx),
+ SNidx = jlib:i2l(Nidx),
Query = fun(mssql, _) ->
ejabberd_sql:sql_query_t(
[<<"select top ">>, Limit,
@@ -876,7 +876,7 @@ del_items(Nidx, [ItemId]) ->
del_item(Nidx, ItemId);
del_items(Nidx, ItemIds) ->
I = str:join([[<<"'">>, ejabberd_sql:escape(X), <<"'">>] || X <- ItemIds], <<",">>),
- SNidx = integer_to_binary(Nidx),
+ SNidx = jlib:i2l(Nidx),
catch
ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>,
I, <<") and nodeid='">>, SNidx, <<"';">>]).
@@ -932,8 +932,9 @@ select_affiliation_subscriptions(Nidx, JID, JID) ->
select_affiliation_subscriptions(Nidx, JID);
select_affiliation_subscriptions(Nidx, GenKey, SubKey) ->
{result, Affiliation} = get_affiliation(Nidx, GenKey),
- {result, Subscriptions} = get_subscriptions(Nidx, SubKey),
- {Affiliation, Subscriptions}.
+ {result, BareJidSubs} = get_subscriptions(Nidx, GenKey),
+ {result, FullJidSubs} = get_subscriptions(Nidx, SubKey),
+ {Affiliation, BareJidSubs++FullJidSubs}.
update_affiliation(Nidx, JID, Affiliation) ->
J = encode_jid(JID),
diff --git a/src/node_mb.erl b/src/node_mb.erl
index 0c3bd3722..c06c08d67 100644
--- a/src/node_mb.erl
+++ b/src/node_mb.erl
@@ -37,6 +37,7 @@
%%% plugins:
%%% - "flat"
%%% - "pep" # Requires mod_caps.
+%%% - "mb"
%%% pep_mapping:
%%% "urn:xmpp:microblog:0": "mb"
%%% </pre></p>
@@ -153,7 +154,7 @@ set_subscriptions(Nidx, Owner, Subscription, SubId) ->
node_pep:set_subscriptions(Nidx, Owner, Subscription, SubId).
get_pending_nodes(Host, Owner) ->
- node_hometree:get_pending_nodes(Host, Owner).
+ node_pep:get_pending_nodes(Host, Owner).
get_states(Nidx) ->
node_pep:get_states(Nidx).
diff --git a/src/node_mb_sql.erl b/src/node_mb_sql.erl
new file mode 100644
index 000000000..125674316
--- /dev/null
+++ b/src/node_mb_sql.erl
@@ -0,0 +1,158 @@
+%%%----------------------------------------------------------------------
+%%% File : node_mb_sql.erl
+%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
+%%% Purpose : PEP microblogging (XEP-0277) plugin with SQL backend
+%%% Created : 6 Sep 2016 by Holger Weiss <holger@zedat.fu-berlin.de>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2016 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(node_mb_sql).
+-behaviour(gen_pubsub_node).
+-author('holger@zedat.fu-berlin.de').
+
+-include("pubsub.hrl").
+-include("jlib.hrl").
+
+-export([init/3, terminate/2, options/0, features/0,
+ create_node_permission/6, create_node/2, delete_node/1,
+ purge_node/2, subscribe_node/8, unsubscribe_node/4,
+ publish_item/7, delete_item/4, remove_extra_items/3,
+ get_entity_affiliations/2, get_node_affiliations/1,
+ get_affiliation/2, set_affiliation/3,
+ get_entity_subscriptions/2, get_node_subscriptions/1,
+ get_subscriptions/2, set_subscriptions/4,
+ get_pending_nodes/2, get_states/1, get_state/2,
+ set_state/1, get_items/7, get_items/3, get_item/7,
+ get_item/2, set_item/1, get_item_name/3, node_to_path/1,
+ path_to_node/1, get_entity_subscriptions_for_send_last/2,
+ get_last_items/3]).
+
+init(Host, ServerHost, Opts) ->
+ node_pep_sql:init(Host, ServerHost, Opts).
+
+terminate(Host, ServerHost) ->
+ node_pep_sql:terminate(Host, ServerHost), ok.
+
+options() ->
+ [{sql, true}, {rsm, true} | node_mb:options()].
+
+features() ->
+ [<<"rsm">> | node_mb:features()].
+
+create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) ->
+ node_pep_sql:create_node_permission(Host, ServerHost, Node, ParentNode,
+ Owner, Access).
+
+create_node(Nidx, Owner) ->
+ node_pep_sql:create_node(Nidx, Owner).
+
+delete_node(Removed) ->
+ node_pep_sql:delete_node(Removed).
+
+subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+ PresenceSubscription, RosterGroup, Options) ->
+ node_pep_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+ PresenceSubscription, RosterGroup, Options).
+
+unsubscribe_node(Nidx, Sender, Subscriber, SubId) ->
+ node_pep_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId).
+
+publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
+ node_pep_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
+ Payload, PubOpts).
+
+remove_extra_items(Nidx, MaxItems, ItemIds) ->
+ node_pep_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
+
+delete_item(Nidx, Publisher, PublishModel, ItemId) ->
+ node_pep_sql:delete_item(Nidx, Publisher, PublishModel, ItemId).
+
+purge_node(Nidx, Owner) ->
+ node_pep_sql:purge_node(Nidx, Owner).
+
+get_entity_affiliations(Host, Owner) ->
+ node_pep_sql:get_entity_affiliations(Host, Owner).
+
+get_node_affiliations(Nidx) ->
+ node_pep_sql:get_node_affiliations(Nidx).
+
+get_affiliation(Nidx, Owner) ->
+ node_pep_sql:get_affiliation(Nidx, Owner).
+
+set_affiliation(Nidx, Owner, Affiliation) ->
+ node_pep_sql:set_affiliation(Nidx, Owner, Affiliation).
+
+get_entity_subscriptions(Host, Owner) ->
+ node_pep_sql:get_entity_subscriptions(Host, Owner).
+
+get_entity_subscriptions_for_send_last(Host, Owner) ->
+ node_pep_sql:get_entity_subscriptions_for_send_last(Host, Owner).
+
+get_node_subscriptions(Nidx) ->
+ node_pep_sql:get_node_subscriptions(Nidx).
+
+get_subscriptions(Nidx, Owner) ->
+ node_pep_sql:get_subscriptions(Nidx, Owner).
+
+set_subscriptions(Nidx, Owner, Subscription, SubId) ->
+ node_pep_sql:set_subscriptions(Nidx, Owner, Subscription, SubId).
+
+get_pending_nodes(Host, Owner) ->
+ node_pep_sql:get_pending_nodes(Host, Owner).
+
+get_states(Nidx) ->
+ node_pep_sql:get_states(Nidx).
+
+get_state(Nidx, JID) ->
+ node_pep_sql:get_state(Nidx, JID).
+
+set_state(State) ->
+ node_pep_sql:set_state(State).
+
+get_items(Nidx, From, RSM) ->
+ node_pep_sql:get_items(Nidx, From, RSM).
+
+get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId,
+ RSM) ->
+ node_pep_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription,
+ RosterGroup, SubId, RSM).
+
+get_last_items(Nidx, JID, Count) ->
+ node_pep_sql:get_last_items(Nidx, JID, Count).
+
+get_item(Nidx, ItemId) ->
+ node_pep_sql:get_item(Nidx, ItemId).
+
+get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup,
+ SubId) ->
+ node_pep_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription,
+ RosterGroup, SubId).
+
+set_item(Item) ->
+ node_pep_sql:set_item(Item).
+
+get_item_name(Host, Node, Id) ->
+ node_pep_sql:get_item_name(Host, Node, Id).
+
+node_to_path(Node) ->
+ node_pep_sql:node_to_path(Node).
+
+path_to_node(Path) ->
+ node_pep_sql:path_to_node(Path).
diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl
index c292c7755..5e4462160 100644
--- a/src/nodetree_tree_sql.erl
+++ b/src/nodetree_tree_sql.erl
@@ -77,9 +77,9 @@ set_node(Record) when is_record(Record, pubsub_node) ->
catch
ejabberd_sql:sql_query_t(
?SQL("update pubsub_node set"
- " host=%(H)s"
- " node=%(Node)s"
- " parent=%(Parent)s"
+ " host=%(H)s,"
+ " node=%(Node)s,"
+ " parent=%(Parent)s,"
" type=%(Type)s "
"where nodeid=%(OldNidx)d")),
OldNidx;
diff --git a/src/randoms.erl b/src/randoms.erl
index 60d0b4e3d..1353f48af 100644
--- a/src/randoms.erl
+++ b/src/randoms.erl
@@ -27,14 +27,29 @@
-author('alexey@process-one.net').
--export([get_string/0]).
+-export([get_string/0, uniform/0, uniform/1, bytes/1]).
-export([start/0]).
+-define(THRESHOLD, 16#10000000000000000).
+
start() ->
ok.
get_string() ->
- R = crypto:rand_uniform(0, 16#10000000000000000),
- integer_to_binary(R).
+ R = crypto:rand_uniform(0, ?THRESHOLD),
+ jlib:integer_to_binary(R).
+
+uniform() ->
+ crypto:rand_uniform(0, ?THRESHOLD)/?THRESHOLD.
+
+uniform(N) ->
+ crypto:rand_uniform(1, N+1).
+-ifdef(STRONG_RAND_BYTES).
+bytes(N) ->
+ crypto:strong_rand_bytes(N).
+-else.
+bytes(N) ->
+ crypto:rand_bytes(N).
+-endif.
diff --git a/src/rest.erl b/src/rest.erl
new file mode 100644
index 000000000..01b04f66a
--- /dev/null
+++ b/src/rest.erl
@@ -0,0 +1,181 @@
+%%%----------------------------------------------------------------------
+%%% File : rest.erl
+%%% Author : Christophe Romain <christophe.romain@process-one.net>
+%%% Purpose : Generic REST client
+%%% Created : 16 Oct 2014 by Christophe Romain <christophe.romain@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 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(rest).
+
+-behaviour(ejabberd_config).
+
+-export([start/1, stop/1, get/2, get/3, post/4, delete/2,
+ request/6, with_retry/4, opt_type/1]).
+
+-include("logger.hrl").
+
+-define(HTTP_TIMEOUT, 10000).
+-define(CONNECT_TIMEOUT, 8000).
+
+start(Host) ->
+ http_p1:start(),
+ Pool_size =
+ ejabberd_config:get_option({ext_api_http_pool_size, Host},
+ fun(X) when is_integer(X), X > 0->
+ X
+ end,
+ 100),
+ http_p1:set_pool_size(Pool_size).
+
+stop(_Host) ->
+ ok.
+
+with_retry(Method, Args, MaxRetries, Backoff) ->
+ with_retry(Method, Args, 0, MaxRetries, Backoff).
+with_retry(Method, Args, Retries, MaxRetries, Backoff) ->
+ case apply(?MODULE, Method, Args) of
+ %% Only retry on timeout errors
+ {error, {http_error,{error,Error}}}
+ when Retries < MaxRetries
+ andalso (Error == 'timeout' orelse Error == 'connect_timeout') ->
+ timer:sleep(round(math:pow(2, Retries)) * Backoff),
+ with_retry(Method, Args, Retries+1, MaxRetries, Backoff);
+ Result ->
+ Result
+ end.
+
+get(Server, Path) ->
+ request(Server, get, Path, [], "application/json", <<>>).
+get(Server, Path, Params) ->
+ request(Server, get, Path, Params, "application/json", <<>>).
+
+delete(Server, Path) ->
+ request(Server, delete, Path, [], "application/json", <<>>).
+
+post(Server, Path, Params, Content) ->
+ Data = case catch jiffy:encode(Content) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("HTTP content encodage failed:~n"
+ "** Content = ~p~n"
+ "** Err = ~p",
+ [Content, Reason]),
+ <<>>;
+ Encoded ->
+ Encoded
+ end,
+ request(Server, post, Path, Params, "application/json", Data).
+
+request(Server, Method, Path, Params, Mime, Data) ->
+ URI = url(Server, Path, Params),
+ Opts = [{connect_timeout, ?CONNECT_TIMEOUT},
+ {timeout, ?HTTP_TIMEOUT}],
+ Hdrs = [{"connection", "keep-alive"},
+ {"content-type", Mime},
+ {"User-Agent", "ejabberd"}],
+ Begin = os:timestamp(),
+ Result = case catch http_p1:request(Method, URI, Hdrs, Data, Opts) of
+ {ok, Code, _, <<>>} ->
+ {ok, Code, []};
+ {ok, Code, _, <<" ">>} ->
+ {ok, Code, []};
+ {ok, Code, _, <<"\r\n">>} ->
+ {ok, Code, []};
+ {ok, Code, _, Body} ->
+ try jiffy:decode(Body) of
+ JSon ->
+ {ok, Code, JSon}
+ catch
+ _:Error ->
+ ?ERROR_MSG("HTTP response decode failed:~n"
+ "** URI = ~s~n"
+ "** Body = ~p~n"
+ "** Err = ~p",
+ [URI, Body, Error]),
+ {error, {invalid_json, Body}}
+ end;
+ {error, Reason} ->
+ ?ERROR_MSG("HTTP request failed:~n"
+ "** URI = ~s~n"
+ "** Err = ~p",
+ [URI, Reason]),
+ {error, {http_error, {error, Reason}}};
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("HTTP request failed:~n"
+ "** URI = ~s~n"
+ "** Err = ~p",
+ [URI, Reason]),
+ {error, {http_error, {error, Reason}}}
+ end,
+ ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]),
+ case Result of
+ {ok, _, _} ->
+ End = os:timestamp(),
+ Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms
+ ejabberd_hooks:run(backend_api_response_time, Server,
+ [Server, Method, Path, Elapsed]);
+ {error, {http_error,{error,timeout}}} ->
+ ejabberd_hooks:run(backend_api_timeout, Server,
+ [Server, Method, Path]);
+ {error, {http_error,{error,connect_timeout}}} ->
+ ejabberd_hooks:run(backend_api_timeout, Server,
+ [Server, Method, Path]);
+ {error, _} ->
+ ejabberd_hooks:run(backend_api_error, Server,
+ [Server, Method, Path])
+ end,
+ Result.
+
+%%%----------------------------------------------------------------------
+%%% HTTP helpers
+%%%----------------------------------------------------------------------
+
+base_url(Server, Path) ->
+ Tail = case iolist_to_binary(Path) of
+ <<$/, Ok/binary>> -> Ok;
+ Ok -> Ok
+ end,
+ case Tail of
+ <<"http", _Url/binary>> -> Tail;
+ _ ->
+ Base = ejabberd_config:get_option({ext_api_url, Server},
+ fun(X) ->
+ iolist_to_binary(X)
+ end,
+ <<"http://localhost/api">>),
+ <<Base/binary, "/", Tail/binary>>
+ end.
+
+url(Server, Path, []) ->
+ binary_to_list(base_url(Server, Path));
+url(Server, Path, Params) ->
+ Base = base_url(Server, Path),
+ [<<$&, ParHead/binary>> | ParTail] =
+ [<<"&", (iolist_to_binary(Key))/binary, "=",
+ (ejabberd_http:url_encode(Value))/binary>>
+ || {Key, Value} <- Params],
+ Tail = iolist_to_binary([ParHead | ParTail]),
+ binary_to_list(<<Base/binary, $?, Tail/binary>>).
+
+opt_type(ext_api_http_pool_size) ->
+ fun (X) when is_integer(X), X > 0 -> X end;
+opt_type(ext_api_url) ->
+ fun (X) -> iolist_to_binary(X) end;
+opt_type(_) -> [ext_api_http_pool_size, ext_api_url].
diff --git a/src/xmpp_codec.erl b/src/xmpp_codec.erl
index 345de7031..8713365cc 100644
--- a/src/xmpp_codec.erl
+++ b/src/xmpp_codec.erl
@@ -20,6 +20,48 @@ decode({xmlel, _name, _attrs, _} = _el, TopXMLNS,
Opts) ->
IgnoreEls = proplists:get_bool(ignore_els, Opts),
case {_name, get_attr(<<"xmlns">>, _attrs), TopXMLNS} of
+ {<<"query">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegation_query(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"query">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegation_query(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"delegate">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegate(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"delegate">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegate(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"delegation">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegation(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"delegation">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegation(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"delegated">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegated(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"delegated">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegated(<<"urn:xmpp:delegation:1">>, IgnoreEls,
+ _el);
+ {<<"attribute">>, <<"urn:xmpp:delegation:1">>, _} ->
+ decode_delegated_attribute(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"attribute">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ decode_delegated_attribute(<<"urn:xmpp:delegation:1">>,
+ IgnoreEls, _el);
+ {<<"privilege">>, <<"urn:xmpp:privilege:1">>, _} ->
+ decode_privilege(<<"urn:xmpp:privilege:1">>, IgnoreEls,
+ _el);
+ {<<"privilege">>, <<>>, <<"urn:xmpp:privilege:1">>} ->
+ decode_privilege(<<"urn:xmpp:privilege:1">>, IgnoreEls,
+ _el);
+ {<<"perm">>, <<"urn:xmpp:privilege:1">>, _} ->
+ decode_privilege_perm(<<"urn:xmpp:privilege:1">>,
+ IgnoreEls, _el);
+ {<<"perm">>, <<>>, <<"urn:xmpp:privilege:1">>} ->
+ decode_privilege_perm(<<"urn:xmpp:privilege:1">>,
+ IgnoreEls, _el);
{<<"thumbnail">>, <<"urn:xmpp:thumbs:1">>, _} ->
decode_thumbnail(<<"urn:xmpp:thumbs:1">>, IgnoreEls,
_el);
@@ -3272,6 +3314,31 @@ decode({xmlel, _name, _attrs, _} = _el, TopXMLNS,
is_known_tag({xmlel, _name, _attrs, _} = _el,
TopXMLNS) ->
case {_name, get_attr(<<"xmlns">>, _attrs), TopXMLNS} of
+ {<<"query">>, <<"urn:xmpp:delegation:1">>, _} -> true;
+ {<<"query">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"delegate">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"delegate">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"delegation">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"delegation">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"delegated">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"delegated">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"attribute">>, <<"urn:xmpp:delegation:1">>, _} ->
+ true;
+ {<<"attribute">>, <<>>, <<"urn:xmpp:delegation:1">>} ->
+ true;
+ {<<"privilege">>, <<"urn:xmpp:privilege:1">>, _} ->
+ true;
+ {<<"privilege">>, <<>>, <<"urn:xmpp:privilege:1">>} ->
+ true;
+ {<<"perm">>, <<"urn:xmpp:privilege:1">>, _} -> true;
+ {<<"perm">>, <<>>, <<"urn:xmpp:privilege:1">>} -> true;
{<<"thumbnail">>, <<"urn:xmpp:thumbs:1">>, _} -> true;
{<<"thumbnail">>, <<>>, <<"urn:xmpp:thumbs:1">>} ->
true;
@@ -5772,7 +5839,17 @@ encode({upload_request, _, _, _, _} = Request,
encode({upload_slot, _, _, _} = Slot, TopXMLNS) ->
encode_upload_slot(Slot, TopXMLNS);
encode({thumbnail, _, _, _, _} = Thumbnail, TopXMLNS) ->
- encode_thumbnail(Thumbnail, TopXMLNS).
+ encode_thumbnail(Thumbnail, TopXMLNS);
+encode({privilege_perm, _, _} = Perm, TopXMLNS) ->
+ encode_privilege_perm(Perm, TopXMLNS);
+encode({privilege, _, _} = Privilege, TopXMLNS) ->
+ encode_privilege(Privilege, TopXMLNS);
+encode({delegated, _, _} = Delegated, TopXMLNS) ->
+ encode_delegated(Delegated, TopXMLNS);
+encode({delegation, _, _} = Delegation, TopXMLNS) ->
+ encode_delegation(Delegation, TopXMLNS);
+encode({delegation_query, _, _} = Query, TopXMLNS) ->
+ encode_delegation_query(Query, TopXMLNS).
get_name({address, _, _, _, _, _}) -> <<"address">>;
get_name({addresses, _}) -> <<"addresses">>;
@@ -5812,6 +5889,9 @@ get_name({db_result, _, _, _, _, _}) -> <<"db:result">>;
get_name({db_verify, _, _, _, _, _, _}) ->
<<"db:verify">>;
get_name({delay, _, _, _}) -> <<"delay">>;
+get_name({delegated, _, _}) -> <<"delegated">>;
+get_name({delegation, _, _}) -> <<"delegation">>;
+get_name({delegation_query, _, _}) -> <<"query">>;
get_name({disco_info, _, _, _, _}) -> <<"query">>;
get_name({disco_item, _, _, _}) -> <<"item">>;
get_name({disco_items, _, _, _}) -> <<"query">>;
@@ -5876,6 +5956,8 @@ get_name({privacy_item, _, _, _, _, _, _, _, _}) ->
get_name({privacy_list, _, _}) -> <<"list">>;
get_name({privacy_query, _, _, _}) -> <<"query">>;
get_name({private, _}) -> <<"query">>;
+get_name({privilege, _, _}) -> <<"privilege">>;
+get_name({privilege_perm, _, _}) -> <<"perm">>;
get_name({ps_affiliation, _, _, _, _}) ->
<<"affiliation">>;
get_name({ps_error, 'closed-node', _}) ->
@@ -6075,6 +6157,12 @@ get_ns({db_result, _, _, _, _, _}) ->
get_ns({db_verify, _, _, _, _, _, _}) ->
<<"jabber:server">>;
get_ns({delay, _, _, _}) -> <<"urn:xmpp:delay">>;
+get_ns({delegated, _, _}) ->
+ <<"urn:xmpp:delegation:1">>;
+get_ns({delegation, _, _}) ->
+ <<"urn:xmpp:delegation:1">>;
+get_ns({delegation_query, _, _}) ->
+ <<"urn:xmpp:delegation:1">>;
get_ns({disco_info, _, _, _, _}) ->
<<"http://jabber.org/protocol/disco#info">>;
get_ns({disco_item, _, _, _}) ->
@@ -6160,6 +6248,9 @@ get_ns({privacy_list, _, _}) -> <<"jabber:iq:privacy">>;
get_ns({privacy_query, _, _, _}) ->
<<"jabber:iq:privacy">>;
get_ns({private, _}) -> <<"jabber:iq:private">>;
+get_ns({privilege, _, _}) -> <<"urn:xmpp:privilege:1">>;
+get_ns({privilege_perm, _, _}) ->
+ <<"urn:xmpp:privilege:1">>;
get_ns({ps_affiliation, Xmlns, _, _, _}) -> Xmlns;
get_ns({ps_error, 'closed-node', _}) ->
<<"http://jabber.org/protocol/pubsub#errors">>;
@@ -6600,6 +6691,11 @@ pp(upload_request, 4) ->
[filename, size, 'content-type', xmlns];
pp(upload_slot, 3) -> [get, put, xmlns];
pp(thumbnail, 4) -> [uri, 'media-type', width, height];
+pp(privilege_perm, 2) -> [access, type];
+pp(privilege, 2) -> [perms, forwarded];
+pp(delegated, 2) -> [ns, attrs];
+pp(delegation, 2) -> [delegated, forwarded];
+pp(delegation_query, 2) -> [to, delegate];
pp(_, _) -> no.
enc_ps_aff(member) -> <<"member">>;
@@ -6704,6 +6800,455 @@ dec_tzo(Val) ->
M = binary_to_integer(M1),
if H >= -12, H =< 12, M >= 0, M < 60 -> {H, M} end.
+decode_delegation_query(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"query">>, _attrs, _els}) ->
+ Delegate = decode_delegation_query_els(__TopXMLNS,
+ __IgnoreEls, _els, []),
+ To = decode_delegation_query_attrs(__TopXMLNS, _attrs,
+ undefined),
+ {delegation_query, To, Delegate}.
+
+decode_delegation_query_els(__TopXMLNS, __IgnoreEls, [],
+ Delegate) ->
+ lists:reverse(Delegate);
+decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"delegate">>, _attrs, _} = _el | _els],
+ Delegate) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els,
+ [decode_delegate(__TopXMLNS, __IgnoreEls,
+ _el)
+ | Delegate]);
+ <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els,
+ [decode_delegate(<<"urn:xmpp:delegation:1">>,
+ __IgnoreEls, _el)
+ | Delegate]);
+ _ ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els, Delegate)
+ end;
+decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Delegate) ->
+ decode_delegation_query_els(__TopXMLNS, __IgnoreEls,
+ _els, Delegate).
+
+decode_delegation_query_attrs(__TopXMLNS,
+ [{<<"to">>, _val} | _attrs], _To) ->
+ decode_delegation_query_attrs(__TopXMLNS, _attrs, _val);
+decode_delegation_query_attrs(__TopXMLNS, [_ | _attrs],
+ To) ->
+ decode_delegation_query_attrs(__TopXMLNS, _attrs, To);
+decode_delegation_query_attrs(__TopXMLNS, [], To) ->
+ decode_delegation_query_attr_to(__TopXMLNS, To).
+
+encode_delegation_query({delegation_query, To,
+ Delegate},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els =
+ lists:reverse('encode_delegation_query_$delegate'(Delegate,
+ __NewTopXMLNS, [])),
+ _attrs = encode_delegation_query_attr_to(To,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"query">>, _attrs, _els}.
+
+'encode_delegation_query_$delegate'([], __TopXMLNS,
+ _acc) ->
+ _acc;
+'encode_delegation_query_$delegate'([Delegate | _els],
+ __TopXMLNS, _acc) ->
+ 'encode_delegation_query_$delegate'(_els, __TopXMLNS,
+ [encode_delegate(Delegate, __TopXMLNS)
+ | _acc]).
+
+decode_delegation_query_attr_to(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"to">>, <<"query">>, __TopXMLNS}});
+decode_delegation_query_attr_to(__TopXMLNS, _val) ->
+ case catch dec_jid(_val) of
+ {'EXIT', _} ->
+ erlang:error({xmpp_codec,
+ {bad_attr_value, <<"to">>, <<"query">>, __TopXMLNS}});
+ _res -> _res
+ end.
+
+encode_delegation_query_attr_to(_val, _acc) ->
+ [{<<"to">>, enc_jid(_val)} | _acc].
+
+decode_delegate(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"delegate">>, _attrs, _els}) ->
+ Namespace = decode_delegate_attrs(__TopXMLNS, _attrs,
+ undefined),
+ Namespace.
+
+decode_delegate_attrs(__TopXMLNS,
+ [{<<"namespace">>, _val} | _attrs], _Namespace) ->
+ decode_delegate_attrs(__TopXMLNS, _attrs, _val);
+decode_delegate_attrs(__TopXMLNS, [_ | _attrs],
+ Namespace) ->
+ decode_delegate_attrs(__TopXMLNS, _attrs, Namespace);
+decode_delegate_attrs(__TopXMLNS, [], Namespace) ->
+ decode_delegate_attr_namespace(__TopXMLNS, Namespace).
+
+encode_delegate(Namespace, __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els = [],
+ _attrs = encode_delegate_attr_namespace(Namespace,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"delegate">>, _attrs, _els}.
+
+decode_delegate_attr_namespace(__TopXMLNS, undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"namespace">>, <<"delegate">>,
+ __TopXMLNS}});
+decode_delegate_attr_namespace(__TopXMLNS, _val) ->
+ _val.
+
+encode_delegate_attr_namespace(_val, _acc) ->
+ [{<<"namespace">>, _val} | _acc].
+
+decode_delegation(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"delegation">>, _attrs, _els}) ->
+ {Forwarded, Delegated} =
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ undefined, []),
+ {delegation, Delegated, Forwarded}.
+
+decode_delegation_els(__TopXMLNS, __IgnoreEls, [],
+ Forwarded, Delegated) ->
+ {Forwarded, lists:reverse(Delegated)};
+decode_delegation_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"delegated">>, _attrs, _} = _el | _els],
+ Forwarded, Delegated) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded,
+ [decode_delegated(__TopXMLNS, __IgnoreEls, _el)
+ | Delegated]);
+ <<"urn:xmpp:delegation:1">> ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded,
+ [decode_delegated(<<"urn:xmpp:delegation:1">>,
+ __IgnoreEls, _el)
+ | Delegated]);
+ _ ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded, Delegated)
+ end;
+decode_delegation_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"forwarded">>, _attrs, _} = _el | _els],
+ Forwarded, Delegated) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"urn:xmpp:forward:0">> ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ decode_forwarded(<<"urn:xmpp:forward:0">>,
+ __IgnoreEls, _el),
+ Delegated);
+ _ ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded, Delegated)
+ end;
+decode_delegation_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Forwarded, Delegated) ->
+ decode_delegation_els(__TopXMLNS, __IgnoreEls, _els,
+ Forwarded, Delegated).
+
+encode_delegation({delegation, Delegated, Forwarded},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els =
+ lists:reverse('encode_delegation_$forwarded'(Forwarded,
+ __NewTopXMLNS,
+ 'encode_delegation_$delegated'(Delegated,
+ __NewTopXMLNS,
+ []))),
+ _attrs = enc_xmlns_attrs(__NewTopXMLNS, __TopXMLNS),
+ {xmlel, <<"delegation">>, _attrs, _els}.
+
+'encode_delegation_$forwarded'(undefined, __TopXMLNS,
+ _acc) ->
+ _acc;
+'encode_delegation_$forwarded'(Forwarded, __TopXMLNS,
+ _acc) ->
+ [encode_forwarded(Forwarded, __TopXMLNS) | _acc].
+
+'encode_delegation_$delegated'([], __TopXMLNS, _acc) ->
+ _acc;
+'encode_delegation_$delegated'([Delegated | _els],
+ __TopXMLNS, _acc) ->
+ 'encode_delegation_$delegated'(_els, __TopXMLNS,
+ [encode_delegated(Delegated, __TopXMLNS)
+ | _acc]).
+
+decode_delegated(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"delegated">>, _attrs, _els}) ->
+ Attrs = decode_delegated_els(__TopXMLNS, __IgnoreEls,
+ _els, []),
+ Ns = decode_delegated_attrs(__TopXMLNS, _attrs,
+ undefined),
+ {delegated, Ns, Attrs}.
+
+decode_delegated_els(__TopXMLNS, __IgnoreEls, [],
+ Attrs) ->
+ lists:reverse(Attrs);
+decode_delegated_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"attribute">>, _attrs, _} = _el | _els],
+ Attrs) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:delegation:1">> ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_delegated_attribute(__TopXMLNS,
+ __IgnoreEls, _el)
+ | Attrs]);
+ <<"urn:xmpp:delegation:1">> ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_delegated_attribute(<<"urn:xmpp:delegation:1">>,
+ __IgnoreEls, _el)
+ | Attrs]);
+ _ ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ Attrs)
+ end;
+decode_delegated_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Attrs) ->
+ decode_delegated_els(__TopXMLNS, __IgnoreEls, _els,
+ Attrs).
+
+decode_delegated_attrs(__TopXMLNS,
+ [{<<"namespace">>, _val} | _attrs], _Ns) ->
+ decode_delegated_attrs(__TopXMLNS, _attrs, _val);
+decode_delegated_attrs(__TopXMLNS, [_ | _attrs], Ns) ->
+ decode_delegated_attrs(__TopXMLNS, _attrs, Ns);
+decode_delegated_attrs(__TopXMLNS, [], Ns) ->
+ decode_delegated_attr_namespace(__TopXMLNS, Ns).
+
+encode_delegated({delegated, Ns, Attrs}, __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els = lists:reverse('encode_delegated_$attrs'(Attrs,
+ __NewTopXMLNS, [])),
+ _attrs = encode_delegated_attr_namespace(Ns,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"delegated">>, _attrs, _els}.
+
+'encode_delegated_$attrs'([], __TopXMLNS, _acc) -> _acc;
+'encode_delegated_$attrs'([Attrs | _els], __TopXMLNS,
+ _acc) ->
+ 'encode_delegated_$attrs'(_els, __TopXMLNS,
+ [encode_delegated_attribute(Attrs, __TopXMLNS)
+ | _acc]).
+
+decode_delegated_attr_namespace(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"namespace">>, <<"delegated">>,
+ __TopXMLNS}});
+decode_delegated_attr_namespace(__TopXMLNS, _val) ->
+ _val.
+
+encode_delegated_attr_namespace(_val, _acc) ->
+ [{<<"namespace">>, _val} | _acc].
+
+decode_delegated_attribute(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"attribute">>, _attrs, _els}) ->
+ Name = decode_delegated_attribute_attrs(__TopXMLNS,
+ _attrs, undefined),
+ Name.
+
+decode_delegated_attribute_attrs(__TopXMLNS,
+ [{<<"name">>, _val} | _attrs], _Name) ->
+ decode_delegated_attribute_attrs(__TopXMLNS, _attrs,
+ _val);
+decode_delegated_attribute_attrs(__TopXMLNS,
+ [_ | _attrs], Name) ->
+ decode_delegated_attribute_attrs(__TopXMLNS, _attrs,
+ Name);
+decode_delegated_attribute_attrs(__TopXMLNS, [],
+ Name) ->
+ decode_delegated_attribute_attr_name(__TopXMLNS, Name).
+
+encode_delegated_attribute(Name, __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:delegation:1">>, [],
+ __TopXMLNS),
+ _els = [],
+ _attrs = encode_delegated_attribute_attr_name(Name,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS)),
+ {xmlel, <<"attribute">>, _attrs, _els}.
+
+decode_delegated_attribute_attr_name(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"name">>, <<"attribute">>,
+ __TopXMLNS}});
+decode_delegated_attribute_attr_name(__TopXMLNS,
+ _val) ->
+ _val.
+
+encode_delegated_attribute_attr_name(_val, _acc) ->
+ [{<<"name">>, _val} | _acc].
+
+decode_privilege(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"privilege">>, _attrs, _els}) ->
+ {Perms, Forwarded} = decode_privilege_els(__TopXMLNS,
+ __IgnoreEls, _els, [], undefined),
+ {privilege, Perms, Forwarded}.
+
+decode_privilege_els(__TopXMLNS, __IgnoreEls, [], Perms,
+ Forwarded) ->
+ {lists:reverse(Perms), Forwarded};
+decode_privilege_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"perm">>, _attrs, _} = _el | _els], Perms,
+ Forwarded) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"">> when __TopXMLNS == <<"urn:xmpp:privilege:1">> ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_privilege_perm(__TopXMLNS, __IgnoreEls,
+ _el)
+ | Perms],
+ Forwarded);
+ <<"urn:xmpp:privilege:1">> ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ [decode_privilege_perm(<<"urn:xmpp:privilege:1">>,
+ __IgnoreEls, _el)
+ | Perms],
+ Forwarded);
+ _ ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms, Forwarded)
+ end;
+decode_privilege_els(__TopXMLNS, __IgnoreEls,
+ [{xmlel, <<"forwarded">>, _attrs, _} = _el | _els],
+ Perms, Forwarded) ->
+ case get_attr(<<"xmlns">>, _attrs) of
+ <<"urn:xmpp:forward:0">> ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms,
+ decode_forwarded(<<"urn:xmpp:forward:0">>,
+ __IgnoreEls, _el));
+ _ ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms, Forwarded)
+ end;
+decode_privilege_els(__TopXMLNS, __IgnoreEls,
+ [_ | _els], Perms, Forwarded) ->
+ decode_privilege_els(__TopXMLNS, __IgnoreEls, _els,
+ Perms, Forwarded).
+
+encode_privilege({privilege, Perms, Forwarded},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:privilege:1">>, [],
+ __TopXMLNS),
+ _els = lists:reverse('encode_privilege_$perms'(Perms,
+ __NewTopXMLNS,
+ 'encode_privilege_$forwarded'(Forwarded,
+ __NewTopXMLNS,
+ []))),
+ _attrs = enc_xmlns_attrs(__NewTopXMLNS, __TopXMLNS),
+ {xmlel, <<"privilege">>, _attrs, _els}.
+
+'encode_privilege_$perms'([], __TopXMLNS, _acc) -> _acc;
+'encode_privilege_$perms'([Perms | _els], __TopXMLNS,
+ _acc) ->
+ 'encode_privilege_$perms'(_els, __TopXMLNS,
+ [encode_privilege_perm(Perms, __TopXMLNS)
+ | _acc]).
+
+'encode_privilege_$forwarded'(undefined, __TopXMLNS,
+ _acc) ->
+ _acc;
+'encode_privilege_$forwarded'(Forwarded, __TopXMLNS,
+ _acc) ->
+ [encode_forwarded(Forwarded, __TopXMLNS) | _acc].
+
+decode_privilege_perm(__TopXMLNS, __IgnoreEls,
+ {xmlel, <<"perm">>, _attrs, _els}) ->
+ {Access, Type} = decode_privilege_perm_attrs(__TopXMLNS,
+ _attrs, undefined, undefined),
+ {privilege_perm, Access, Type}.
+
+decode_privilege_perm_attrs(__TopXMLNS,
+ [{<<"access">>, _val} | _attrs], _Access, Type) ->
+ decode_privilege_perm_attrs(__TopXMLNS, _attrs, _val,
+ Type);
+decode_privilege_perm_attrs(__TopXMLNS,
+ [{<<"type">>, _val} | _attrs], Access, _Type) ->
+ decode_privilege_perm_attrs(__TopXMLNS, _attrs, Access,
+ _val);
+decode_privilege_perm_attrs(__TopXMLNS, [_ | _attrs],
+ Access, Type) ->
+ decode_privilege_perm_attrs(__TopXMLNS, _attrs, Access,
+ Type);
+decode_privilege_perm_attrs(__TopXMLNS, [], Access,
+ Type) ->
+ {decode_privilege_perm_attr_access(__TopXMLNS, Access),
+ decode_privilege_perm_attr_type(__TopXMLNS, Type)}.
+
+encode_privilege_perm({privilege_perm, Access, Type},
+ __TopXMLNS) ->
+ __NewTopXMLNS =
+ choose_top_xmlns(<<"urn:xmpp:privilege:1">>, [],
+ __TopXMLNS),
+ _els = [],
+ _attrs = encode_privilege_perm_attr_type(Type,
+ encode_privilege_perm_attr_access(Access,
+ enc_xmlns_attrs(__NewTopXMLNS,
+ __TopXMLNS))),
+ {xmlel, <<"perm">>, _attrs, _els}.
+
+decode_privilege_perm_attr_access(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"access">>, <<"perm">>, __TopXMLNS}});
+decode_privilege_perm_attr_access(__TopXMLNS, _val) ->
+ case catch dec_enum(_val, [roster, message, presence])
+ of
+ {'EXIT', _} ->
+ erlang:error({xmpp_codec,
+ {bad_attr_value, <<"access">>, <<"perm">>,
+ __TopXMLNS}});
+ _res -> _res
+ end.
+
+encode_privilege_perm_attr_access(_val, _acc) ->
+ [{<<"access">>, enc_enum(_val)} | _acc].
+
+decode_privilege_perm_attr_type(__TopXMLNS,
+ undefined) ->
+ erlang:error({xmpp_codec,
+ {missing_attr, <<"type">>, <<"perm">>, __TopXMLNS}});
+decode_privilege_perm_attr_type(__TopXMLNS, _val) ->
+ case catch dec_enum(_val,
+ [none, get, set, both, outgoing, roster,
+ managed_entity])
+ of
+ {'EXIT', _} ->
+ erlang:error({xmpp_codec,
+ {bad_attr_value, <<"type">>, <<"perm">>, __TopXMLNS}});
+ _res -> _res
+ end.
+
+encode_privilege_perm_attr_type(_val, _acc) ->
+ [{<<"type">>, enc_enum(_val)} | _acc].
+
decode_thumbnail(__TopXMLNS, __IgnoreEls,
{xmlel, <<"thumbnail">>, _attrs, _els}) ->
{Uri, Media_type, Width, Height} =
diff --git a/src/xmpp_util.erl b/src/xmpp_util.erl
index fb3bbc7ab..7b3e0e892 100644
--- a/src/xmpp_util.erl
+++ b/src/xmpp_util.erl
@@ -92,12 +92,20 @@ has_xdata_var(Var, #xdata{fields = Fields}) ->
-spec make_adhoc_response(adhoc_command(), adhoc_command()) -> adhoc_command().
make_adhoc_response(#adhoc_command{lang = Lang, node = Node, sid = SID},
Command) ->
- Command#adhoc_command{lang = Lang, node = Node, sid = SID}.
+ make_adhoc_response(
+ Command#adhoc_command{lang = Lang, node = Node, sid = SID}).
-spec make_adhoc_response(adhoc_command()) -> adhoc_command().
-make_adhoc_response(#adhoc_command{sid = <<"">>} = Command) ->
+make_adhoc_response(#adhoc_command{sid = <<"">>,
+ status = Status,
+ actions = Actions} = Command) ->
SID = encode_timestamp(p1_time_compat:timestamp()),
- Command#adhoc_command{sid = SID};
+ NewActions = if Actions == undefined, Status /= completed ->
+ #adhoc_actions{execute = complete, complete = true};
+ true ->
+ undefined
+ end,
+ Command#adhoc_command{sid = SID, actions = NewActions};
make_adhoc_response(Command) ->
Command.
diff --git a/test/acl_test.exs b/test/acl_test.exs
index 551c74ae0..4bd8e6989 100644
--- a/test/acl_test.exs
+++ b/test/acl_test.exs
@@ -26,6 +26,7 @@ defmodule ACLTest do
setup_all do
:ok = :mnesia.start
:ok = :jid.start
+ :stringprep.start
:ok = :ejabberd_config.start(["domain1", "domain2"], [])
:ok = :acl.start
end
diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs
index 1c999314c..31b8ab2e2 100644
--- a/test/ejabberd_admin_test.exs
+++ b/test/ejabberd_admin_test.exs
@@ -28,6 +28,7 @@ defmodule EjabberdAdminTest do
# For some myterious reason, :ejabberd_commands.init mays
# sometimes fails if module is not loaded before
{:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands)
+ {:ok, _} = :ejabberd_access_permissions.start_link()
:ejabberd_commands.init
:ejabberd_admin.start
:ok
diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs
index 487cf6a4b..419a989d6 100644
--- a/test/ejabberd_commands_mock_test.exs
+++ b/test/ejabberd_commands_mock_test.exs
@@ -18,9 +18,13 @@
#
# ----------------------------------------------------------------------
+## TODO Fix next test error: add admin user ACL
+
defmodule EjabberdCommandsMockTest do
use ExUnit.Case, async: false
+ require EjabberdOauthMock
+
@author "jsautret@process-one.net"
# mocked callback module
@@ -44,13 +48,18 @@ defmodule EjabberdCommandsMockTest do
_ -> :ok
end
:mnesia.start
+ :ok = :jid.start
+ :ok = :ejabberd_config.start(["domain1", "domain2"], [])
+ {:ok, _} = :ejabberd_access_permissions.start_link()
+ :ok = :acl.start
EjabberdOauthMock.init
- :ok
+ on_exit fn -> :meck.unload end
end
setup do
:meck.unload
:meck.new(@module, [:non_strict])
+ :mnesia.delete_table(:ejabberd_commands)
:ejabberd_commands.init
end
@@ -167,7 +176,7 @@ defmodule EjabberdCommandsMockTest do
# default version is latest one
assert :result3 == :ejabberd_commands.execute_command(command_name, [])
# no such command in APIv0
- assert :unknown_command ==
+ assert {:error, :unknown_command} ==
catch_throw :ejabberd_commands.execute_command(command_name, [], 0)
assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1)
assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2)
@@ -180,7 +189,7 @@ defmodule EjabberdCommandsMockTest do
test "API command with user policy" do
- mock_commands_config
+ mock_commands_config [:user, :admin]
# Register a command test(user, domain) -> {:versionN, user, domain}
# with policy=user and versions 1 & 3
@@ -313,9 +322,8 @@ defmodule EjabberdCommandsMockTest do
end
-
test "API command with admin policy" do
- mock_commands_config
+ mock_commands_config [:admin]
# Register a command test(user, domain) -> {user, domain}
# with policy=admin
@@ -393,13 +401,47 @@ defmodule EjabberdCommandsMockTest do
assert :meck.validate @module
end
+ test "Commands can perform extra check on access" do
+ mock_commands_config [:admin, :open]
+
+ command_name = :test
+ function = :test_command
+ command = ejabberd_commands(name: command_name,
+ args: [{:user, :binary}, {:host, :binary}],
+ access: [:basic_rule_1],
+ module: @module,
+ function: function,
+ policy: :open)
+ :meck.expect(@module, function,
+ fn(user, domain) when is_binary(user) and is_binary(domain) ->
+ {user, domain}
+ end)
+ assert :ok == :ejabberd_commands.register_commands [command]
+
+# :acl.add(:global, :basic_acl_1, {:user, @user, @host})
+# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}])
+
+ assert {@user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, false},
+ command_name,
+ [@user, @domain])
+ assert {@user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass, false},
+ command_name,
+ [@user, @domain])
+
+ end
##########################################################
# Utils
# Mock a config where only @admin user is allowed to call commands
# as admin
- def mock_commands_config do
+ def mock_commands_config(commands \\ []) do
EjabberdAuthMock.init
EjabberdAuthMock.create_user @user, @domain, @userpass
EjabberdAuthMock.create_user @admin, @domain, @adminpass
@@ -408,10 +450,12 @@ defmodule EjabberdCommandsMockTest do
:meck.expect(:ejabberd_config, :get_option,
fn(:commands_admin_access, _, _) -> :commands_admin_access
(:oauth_access, _, _) -> :all
+ (:commands, _, _) -> [{:add_commands, commands}]
(_, _, default) -> default
end)
:meck.expect(:ejabberd_config, :get_myhosts,
fn() -> [@domain] end)
+
:meck.new :acl
:meck.expect(:acl, :access_matches,
fn(:commands_admin_access, info, _scope) ->
diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs
index 31d108214..c8219d0cf 100644
--- a/test/ejabberd_commands_test.exs
+++ b/test/ejabberd_commands_test.exs
@@ -28,7 +28,12 @@ defmodule EjabberdCommandsTest do
setup_all do
:mnesia.start
+ :stringprep.start
+ :ok = :ejabberd_config.start(["localhost"], [])
+ {:ok, _} = :ejabberd_access_permissions.start_link()
+
:ejabberd_commands.init
+ :ok
end
test "Check that we can register a command" do
@@ -37,6 +42,14 @@ defmodule EjabberdCommandsTest do
assert Enum.member?(commands, {:test_user, [], "Test user"})
end
+ test "get_exposed_commands/0 returns registered commands" do
+ commands = [open_test_command]
+ :ok = :ejabberd_commands.register_commands(commands)
+ :ok = :ejabberd_commands.expose_commands(commands)
+ exposed_commands = :ejabberd_commands.get_exposed_commands
+ assert Enum.member?(exposed_commands, :test_open)
+ end
+
test "Check that admin commands are rejected with noauth credentials" do
:ok = :ejabberd_commands.register_commands([admin_test_command])
@@ -70,6 +83,16 @@ defmodule EjabberdCommandsTest do
]}}}})
end
+ defp open_test_command do
+ ejabberd_commands(name: :test_open, tags: [:test],
+ desc: "Test open",
+ policy: :open,
+ module: __MODULE__,
+ function: :test_open,
+ args: [],
+ result: {:res, :rescode})
+ end
+
defp admin_test_command do
ejabberd_commands(name: :test_admin, tags: [:roster],
desc: "Test admin",
diff --git a/test/ejabberd_cyrsasl_test.exs b/test/ejabberd_cyrsasl_test.exs
index 0dc64ee44..d9b949294 100644
--- a/test/ejabberd_cyrsasl_test.exs
+++ b/test/ejabberd_cyrsasl_test.exs
@@ -71,8 +71,8 @@ defmodule EjabberdCyrsaslTest do
response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <>
"nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <>
"charset=utf-8,algorithm=md5-sess"
- assert {:continue, calc_str, state3} = :cyrsasl.server_step(state1, response)
- assert {:ok, list} = :cyrsasl.server_step(state3, "")
+ assert {:continue, _calc_str, state3} = :cyrsasl.server_step(state1, response)
+ assert {:ok, _list} = :cyrsasl.server_step(state3, "")
end
defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do
@@ -94,7 +94,7 @@ defmodule EjabberdCyrsaslTest do
defp setup_anonymous_mocks() do
:meck.unload
mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled,
- fn (host) ->
+ fn (_host) ->
true
end)
mock(:ejabberd_auth, :is_user_exists,
@@ -119,7 +119,7 @@ defmodule EjabberdCyrsaslTest do
end
end
- defp check_password(user, authzid, pass) do
+ defp check_password(_user, authzid, pass) do
case get_password(authzid) do
{^pass, mod} ->
{true, mod}
@@ -128,7 +128,7 @@ defmodule EjabberdCyrsaslTest do
end
end
- defp check_password_digest(user, authzid, pass, digest, digest_gen) do
+ defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do
case get_password(authzid) do
{spass, mod} ->
v = digest_gen.(spass)
diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs
index 81cfdc038..965bff1e6 100644
--- a/test/ejabberd_oauth_mock.exs
+++ b/test/ejabberd_oauth_mock.exs
@@ -26,7 +26,10 @@ defmodule EjabberdOauthMock do
:mnesia.start
:mnesia.create_table(:oauth_token,
[ram_copies: [node],
- attributes: [:oauth_token, :us, :scope, :expire]])
+ attributes: [:oauth_token, :us, :scope, :expire]])
+ :application.start(:cache_tab)
+ :cache_tab.new(:oauth_token,
+ [{:max_size, 1000}, {:life_time, 3600}])
end
def get_token(user, domain, command, expiration \\ 3600) do
@@ -40,7 +43,7 @@ defmodule EjabberdOauthMock do
{:user, user, domain}},
{"scope", [to_string command]},
{"expiry_time", expire}],
- :undefined)
+ [])
token
end
diff --git a/test/elixir-config/attr_test.exs b/test/elixir-config/attr_test.exs
new file mode 100644
index 000000000..c5cab5bd8
--- /dev/null
+++ b/test/elixir-config/attr_test.exs
@@ -0,0 +1,87 @@
+defmodule Ejabberd.Config.AttrTest do
+ use ExUnit.Case, async: true
+
+ alias Ejabberd.Config.Attr
+
+ test "extract attrs from single line block" do
+ block = quote do
+ @active false
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+ assert {:active, false} in block_res
+ end
+
+ test "extract attrs from multi line block" do
+ block = quote do
+ @active false
+ @opts [http: true]
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+ assert {:active, false} in block_res
+ assert {:opts, [http: true]} in block_res
+ end
+
+ test "inserts correctly defaults attr when missing in block" do
+ block = quote do
+ @active false
+ @opts [http: true]
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+
+ assert {:active, false} in block_res
+ assert {:git, ""} in block_res
+ assert {:name, ""} in block_res
+ assert {:opts, [http: true]} in block_res
+ assert {:dependency, []} in block_res
+ end
+
+ test "inserts all defaults attr when passed an empty block" do
+ block = quote do
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+
+ assert {:active, true} in block_res
+ assert {:git, ""} in block_res
+ assert {:name, ""} in block_res
+ assert {:opts, []} in block_res
+ assert {:dependency, []} in block_res
+ end
+
+ test "validates attrs and returns errors, if any" do
+ block = quote do
+ @not_supported_attr true
+ @active "false"
+ @opts [http: true]
+ end
+
+ block_res =
+ block
+ |> Attr.extract_attrs_from_block_with_defaults
+ |> Attr.validate
+
+ assert {:ok, {:opts, [http: true]}} in block_res
+ assert {:ok, {:git, ""}} in block_res
+ assert {:error, {:not_supported_attr, true}, :attr_not_supported} in block_res
+ assert {:error, {:active, "false"}, :type_not_supported} in block_res
+ end
+
+ test "returns the correct type for an attribute" do
+ assert :boolean == Attr.get_type_for_attr(:active)
+ assert :string == Attr.get_type_for_attr(:git)
+ assert :string == Attr.get_type_for_attr(:name)
+ assert :list == Attr.get_type_for_attr(:opts)
+ assert :list == Attr.get_type_for_attr(:dependency)
+ end
+
+ test "returns the correct default for an attribute" do
+ assert true == Attr.get_default_for_attr(:active)
+ assert "" == Attr.get_default_for_attr(:git)
+ assert "" == Attr.get_default_for_attr(:name)
+ assert [] == Attr.get_default_for_attr(:opts)
+ assert [] == Attr.get_default_for_attr(:dependency)
+ end
+end
diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs
new file mode 100644
index 000000000..c359c49c3
--- /dev/null
+++ b/test/elixir-config/config_test.exs
@@ -0,0 +1,65 @@
+defmodule Ejabberd.ConfigTest do
+ use ExUnit.Case
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "extracts successfully the module name from config file" do
+ assert [Ejabberd.ConfigFile] == Store.get(:module_name)
+ end
+
+ test "extracts successfully general opts from config file" do
+ [general] = Store.get(:general)
+ shaper = [normal: 1000, fast: 50000, max_fsm_queue: 1000]
+ assert [loglevel: 4, language: "en", hosts: ["localhost"], shaper: shaper] == general
+ end
+
+ test "extracts successfully listeners from config file" do
+ [listen] = Store.get(:listeners)
+ assert :ejabberd_c2s == listen.module
+ assert [port: 5222, max_stanza_size: 65536, shaper: :c2s_shaper, access: :c2s] == listen.attrs[:opts]
+ end
+
+ test "extracts successfully modules from config file" do
+ [module] = Store.get(:modules)
+ assert :mod_adhoc == module.module
+ assert [] == module.attrs[:opts]
+ end
+
+ test "extracts successfully hooks from config file" do
+ [register_hook] = Store.get(:hooks)
+
+ assert :register_user == register_hook.hook
+ assert [host: "localhost"] == register_hook.opts
+ assert is_function(register_hook.fun)
+ end
+
+ # TODO: When enalbed, this test causes the evaluation of a different config file, so
+ # the other tests, that uses the store, are compromised because the data is different.
+ # So, until a good way is found, this test should remain disabed.
+ #
+ # test "init/2 with force:true re-initializes the config store with new data" do
+ # config_file_path = File.cwd! <> "/ejabberd_different_from_default.exs"
+ # Config.init(config_file_path, true)
+ #
+ # assert [Ejabberd.ConfigFile] == Store.get(:module_name)
+ # assert [[loglevel: 4, language: "en", hosts: ["localhost"]]] == Store.get(:general)
+ # assert [] == Store.get(:modules)
+ # assert [] == Store.get(:listeners)
+ #
+ # Store.stop
+ # end
+end
diff --git a/test/elixir-config/ejabberd_logger.exs b/test/elixir-config/ejabberd_logger.exs
new file mode 100644
index 000000000..d13f79aa6
--- /dev/null
+++ b/test/elixir-config/ejabberd_logger.exs
@@ -0,0 +1,49 @@
+defmodule Ejabberd.Config.EjabberdLoggerTest do
+ use ExUnit.Case
+
+ import ExUnit.CaptureIO
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+ alias Ejabberd.Config.Validation
+ alias Ejabberd.Config.EjabberdLogger
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd_for_validation.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "outputs correctly when attr is not supported" do
+ error_msg = "[ WARN ] Annotation @attr_not_supported is not supported.\n"
+
+ [_mod_irc, _mod_configure, mod_time] = Store.get(:modules)
+ fun = fn ->
+ mod_time
+ |> Validation.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ assert capture_io(fun) == error_msg
+ end
+
+ test "outputs correctly when dependency is not found" do
+ error_msg = "[ WARN ] Module :mod_adhoc was not found, but is required as a dependency.\n"
+
+ [_mod_irc, mod_configure, _mod_time] = Store.get(:modules)
+ fun = fn ->
+ mod_configure
+ |> Validation.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ assert capture_io(fun) == error_msg
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd.exs b/test/elixir-config/shared/ejabberd.exs
new file mode 100644
index 000000000..5d0243bb5
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd.exs
@@ -0,0 +1,31 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"],
+ shaper: shaper]
+ end
+
+ defp shaper do
+ [normal: 1000,
+ fast: 50000,
+ max_fsm_queue: 1000]
+ end
+
+ listen :ejabberd_c2s do
+ @opts [
+ port: 5222,
+ max_stanza_size: 65536,
+ shaper: :c2s_shaper,
+ access: :c2s]
+ end
+
+ module :mod_adhoc do
+ end
+
+ hook :register_user, [host: "localhost"], fn(user, server) ->
+ info("User registered: #{user} on #{server}")
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd_different_from_default.exs b/test/elixir-config/shared/ejabberd_different_from_default.exs
new file mode 100644
index 000000000..a39409683
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd_different_from_default.exs
@@ -0,0 +1,9 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"]]
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd_for_validation.exs b/test/elixir-config/shared/ejabberd_for_validation.exs
new file mode 100644
index 000000000..8c0196c7e
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd_for_validation.exs
@@ -0,0 +1,20 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"]]
+ end
+
+ module :mod_time do
+ @attr_not_supported true
+ end
+
+ module :mod_configure do
+ @dependency [:mod_adhoc]
+ end
+
+ module :mod_irc do
+ end
+end
diff --git a/test/elixir-config/validation_test.exs b/test/elixir-config/validation_test.exs
new file mode 100644
index 000000000..1df775966
--- /dev/null
+++ b/test/elixir-config/validation_test.exs
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.ValidationTest do
+ use ExUnit.Case
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+ alias Ejabberd.Config.Validation
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd_for_validation.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "validates correctly the modules" do
+ [mod_irc, mod_configure, mod_time] = Store.get(:modules)
+
+ [{:error, _mod, errors}] = Validation.validate(mod_configure)
+ assert %{dependency: [mod_adhoc: :not_found]} == errors
+
+ [{:error, _mod, errors}] = Validation.validate(mod_time)
+ assert %{attribute: [{{:attr_not_supported, true}, :attr_not_supported}]} == errors
+
+ [{:ok, _mod}] = Validation.validate(mod_irc)
+ end
+end
diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs
index 761b07b7c..fde66f03f 100644
--- a/test/mod_admin_extra_test.exs
+++ b/test/mod_admin_extra_test.exs
@@ -22,6 +22,9 @@ defmodule EjabberdModAdminExtraTest do
use ExUnit.Case, async: false
require EjabberdAuthMock
+ require EjabberdSmMock
+ require ModLastMock
+ require ModRosterMock
@author "jsautret@process-one.net"
@@ -42,6 +45,7 @@ defmodule EjabberdModAdminExtraTest do
rescue
_ -> :ok
end
+ {:ok, _} = :ejabberd_access_permissions.start_link()
:ejabberd_commands.init
:ok = :ejabberd_config.start([@domain], [])
:mod_admin_extra.start(@domain, [])
diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs
index 47b1fe94a..4809ecd59 100644
--- a/test/mod_http_api_mock_test.exs
+++ b/test/mod_http_api_mock_test.exs
@@ -46,6 +46,7 @@ defmodule ModHttpApiMockTest do
:mnesia.start
:stringprep.start
:ejabberd_config.start([@domain], [])
+ {:ok, _} = :ejabberd_access_permissions.start_link()
:ejabberd_commands.init
rescue
_ -> :ok
@@ -58,6 +59,7 @@ defmodule ModHttpApiMockTest do
setup do
:meck.unload
:meck.new :ejabberd_commands
+ :meck.new(:acl, [:passthrough]) # Need to fake acl to allow oauth
EjabberdAuthMock.init
:ok
end
@@ -70,9 +72,9 @@ defmodule ModHttpApiMockTest do
fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
{[], {:res, :rescode}}
end)
- :meck.expect(:ejabberd_commands, :get_command_policy,
- fn (@acommand) -> {:ok, :user} end)
- :meck.expect(:ejabberd_commands, :get_commands,
+ :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+ fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end)
+ :meck.expect(:ejabberd_commands, :get_exposed_commands,
fn () -> [@acommand] end)
:meck.expect(:ejabberd_commands, :execute_command,
fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) ->
@@ -123,9 +125,9 @@ defmodule ModHttpApiMockTest do
fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
{[], {:res, :rescode}}
end)
- :meck.expect(:ejabberd_commands, :get_command_policy,
- fn (@acommand) -> {:ok, :user} end)
- :meck.expect(:ejabberd_commands, :get_commands,
+ :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+ fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end)
+ :meck.expect(:ejabberd_commands, :get_exposed_commands,
fn () -> [@acommand] end)
:meck.expect(:ejabberd_commands, :execute_command,
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
@@ -134,7 +136,7 @@ defmodule ModHttpApiMockTest do
end)
- # Correct OAuth call
+ # Correct OAuth call using specific scope
token = EjabberdOauthMock.get_token @user, @domain, @command
req = request(method: :GET,
path: ["api", @command],
@@ -147,6 +149,19 @@ defmodule ModHttpApiMockTest do
assert 200 == elem(result, 0) # HTTP code
assert "0" == elem(result, 2) # command result
+ # Correct OAuth call using specific ejabberd:user scope
+ token = EjabberdOauthMock.get_token @user, @domain, "ejabberd:user"
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # OAuth
+ auth: {:oauth, token, []},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 200 == elem(result, 0) # HTTP code
+ assert "0" == elem(result, 2) # command result
+
# Wrong OAuth token
req = request(method: :GET,
path: ["api", @command],
@@ -184,8 +199,8 @@ defmodule ModHttpApiMockTest do
result = :mod_http_api.process([@command], req)
assert 401 == elem(result, 0) # HTTP code
- # Check that the command was executed only once
- assert 1 ==
+ # Check that the command was executed twice
+ assert 2 ==
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
assert :meck.validate :ejabberd_auth
@@ -193,5 +208,69 @@ defmodule ModHttpApiMockTest do
#assert :ok = :meck.history(:ejabberd_commands)
end
+ test "Request oauth token, resource owner password credentials" do
+ EjabberdAuthMock.create_user @user, @domain, @userpass
+ :application.set_env(:oauth2, :backend, :ejabberd_oauth)
+ :application.start(:oauth2)
+
+ # Mock a simple command() -> :ok
+ :meck.expect(:ejabberd_commands, :get_command_format,
+ fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
+ {[], {:res, :rescode}}
+ end)
+ :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+ fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end)
+ :meck.expect(:ejabberd_commands, :get_exposed_commands,
+ fn () -> [@acommand] end)
+ :meck.expect(:ejabberd_commands, :execute_command,
+ fn (:undefined, {@user, @domain, {:oauth, _token}, false},
+ @acommand, [], @version, _) ->
+ :ok
+ end)
+
+ #Mock acl to allow oauth authorizations
+ :meck.expect(:acl, :match_rule, fn(_Server, _Access, _Jid) -> :allow end)
+
+
+ # Correct password
+ req = request(method: :POST,
+ path: ["oauth", "token"],
+ q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"ttl", "4000"}, {"password", @userpass}],
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :ejabberd_oauth.process([], req)
+ assert 200 = elem(result, 0) #http code
+ {kv} = :jiffy.decode(elem(result,2))
+ assert {_, "bearer"} = List.keyfind(kv, "token_type", 0)
+ assert {_, @command} = List.keyfind(kv, "scope", 0)
+ assert {_, 4000} = List.keyfind(kv, "expires_in", 0)
+ {"access_token", _token} = List.keyfind(kv, "access_token", 0)
+
+ #missing grant_type
+ req = request(method: :POST,
+ path: ["oauth", "token"],
+ q: [{"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass}],
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :ejabberd_oauth.process([], req)
+ assert 400 = elem(result, 0) #http code
+ {kv} = :jiffy.decode(elem(result,2))
+ assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0)
+
+
+ # incorrect user/pass
+ req = request(method: :POST,
+ path: ["oauth", "token"],
+ q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass<>"aa"}],
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :ejabberd_oauth.process([], req)
+ assert 400 = elem(result, 0) #http code
+ {kv} = :jiffy.decode(elem(result,2))
+ assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0)
+
+ assert :meck.validate :ejabberd_auth
+ assert :meck.validate :ejabberd_commands
+ end
end
diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs
index 99b8d9b28..c68270f1f 100644
--- a/test/mod_http_api_test.exs
+++ b/test/mod_http_api_test.exs
@@ -31,43 +31,44 @@ defmodule ModHttpApiTest do
:ok = :mnesia.start
:stringprep.start
:ok = :ejabberd_config.start(["localhost"], [])
-
+ {:ok, _} = :ejabberd_access_permissions.start_link()
:ok = :ejabberd_commands.init
-
:ok = :ejabberd_commands.register_commands(cmds)
- on_exit fn -> unregister_commands(cmds) end
+ on_exit fn ->
+ :meck.unload
+ unregister_commands(cmds) end
end
test "We can expose several commands to API at a time" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]])
- commands = :ejabberd_commands.get_commands()
+ :ejabberd_commands.expose_commands([:open_cmd, :user_cmd])
+ commands = :ejabberd_commands.get_exposed_commands()
assert Enum.member?(commands, :open_cmd)
assert Enum.member?(commands, :user_cmd)
end
- test "We can call open commands without authentication" do
- setup_mocks()
- :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]])
- request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
- {200, _, _} = :mod_http_api.process(["open_cmd"], request)
- end
+# test "We can call open commands without authentication" do
+# setup_mocks()
+# :ejabberd_commands.expose_commands([:open_cmd])
+# request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
+# {200, _, _} = :mod_http_api.process(["open_cmd"], request)
+# end
# This related to the commands config file option
- test "Attempting to access a command that is not exposed as HTTP API returns 401" do
+ test "Attempting to access a command that is not exposed as HTTP API returns 403" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [])
+ :ejabberd_commands.expose_commands([])
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
- {401, _, _} = :mod_http_api.process(["open_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["open_cmd"], request)
end
test "Call to user, admin or restricted commands without authentication are rejected" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]])
+ :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted])
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
- {401, _, _} = :mod_http_api.process(["user_cmd"], request)
- {401, _, _} = :mod_http_api.process(["admin_cmd"], request)
- {401, _, _} = :mod_http_api.process(["restricted_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["user_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["admin_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["restricted_cmd"], request)
end
@tag pending: true
@@ -98,7 +99,7 @@ defmodule ModHttpApiTest do
defp setup_mocks() do
:meck.unload
mock(:gen_mod, :get_module_opt,
- fn (_server, :mod_http_api, admin_ip_access, _, _) ->
+ fn (_server, :mod_http_api, _admin_ip_access, _, _) ->
[{:allow, [{:ip, {{127,0,0,2}, 32}}]}]
end)
end
diff --git a/test/test_helper.exs b/test/test_helper.exs
new file mode 100644
index 000000000..454f2338a
--- /dev/null
+++ b/test/test_helper.exs
@@ -0,0 +1,7 @@
+Code.require_file "ejabberd_auth_mock.exs", __DIR__
+Code.require_file "ejabberd_oauth_mock.exs", __DIR__
+Code.require_file "ejabberd_sm_mock.exs", __DIR__
+Code.require_file "mod_last_mock.exs", __DIR__
+Code.require_file "mod_roster_mock.exs", __DIR__
+
+ExUnit.start