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 +%%% Purpose : Administrative functions and commands +%%% Created : 7 Sep 2016 by Paweł Chmielowski +%%% +%%% +%%% 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(<>) 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,17 +2502,47 @@ 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 - true -> - StateData; - false -> +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 -> + request_ack(StateData#state{mgmt_ack_timer = undefined}); + false -> + StateData#state{mgmt_ack_timer = undefined} + end. + -spec mgmt_queue_add(state(), xmpp_element()) -> state(). mgmt_queue_add(StateData, El) -> NewNum = case StateData#state.mgmt_stanzas_out of @@ -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_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) -> +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. + +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), <>); 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,92 +246,155 @@ 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(User, Server, Scope, Token) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), - case catch mnesia:dirty_read(oauth_token, Token) of - [#oauth_token{us = {LUser, LServer}, - scope = TokenScope, - expire = Expire}] -> +check_token(Token) -> + case lookup(Token) of + {ok, #oauth_token{us = US, + scope = TokenScope, + expire = Expire}} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - oauth2_priv_set:is_member( - Scope, oauth2_priv_set:new(TokenScope)) andalso - Expire > TS; - _ -> - false - end. - -check_token(Scope, Token) -> - case catch mnesia:dirty_read(oauth_token, Token) of - [#oauth_token{us = US, - scope = TokenScope, - 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} - end; - false -> false + if + Expire > TS -> + {ok, US, TokenScope}; + true -> + {false, expired} end; _ -> - false + {false, not_found} end. +check_token(User, Server, ScopeList, Token) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + case lookup(Token) of + {ok, #oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}} -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + 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, not_found} + end. + +check_token(ScopeList, 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 -> + 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; + true -> + {false, expired} + end; + _ -> + {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( @@ -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 +%%% Purpose : OAUTH2 mnesia backend +%%% Created : 20 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% 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 +%%% Purpose : OAUTH2 REST backend +%%% Created : 26 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% 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">>), + <>. + + +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 +%%% Purpose : OAUTH2 SQL backend +%%% Created : 27 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% 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 +%%% Purpose : Provide a common API for inets / lhttpc / ibrowse +%%% Created : 29 Jul 2010 by Emilio Bustos +%%% +%%% +%%% 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 = <>, - 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()]}}. - -filter_other({C2SState, _OutStanzas}, Host, Stanza) -> - ?DEBUG("Won't add stanza to CSI queue", []), - queue_take(Stanza, Host, C2SState). - --spec flush_queue({ejabberd_c2s:state(), [stanza()]}, binary()) +-spec filter_other({ejabberd_c2s:state(), [stanza()]}, binary(), jid(), stanza()) -> {ejabberd_c2s:state(), [stanza()]}. -flush_queue({C2SState, _OutStanzas}, Host) -> - ?DEBUG("Going to flush 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(), jid()) + -> {ejabberd_c2s:state(), [stanza()]}. + +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 +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 10 Nov 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-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() + 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, 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} - 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 +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 11 Nov 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-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. %%

The Erlang code for the plugin is located in a module called %% node_plugin. The 'node_' prefix is mandatory.

-%%

The modules are initialized in alphetical order and the list is checked -%% and sorted to ensure that each module is initialized only once.

%%

See {@link node_hometree:init/1} for an example implementation.

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" %%%

@@ -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 +%%% Purpose : PEP microblogging (XEP-0277) plugin with SQL backend +%%% Created : 6 Sep 2016 by Holger Weiss +%%% +%%% +%%% 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 +%%% Purpose : Generic REST client +%%% Created : 16 Oct 2014 by Christophe Romain +%%% +%%% +%%% 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">>), + <> + 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(<>). + +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