diff options
54 files changed, 3616 insertions, 316 deletions
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.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_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/mod_muc_room.hrl b/include/mod_muc_room.hrl index d985f3f3b..551da7285 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -77,11 +77,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(), @@ -101,6 +105,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 a150746e7..3dbc765b0 100644 --- a/include/ns.hrl +++ b/include/ns.hrl @@ -164,6 +164,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/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 ba5abe90e..09bf58405 100644 --- a/lib/mod_presence_demo.ex +++ b/lib/mod_presence_demo.ex @@ -1,16 +1,15 @@ 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 @@ -18,9 +17,4 @@ defmodule ModPresenceDemo do info('Receive presence for #{user}') :none end - - # gen_mod callbacks - def depends(_host, _opts), do: [] - def mod_opt_type(_), do: [] - end @@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do def project do [app: :ejabberd, - version: "16.06.0", + version: "16.08.0", description: description, elixir: "~> 1.2", elixirc_paths: ["lib"], @@ -29,7 +29,7 @@ defmodule Ejabberd.Mixfile do 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, + :esip, :jiffy, :p1_oauth2, :eredis, :p1_mysql, :p1_pgsql, :sqlite3]] end @@ -40,7 +40,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"}, @@ -51,7 +51,6 @@ 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"}, @@ -61,7 +60,8 @@ defmodule Ejabberd.Mixfile do {: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.19.0", only: :dev}, + {:relx, "~> 3.21", only: :dev}, + {:ex_doc, ">= 0.0.0", only: :dev}, {:meck, "~> 0.8.4", only: :test}, {:moka, github: "processone/moka", tag: "1.0.5c", only: :test}] end @@ -1,29 +1,29 @@ %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, "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], []}, + "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, - "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, + "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, 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.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [: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.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], []}, + "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]}]}, + "lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]}, "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, "moka": {:git, "https://github.com/processone/moka.git", "3eed3a6dd7dedb70a6cd18f86c7561a18626eb3b", [tag: "1.0.5c"]}, "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "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.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []}, - "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, - "pc": {:hex, :pc, "1.2.0", "5e07731d1f8bf997a8d0271510983e570f910b42cd59bf612e664ad6dc35742e", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, - "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, + "relx": {:hex, :relx, "3.21.0", "91e1ea9f09b4edfda8461901f4b5c5e0226e43ec161e147eeab29f7761df6eb5", [: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]}]}, "samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, "stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, diff --git a/rebar.config b/rebar.config index ab5562858..3c9610021 100644 --- a/rebar.config +++ b/rebar.config @@ -18,7 +18,6 @@ {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"}}}}, @@ -75,6 +74,7 @@ {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_var_true, hipe, native}, {src_dirs, [asn1, src, diff --git a/src/acl.erl b/src/acl.erl index 897996976..d3f9afe38 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -262,6 +262,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}}; @@ -686,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/ejabberd_app.erl b/src/ejabberd_app.erl index 6f0b97fa3..890ab6f90 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -56,6 +56,7 @@ start(normal, _Args) -> ejabberd_admin:start(), gen_mod:start(), ext_mod:start(), + setup_if_elixir_conf_used(), ejabberd_config:start(), set_settings_from_config(), acl:start(), @@ -75,6 +76,8 @@ start(normal, _Args) -> ejabberd_oauth:start(), gen_mod:start_modules(), ejabberd_listener:start_listeners(), + ejabberd_service:start(), + register_elixir_config_hooks(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), Sup; start(_, _) -> @@ -239,8 +242,25 @@ opt_type(modules) -> end; opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime]. -start_elixir_application() -> - case application:ensure_started(elixir) of - ok -> ok; - {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) +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_c2s.erl b/src/ejabberd_c2s.erl index 73cc57247..226c5e0da 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,6 +49,7 @@ 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, @@ -116,9 +118,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 = <<"">>}). @@ -217,6 +222,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). get_aux_field(Key, #state{aux_fields = Opts}) -> case lists:keysearch(Key, 1, Opts) of @@ -329,13 +337,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; @@ -359,6 +372,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}. @@ -1332,6 +1346,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), @@ -1775,6 +1798,11 @@ 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. @@ -1910,8 +1938,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. @@ -2499,6 +2527,12 @@ 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(JID)]), Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}], @@ -2779,7 +2813,8 @@ handle_r(StateData) -> handle_a(StateData, Attrs) -> case catch jlib:binary_to_integer(fxml:get_attr_s(<<"h">>, Attrs)) of H when is_integer(H), H >= 0 -> - check_h_attribute(StateData, H); + NewStateData = check_h_attribute(StateData, H), + maybe_renew_ack_request(NewStateData); _ -> ?DEBUG("Ignoring invalid ACK element from ~s", [jid:to_string(StateData#state.jid)]), @@ -2878,16 +2913,45 @@ update_num_stanzas_in(#state{mgmt_state = MgmtState} = StateData, El) update_num_stanzas_in(StateData, _El) -> StateData. -send_stanza_and_ack_req(StateData, Stanza) -> - AckReq = #xmlel{name = <<"r">>, - attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}], - children = []}, - case send_element(StateData, Stanza) == ok andalso - send_element(StateData, AckReq) == ok of +mgmt_send_stanza(StateData, Stanza) -> + case send_element(StateData, Stanza) of + ok -> + maybe_request_ack(StateData); + _ -> + StateData#state{mgmt_state = pending} + end. + +maybe_request_ack(#state{mgmt_ack_timer = undefined} = StateData) -> + request_ack(StateData); +maybe_request_ack(StateData) -> + StateData. + +request_ack(#state{mgmt_xmlns = Xmlns, + mgmt_ack_timeout = AckTimeout} = StateData) -> + AckReq = #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, Xmlns}]}, + case {send_element(StateData, AckReq), AckTimeout} of + {ok, undefined} -> + ok; + {ok, Timeout} -> + Timer = erlang:send_after(Timeout, self(), close), + StateData#state{mgmt_ack_timer = Timer, + mgmt_stanzas_req = StateData#state.mgmt_stanzas_out}; + _ -> + StateData#state{mgmt_state = pending} + end. + +maybe_renew_ack_request(#state{mgmt_ack_timer = undefined} = StateData) -> + StateData; +maybe_renew_ack_request(#state{mgmt_ack_timer = Timer, + mgmt_queue = Queue, + mgmt_stanzas_out = NumStanzasOut, + mgmt_stanzas_req = NumStanzasReq} = StateData) -> + erlang:cancel_timer(Timer), + case NumStanzasReq < NumStanzasOut andalso not queue:is_empty(Queue) of true -> - StateData; + request_ack(StateData#state{mgmt_ack_timer = undefined}); false -> - StateData#state{mgmt_state = pending} + StateData#state{mgmt_ack_timer = undefined} end. mgmt_queue_add(StateData, El) -> diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 87a918704..6ca6a40a8 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -33,10 +33,12 @@ 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]). + env_binary_to_list/2, opt_type/1, may_hide_data/1, + is_elixir_enabled/0]). -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 @@ -318,7 +331,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. @@ -1040,6 +1055,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_http.erl b/src/ejabberd_http.erl index a79f26305..31f80be78 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, diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl index ea8cd792f..628119e6f 100644 --- a/src/ejabberd_http_bind.erl +++ b/src/ejabberd_http_bind.erl @@ -338,8 +338,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 e66cf33a5..e76e8689a 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_oauth.erl b/src/ejabberd_oauth.erl index 36c679cb6..4541190ad 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -66,7 +66,7 @@ %% * 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). --define(EXPIRE, 31536000). +-define(EXPIRE, 4294967). start() -> DBMod = get_db_backend(), diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 9d72b17b4..9dd7c831e 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -36,7 +36,7 @@ -behaviour(?GEN_FSM). %% External exports --export([start/2, start_link/2, send_text/2, +-export([start/0, start/2, start_link/2, send_text/2, send_element/2, socket_type/0, transform_listen_option/2]). -export([init/1, wait_for_stream/2, @@ -44,19 +44,10 @@ handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, terminate/3, print_state/1, opt_type/1]). --include("ejabberd.hrl"). --include("logger.hrl"). +-include("ejabberd_service.hrl"). +-include("mod_privacy.hrl"). --include("jlib.hrl"). - --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()}). +-export([get_delegated_ns/1]). %-define(DBGFSM, true). @@ -99,6 +90,15 @@ %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- + +%% for xep-0355 +%% table contans records like {namespace, fitering attributes, pid(), +%% host, disco info for general case, bare jid disco info } + +start() -> + ets:new(delegated_namespaces, [named_table, public]), + ets:new(hooks_tmp, [named_table, public]). + start(SockData, Opts) -> supervisor:start_child(ejabberd_service_sup, [SockData, Opts]). @@ -109,6 +109,9 @@ start_link(SockData, Opts) -> socket_type() -> xml_stream. +get_delegated_ns(FsmRef) -> + (?GEN_FSM):sync_send_all_state_event(FsmRef, {get_delegated_ns}). + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- @@ -141,6 +144,21 @@ init([{SockMod, Socket}, Opts]) -> p1_sha:sha(crypto:rand_bytes(20))), dict:from_list([{global, Pass}]) end, + %% privilege access to entities data + PrivAccess = case lists:keysearch(privilege_access, 1, Opts) of + {value, {_, PrivAcc}} -> PrivAcc; + _ -> [] + end, + Delegations = case lists:keyfind(delegations, 1, Opts) of + {delegations, Del} -> + lists:foldl( + fun({Ns, FiltAttr}, D) when Ns /= ?NS_DELEGATION -> + Attr = proplists:get_value(filtering, FiltAttr, []), + D ++ [{Ns, Attr}]; + (_Deleg, D) -> D + end, [], Del); + false -> [] + end, Shaper = case lists:keysearch(shaper_rule, 1, Opts) of {value, {_, S}} -> S; _ -> none @@ -154,8 +172,9 @@ init([{SockMod, Socket}, Opts]) -> SockMod:change_shaper(Socket, Shaper), {ok, wait_for_stream, #state{socket = Socket, sockmod = SockMod, - streamid = new_id(), host_opts = HostOpts, - access = Access, check_from = CheckFrom}}. + streamid = new_id(), host_opts = HostOpts, access = Access, + check_from = CheckFrom, privilege_access = PrivAccess, + delegations = Delegations}}. %%---------------------------------------------------------------------- %% Func: StateName/2 @@ -227,8 +246,31 @@ wait_for_handshake({xmlstreamelement, El}, StateData) -> [H]), ejabberd_hooks:run(component_connected, [H]) - end, dict:fetch_keys(StateData#state.host_opts)), - {next_state, stream_established, StateData}; + end, dict:fetch_keys(StateData#state.host_opts)), + + mod_privilege:advertise_permissions(StateData), + DelegatedNs = mod_delegation:advertise_delegations(StateData), + + RosterAccess = proplists:get_value(roster, + StateData#state.privilege_access), + + case proplists:get_value(presence, + StateData#state.privilege_access) of + <<"managed_entity">> -> + mod_privilege:initial_presences(StateData), + Fun = mod_privilege:process_presence(self()), + add_hooks(user_send_packet, Fun); + <<"roster">> when (RosterAccess == <<"both">>) or + (RosterAccess == <<"get">>) -> + mod_privilege:initial_presences(StateData), + Fun = mod_privilege:process_presence(self()), + add_hooks(user_send_packet, Fun), + Fun2 = mod_privilege:process_roster_presence(self()), + add_hooks(s2s_receive_packet, Fun2); + _ -> ok + end, + {next_state, stream_established, + StateData#state{delegations = DelegatedNs}}; _ -> send_text(StateData, ?INVALID_HANDSHAKE_ERR), {stop, normal, StateData} @@ -276,11 +318,12 @@ stream_established({xmlstreamelement, El}, StateData) -> <<"">> -> error; _ -> jid:from_string(To) end, - if ((Name == <<"iq">>) or (Name == <<"message">>) or - (Name == <<"presence">>)) - and (ToJID /= error) - and (FromJID /= error) -> - ejabberd_router:route(FromJID, ToJID, NewEl); + if (Name == <<"iq">>) and (ToJID /= error) and (FromJID /= error) -> + mod_privilege:process_iq(StateData, FromJID, ToJID, NewEl); + (Name == <<"presence">>) and (ToJID /= error) and (FromJID /= error) -> + ejabberd_router:route(FromJID, ToJID, NewEl); + (Name == <<"message">>) and (ToJID /= error) and (FromJID /= error) -> + mod_privilege:process_message(StateData, FromJID, ToJID, NewEl); true -> Lang = fxml:get_tag_attr_s(<<"xml:lang">>, El), Txt = <<"Incorrect stanza name or from/to JID">>, @@ -330,8 +373,11 @@ handle_event(_Event, StateName, StateData) -> %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- -handle_sync_event(_Event, _From, StateName, - StateData) -> +handle_sync_event({get_delegated_ns}, _From, StateName, StateData) -> + Reply = {StateData#state.host, StateData#state.delegations}, + {reply, Reply, StateName, StateData}; + +handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> @@ -370,6 +416,36 @@ handle_info({route, From, To, Packet}, StateName, ejabberd_router:route_error(To, From, Err, Packet) end, {next_state, StateName, StateData}; + +handle_info({user_presence, Packet, From}, + stream_established, StateData) -> + To = jid:from_string(StateData#state.host), + PacketNew = jlib:replace_from_to(From, To, Packet), + send_element(StateData, PacketNew), + {next_state, stream_established, StateData}; + +handle_info({roster_presence, Packet, From}, + stream_established, StateData) -> + %% check that current presence stanza is equivalent to last + PresenceNew = jlib:remove_attr(<<"to">>, Packet), + Dict = StateData#state.last_pres, + LastPresence = + try dict:fetch(From, Dict) + catch _:_ -> + undefined + end, + case mod_privilege:compare_presences(LastPresence, PresenceNew) of + false -> + #xmlel{attrs = Attrs} = PresenceNew, + Presence = PresenceNew#xmlel{attrs = [{<<"to">>, StateData#state.host} | Attrs]}, + send_element(StateData, Presence), + DictNew = dict:store(From, PresenceNew, Dict), + StateDataNew = StateData#state{last_pres = DictNew}, + {next_state, stream_established, StateDataNew}; + _ -> + {next_state, stream_established, StateData} + end; + handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {next_state, StateName, StateData}. @@ -388,7 +464,26 @@ terminate(Reason, StateName, StateData) -> ejabberd_hooks:run(component_disconnected, [StateData#state.host, Reason]) end, - dict:fetch_keys(StateData#state.host_opts)); + dict:fetch_keys(StateData#state.host_opts)), + + lists:foreach(fun({Ns, _FilterAttr}) -> + ets:delete(delegated_namespaces, Ns), + remove_iq_handlers(Ns) + end, StateData#state.delegations), + + RosterAccess = proplists:get_value(roster, StateData#state.privilege_access), + case proplists:get_value(presence, StateData#state.privilege_access) of + <<"managed_entity">> -> + Fun = mod_privilege:process_presence(self()), + remove_hooks(user_send_packet, Fun); + <<"roster">> when (RosterAccess == <<"both">>) or + (RosterAccess == <<"get">>) -> + Fun = mod_privilege:process_presence(self()), + remove_hooks(user_send_packet, Fun), + Fun2 = mod_privilege:process_roster_presence(self()), + remove_hooks(s2s_receive_packet, Fun2); + _ -> ok + end; _ -> ok end, (StateData#state.sockmod):close(StateData#state.socket), @@ -448,3 +543,19 @@ fsm_limit_opts(Opts) -> opt_type(max_fsm_queue) -> fun (I) when is_integer(I), I > 0 -> I end; opt_type(_) -> [max_fsm_queue]. + +remove_iq_handlers(Ns) -> + lists:foreach(fun(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, Ns), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, Ns) + end, ?MYHOSTS). + +add_hooks(Hook, Fun) -> + lists:foreach(fun(Host) -> + ejabberd_hooks:add(Hook, Host,Fun, 100) + end, ?MYHOSTS). + +remove_hooks(Hook, Fun) -> + lists:foreach(fun(Host) -> + ejabberd_hooks:delete(Hook, Host, Fun, 100) + end, ?MYHOSTS). diff --git a/src/jlib.erl b/src/jlib.erl index 4bc9b0055..3384e670e 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -371,15 +371,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()) -> diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl new file mode 100644 index 000000000..f2d1a13b5 --- /dev/null +++ b/src/mod_delegation.erl @@ -0,0 +1,538 @@ +%%%-------------------------------------------------------------------------------------- +%%% File : mod_delegation.erl +%%% Author : Anna Mukharram <amuhar3@gmail.com> +%%% Purpose : This module is an implementation for XEP-0355: Namespace Delegation +%%%-------------------------------------------------------------------------------------- + +-module(mod_delegation). + +-author('amuhar3@gmail.com'). + +-behaviour(gen_mod). + +-protocol({xep, 0355, '0.3'}). + +-export([start/2, stop/1, depends/2, mod_opt_type/1]). + +-export([advertise_delegations/1, process_iq/3, + disco_local_features/5, disco_sm_features/5, + disco_local_identity/5, disco_sm_identity/5, disco_info/5, clean/0]). + +-include_lib("stdlib/include/ms_transform.hrl"). + +-include("ejabberd_service.hrl"). + +-define(CLEAN_INTERVAL, timer:minutes(10)). + +%%%-------------------------------------------------------------------------------------- +%%% API +%%%-------------------------------------------------------------------------------------- + +start(Host, _Opts) -> + mod_disco:register_feature(Host, ?NS_DELEGATION), + %% start timer for hooks_tmp table cleaning + timer:apply_after(?CLEAN_INTERVAL, ?MODULE, clean, []), + + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, + disco_local_features, 500), %% This hook should be the last + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, + disco_local_identity, 500), + ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, + disco_sm_identity, 500), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 500), + ejabberd_hooks:add(disco_info, Host, ?MODULE, + disco_info, 500). + + +stop(Host) -> + mod_disco:unregister_feature(Host, ?NS_DELEGATION), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, + disco_local_features, 500), + ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, + disco_local_identity, 500), + ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, + disco_sm_identity, 500), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, + disco_sm_features, 500), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, + disco_info, 500). + +depends(_Host, _Opts) -> []. + +mod_opt_type(_Opt) -> []. + +%%%-------------------------------------------------------------------------------------- +%%% 4.2 Functions to advertise service of delegated namespaces +%%%-------------------------------------------------------------------------------------- +attribute_tag(Attrs) -> + lists:map(fun(Attr) -> + #xmlel{name = <<"attribute">>, attrs = [{<<"name">> , Attr}]} + end, Attrs). + +delegations(From, To, Delegations) -> + {Elem0, DelegatedNs} = + lists:foldl(fun({Ns, FiltAttr}, {Acc, AccNs}) -> + case ets:insert_new(delegated_namespaces, + {Ns, FiltAttr, self(), To, {}, {}}) of + true -> + Attrs = + if + FiltAttr == [] -> + ?DEBUG("namespace ~s is delegated to ~s with" + " no filtering attributes ~n",[Ns, To]), + []; + true -> + ?DEBUG("namespace ~s is delegated to ~s with" + " ~p filtering attributes ~n",[Ns, To, FiltAttr]), + attribute_tag(FiltAttr) + end, + add_iq_handlers(Ns), + {[#xmlel{name = <<"delegated">>, + attrs = [{<<"namespace">>, Ns}], + children = Attrs}| Acc], [{Ns, FiltAttr}|AccNs]}; + false -> {Acc, AccNs} + end + end, {[], []}, Delegations), + case Elem0 of + [] -> {ignore, DelegatedNs}; + _ -> + Elem1 = #xmlel{name = <<"delegation">>, + attrs = [{<<"xmlns">>, ?NS_DELEGATION}], + children = Elem0}, + Id = randoms:get_string(), + {#xmlel{name = <<"message">>, + attrs = [{<<"id">>, Id}, {<<"from">>, From}, {<<"to">>, To}], + children = [Elem1]}, DelegatedNs} + end. + +add_iq_handlers(Ns) -> + lists:foreach(fun(Host) -> + IQDisc = + gen_mod:get_module_opt(Host, ?MODULE, iqdisc, + fun gen_iq_handler:check_type/1, one_queue), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + Ns, ?MODULE, + process_iq, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + Ns, ?MODULE, + process_iq, IQDisc) + end, ?MYHOSTS). + +advertise_delegations(#state{delegations = []}) -> []; +advertise_delegations(StateData) -> + {Delegated, DelegatedNs} = + delegations(?MYNAME, StateData#state.host, StateData#state.delegations), + if + Delegated /= ignore -> + ejabberd_service:send_element(StateData, Delegated), + % server asks available features for delegated namespaces + disco_info(StateData#state{delegations = DelegatedNs}); + true -> ok + end, + DelegatedNs. + +%%%-------------------------------------------------------------------------------------- +%%% Delegated namespaces hook +%%%-------------------------------------------------------------------------------------- + +check_filter_attr([], _Children) -> true; +check_filter_attr(_FilterAttr, []) -> false; +check_filter_attr(FilterAttr, [#xmlel{} = Stanza|_]) -> + Attrs = proplists:get_keys(Stanza#xmlel.attrs), + lists:all(fun(Attr) -> + lists:member(Attr, Attrs) + end, FilterAttr); +check_filter_attr(_FilterAttr, _Children) -> false. + +-spec get_client_server([attr()]) -> {jid(), jid()}. + +get_client_server(Attrs) -> + Client = fxml:get_attr_s(<<"from">>, Attrs), + ClientJID = jid:from_string(Client), + ServerJID = jid:from_string(ClientJID#jid.lserver), + {ClientJID, ServerJID}. + +decapsulate_result(#xmlel{children = []}) -> ok; +decapsulate_result(#xmlel{children = Children}) -> + decapsulate_result0(Children). + +decapsulate_result0([]) -> ok; +decapsulate_result0([#xmlel{name = <<"delegation">>, + attrs = [{<<"xmlns">>, ?NS_DELEGATION}]} = Packet]) -> + decapsulate_result1(Packet#xmlel.children); +decapsulate_result0(_Children) -> ok. + +decapsulate_result1([]) -> ok; +decapsulate_result1([#xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}]} = Packet]) -> + decapsulate_result2(Packet#xmlel.children); +decapsulate_result1(_Children) -> ok. + +decapsulate_result2([]) -> ok; +decapsulate_result2([#xmlel{name = <<"iq">>, attrs = Attrs} = Packet]) -> + Ns = fxml:get_attr_s(<<"xmlns">>, Attrs), + if + Ns /= <<"jabber:client">> -> + ok; + true -> Packet + end; +decapsulate_result2(_Children) -> ok. + +-spec check_iq(xmlel(), xmlel()) -> xmlel() | ignore. + +check_iq(#xmlel{attrs = Attrs} = Packet, + #xmlel{attrs = AttrsOrigin} = OriginPacket) -> + % Id attribute of OriginPacket Must be equil to Packet Id attribute + Id1 = fxml:get_attr_s(<<"id">>, Attrs), + Id2 = fxml:get_attr_s(<<"id">>, AttrsOrigin), + % From attribute of OriginPacket Must be equil to Packet To attribute + From = fxml:get_attr_s(<<"from">>, AttrsOrigin), + To = fxml:get_attr_s(<<"to">>, Attrs), + % Type attribute Must be error or result + Type = fxml:get_attr_s(<<"type">>, Attrs), + if + ((Type == <<"result">>) or (Type == <<"error">>)), + Id1 == Id2, To == From -> + NewPacket = jlib:remove_attr(<<"xmlns">>, Packet), + %% We can send the decapsulated stanza from Server to Client (To) + NewPacket; + true -> + %% service-unavailable + Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE), + Err + end; +check_iq(_Packet, _OriginPacket) -> ignore. + +-spec manage_service_result(atom(), atom(), binary(), xmlel()) -> ok. + +manage_service_result(HookRes, HookErr, Service, OriginPacket) -> + fun(Packet) -> + {ClientJID, ServerJID} = get_client_server(OriginPacket#xmlel.attrs), + Server = ClientJID#jid.lserver, + + ets:delete(hooks_tmp, {HookRes, Server}), + ets:delete(hooks_tmp, {HookErr, Server}), + % Check Packet "from" attribute + % It Must be equil to current service host + From = fxml:get_attr_s(<<"from">> , Packet#xmlel.attrs), + if + From == Service -> + % decapsulate iq result + ResultIQ = decapsulate_result(Packet), + ServResponse = check_iq(ResultIQ, OriginPacket), + if + ServResponse /= ignore -> + ejabberd_router:route(ServerJID, ClientJID, ServResponse); + true -> ok + end; + true -> + % service unavailable + Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(ServerJID, ClientJID, Err) + end + end. + +-spec manage_service_error(atom(), atom(), xmlel()) -> ok. + +manage_service_error(HookRes, HookErr, OriginPacket) -> + fun(_Packet) -> + {ClientJID, ServerJID} = get_client_server(OriginPacket#xmlel.attrs), + Server = ClientJID#jid.lserver, + ets:delete(hooks_tmp, {HookRes, Server}), + ets:delete(hooks_tmp, {HookErr, Server}), + Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(ServerJID, ClientJID, Err) + end. + + +-spec forward_iq(binary(), binary(), xmlel()) -> ok. + +forward_iq(Server, Service, Packet) -> + Elem0 = #xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}], children = [Packet]}, + Elem1 = #xmlel{name = <<"delegation">>, + attrs = [{<<"xmlns">>, ?NS_DELEGATION}], children = [Elem0]}, + Id = randoms:get_string(), + Elem2 = #xmlel{name = <<"iq">>, + attrs = [{<<"from">>, Server}, {<<"to">>, Service}, + {<<"type">>, <<"set">>}, {<<"id">>, Id}], + children = [Elem1]}, + + HookRes = {iq, result, Id}, + HookErr = {iq, error, Id}, + + FunRes = manage_service_result(HookRes, HookErr, Service, Packet), + FunErr = manage_service_error(HookRes, HookErr, Packet), + + Timestamp = p1_time_compat:system_time(seconds), + ets:insert(hooks_tmp, {{HookRes, Server}, FunRes, Timestamp}), + ets:insert(hooks_tmp, {{HookErr, Server}, FunErr, Timestamp}), + + From = jid:make(<<"">>, Server, <<"">>), + To = jid:make(<<"">>, Service, <<"">>), + ejabberd_router:route(From, To, Elem2). + +process_iq(From, #jid{lresource = <<"">>} = To, + #iq{type = Type, xmlns = XMLNS} = IQ) -> + %% check if stanza directed to server + %% or directed to the bare JID of the sender + case ((Type == get) or (Type == set)) of + true -> + Packet = jlib:iq_to_xml(IQ), + #xmlel{name = <<"iq">>, attrs = Attrs, children = Children} = Packet, + AttrsNew = [{<<"xmlns">>, <<"jabber:client">>} | Attrs], + AttrsNew2 = jlib:replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), AttrsNew), + case ets:lookup(delegated_namespaces, XMLNS) of + [{XMLNS, FiltAttr, _Pid, ServiceHost, _, _}] -> + case check_filter_attr(FiltAttr, Children) of + true -> + forward_iq(From#jid.server, ServiceHost, + Packet#xmlel{attrs = AttrsNew2}); + _ -> ok + end; + [] -> ok + end, + ignore; + _ -> + ignore + end; +process_iq(_From, _To, _IQ) -> ignore. + +%%%-------------------------------------------------------------------------------------- +%%% 7. Discovering Support +%%%-------------------------------------------------------------------------------------- + +decapsulate_features(#xmlel{attrs = Attrs} = Packet, Node) -> + case fxml:get_attr_s(<<"node">>, Attrs) of + Node -> + PREFIX = << ?NS_DELEGATION/binary, "::" >>, + Size = byte_size(PREFIX), + BARE_PREFIX = << ?NS_DELEGATION/binary, ":bare:" >>, + SizeBare = byte_size(BARE_PREFIX), + + Features = [Feat || #xmlel{attrs = [{<<"var">>, Feat}]} <- + fxml:get_subtags(Packet, <<"feature">>)], + + Identity = [I || I <- fxml:get_subtags(Packet, <<"identity">>)], + + Exten = [I || I <- fxml:get_subtags_with_xmlns(Packet, <<"x">>, ?NS_XDATA)], + + case Node of + << PREFIX:Size/binary, NS/binary >> -> + ets:update_element(delegated_namespaces, NS, + {5, {Features, Identity, Exten}}); + << BARE_PREFIX:SizeBare/binary, NS/binary >> -> + ets:update_element(delegated_namespaces, NS, + {6, {Features, Identity, Exten}}); + _ -> ok + end; + _ -> ok + end; +decapsulate_features(_Packet, _Node) -> ok. + +-spec disco_result(atom(), atom(), binary()) -> ok. + +disco_result(HookRes, HookErr, Node) -> + fun(Packet) -> + Tag = fxml:get_subtag_with_xmlns(Packet, <<"query">>, ?NS_DISCO_INFO), + decapsulate_features(Tag, Node), + + ets:delete(hooks_tmp, {HookRes, ?MYNAME}), + ets:delete(hooks_tmp, {HookErr, ?MYNAME}) + end. + +-spec disco_error(atom(), atom()) -> ok. + +disco_error(HookRes, HookErr) -> + fun(_Packet) -> + ets:delete(hooks_tmp, {HookRes, ?MYNAME}), + ets:delete(hooks_tmp, {HookErr, ?MYNAME}) + end. + +-spec disco_info(state()) -> ok. + +disco_info(StateData) -> + disco_info(StateData, <<"::">>), + disco_info(StateData, <<":bare:">>). + +-spec disco_info(state(), binary()) -> ok. + +disco_info(StateData, Sep) -> + lists:foreach(fun({Ns, _FilterAttr}) -> + Id = randoms:get_string(), + Node = << ?NS_DELEGATION/binary, Sep/binary, Ns/binary >>, + + HookRes = {iq, result, Id}, + HookErr = {iq, error, Id}, + + FunRes = disco_result(HookRes, HookErr, Node), + FunErr = disco_error(HookRes, HookErr), + + Timestamp = p1_time_compat:system_time(seconds), + ets:insert(hooks_tmp, {{HookRes, ?MYNAME}, FunRes, Timestamp}), + ets:insert(hooks_tmp, {{HookErr, ?MYNAME}, FunErr, Timestamp}), + + Tag = #xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}, + {<<"node">>, Node}], + children = []}, + DiscoReq = #xmlel{name = <<"iq">>, + attrs = [{<<"type">>, <<"get">>}, {<<"id">>, Id}, + {<<"from">>, ?MYNAME}, + {<<"to">>, StateData#state.host }], + children = [Tag]}, + ejabberd_service:send_element(StateData, DiscoReq) + + end, StateData#state.delegations). + + +disco_features(Acc, Bare) -> + Fun = fun(Feat) -> + ets:foldl(fun({Ns, _, _, _, _, _}, A) -> + A or str:prefix(Ns, Feat) + end, false, delegated_namespaces) + end, + % delete feature namespace which is delegated to service + Features = lists:filter(fun ({{Feature, _Host}}) -> + not Fun(Feature); + (Feature) when is_binary(Feature) -> + not Fun(Feature) + end, Acc), + % add service features + FeaturesList = + ets:foldl(fun({_, _, _, _, {Feats, _, _}, {FeatsBare, _, _}}, A) -> + if + Bare -> A ++ FeatsBare; + true -> A ++ Feats + end; + (_, A) -> A + end, Features, delegated_namespaces), + {result, FeaturesList}. + +disco_identity(Acc, Bare) -> + % filter delegated identites + Fun = fun(Ident) -> + ets:foldl(fun({_, _, _, _, {_ , I, _}, {_ , IBare, _}}, A) -> + Identity = + if + Bare -> IBare; + true -> I + end, + (fxml:get_attr_s(<<"category">> , Ident) == + fxml:get_attr_s(<<"category">>, Identity)) and + (fxml:get_attr_s(<<"type">> , Ident) == + fxml:get_attr_s(<<"type">>, Identity)) or A; + (_, A) -> A + end, false, delegated_namespaces) + end, + + Identities = + lists:filter(fun (#xmlel{attrs = Attrs}) -> + not Fun(Attrs) + end, Acc), + % add service features + ets:foldl(fun({_, _, _, _, {_, I, _}, {_, IBare, _}}, A) -> + if + Bare -> A ++ IBare; + true -> A ++ I + end; + (_, A) -> A + end, Identities, delegated_namespaces). + +%% xmlns from value element + +-spec get_field_value([xmlel()]) -> binary(). + +get_field_value([]) -> <<"">>; +get_field_value([Elem| Elems]) -> + case (fxml:get_attr_s(<<"var">>, Elem#xmlel.attrs) == <<"FORM_TYPE">>) and + (fxml:get_attr_s(<<"type">>, Elem#xmlel.attrs) == <<"hidden">>) of + true -> + Ns = fxml:get_subtag_cdata(Elem, <<"value">>), + if + Ns /= <<"">> -> Ns; + true -> get_field_value(Elems) + end; + _ -> get_field_value(Elems) + end. + +get_info(Acc, Bare) -> + Fun = fun(Feat) -> + ets:foldl(fun({Ns, _, _, _, _, _}, A) -> + (A or str:prefix(Ns, Feat)) + end, false, delegated_namespaces) + end, + Exten = lists:filter(fun(Xmlel) -> + Tags = fxml:get_subtags(Xmlel, <<"field">>), + case get_field_value(Tags) of + <<"">> -> true; + Value -> not Fun(Value) + end + end, Acc), + ets:foldl(fun({_, _, _, _, {_, _, Ext}, {_, _, ExtBare}}, A) -> + if + Bare -> A ++ ExtBare; + true -> A ++ Ext + end; + (_, A) -> A + end, Exten, delegated_namespaces). + +%% 7.2.1 General Case + +disco_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_local_features(Acc, _From, _To, <<>>, _Lang) -> + FeatsOld = case Acc of + {result, I} -> I; + _ -> [] + end, + disco_features(FeatsOld, false); +disco_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_local_identity(Acc, _From, _To, <<>>, _Lang) -> + disco_identity(Acc, false); +disco_local_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +%% 7.2.2 Rediction Of Bare JID Disco Info + +disco_sm_features({error, ?ERR_ITEM_NOT_FOUND}, _From, + #jid{lresource = <<"">>}, <<>>, _Lang) -> + disco_features([], true); +disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_sm_features(Acc, _From, #jid{lresource = <<"">>}, <<>>, _Lang) -> + FeatsOld = case Acc of + {result, I} -> I; + _ -> [] + end, + disco_features(FeatsOld, true); +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_sm_identity(Acc, _From, #jid{lresource = <<"">>}, <<>>, _Lang) -> + disco_identity(Acc, true); +disco_sm_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_info(Acc, #jid{}, #jid{lresource = <<"">>}, <<>>, _Lang) -> + get_info(Acc, true); +disco_info(Acc, _Host, _Mod, <<>>, _Lang) -> + get_info(Acc, false); +disco_info(Acc, _Host, _Mod, _Node, _Lang) -> + Acc. + +%% clean hooks_tmp table + +clean() -> + ?DEBUG("cleaning ~p ETS table~n", [hooks_tmp]), + Now = p1_time_compat:system_time(seconds), + catch ets:select_delete(hooks_tmp, + ets:fun2ms(fun({_, _, Timestamp}) -> + Now - 300 >= Timestamp + end)), + %% start timer for table cleaning + timer:apply_after(?CLEAN_INTERVAL, ?MODULE, clean, []). diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 73e6f7e4e..dbca82375 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">>}). @@ -259,8 +259,10 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> ?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]), json_error(400, 40, <<"Missing command name.">>). @@ -385,6 +387,20 @@ format_args(Args, ArgsFormat) -> L when is_list(L) -> exit({additional_unused_args, L}) end. +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) -> + [{format_arg(Element, ElementDefFormat)} + || Element <- Elements]; format_arg(Elements, {list, {_ElementDefName, ElementDefFormat}}) when is_list(Elements) -> @@ -492,6 +508,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]}}; diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 571f85926..4eb129a87 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -488,9 +488,8 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, _ -> case mnesia:dirty_read(muc_online_room, {Room, Host}) of [] -> - Type = fxml:get_attr_s(<<"type">>, Attrs), - case {Name, Type} of - {<<"presence">>, <<"">>} -> + case is_create_request(Packet) of + true -> case check_user_can_create_room(ServerHost, AccessCreate, From, Room) and check_create_roomid(ServerHost, Room) of @@ -508,7 +507,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), ejabberd_router:route(To, From, Err) end; - _ -> + false -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), ErrText = <<"Conference room does not exist">>, Err = jlib:make_error_reply(Packet, @@ -523,6 +522,22 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, end end. +-spec is_create_request(xmlel()) -> boolean(). +is_create_request(#xmlel{name = <<"presence">>} = Packet) -> + <<"">> == fxml:get_tag_attr_s(<<"type">>, Packet); +is_create_request(#xmlel{name = <<"iq">>} = Packet) -> + case jlib:iq_query_info(Packet) of + #iq{type = set, xmlns = ?NS_MUCSUB, + sub_el = #xmlel{name = <<"subscribe">>}} -> + true; + #iq{type = get, xmlns = ?NS_MUC_OWNER, sub_el = SubEl} -> + [] == fxml:remove_cdata(SubEl#xmlel.children); + _ -> + false + end; +is_create_request(_) -> + false. + check_user_can_create_room(ServerHost, AccessCreate, From, _RoomID) -> case acl:match_rule(ServerHost, AccessCreate, From) of @@ -705,10 +720,11 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> get_subscribed_rooms(ServerHost, Host, From) -> Rooms = get_rooms(ServerHost, Host), + BareFrom = jid:remove_resource(From), lists:flatmap( fun(#muc_room{name_host = {Name, _}, opts = Opts}) -> Subscribers = proplists:get_value(subscribers, Opts, []), - case lists:keymember(From, 1, Subscribers) of + case lists:keymember(BareFrom, 1, Subscribers) of true -> [jid:make(Name, Host, <<>>)]; false -> [] end; diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index e1d48cdab..e334dca2b 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,7 +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, + subscribe_room/4, unsubscribe_room/2, get_subscribers/2, web_page_host/3, mod_opt_type/1, get_commands_spec/0]). -include("ejabberd.hrl"). @@ -88,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.", @@ -163,6 +176,11 @@ get_commands_spec() -> 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, @@ -411,15 +429,23 @@ prepare_room_info(Room_info) -> %% ok | error %% @doc Create a room immediately with the default options. create_room(Name1, Host1, ServerHost) -> + create_room_with_opts(Name1, Host1, ServerHost, []). + +create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) -> Name = jid:nodeprep(Name1), 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), @@ -440,7 +466,7 @@ create_room(Name1, Host1, ServerHost) -> Name, HistorySize, RoomShaper, - DefRoomOpts), + RoomOpts), {atomic, ok} = register_room(Host, Name, Pid), ok; _ -> @@ -759,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; @@ -775,12 +809,7 @@ change_room_option(Name, Service, OptionString, ValueString) -> max_users -> jlib: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) -> @@ -800,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}; @@ -954,6 +984,15 @@ unsubscribe_room(User, Room) -> 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_room.erl b/src/mod_muc_room.erl index e5ed4cc68..ac2f5daf1 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -139,7 +139,8 @@ normal_state({route, From, <<"">>, StateData) -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), case is_user_online(From, StateData) orelse - is_user_allowed_message_nonparticipant(From, StateData) + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of true -> case fxml:get_attr_s(<<"type">>, Attrs) of @@ -252,20 +253,13 @@ normal_state({route, From, <<"">>, Err = jlib:make_error_reply(Packet, Error), ejabberd_router:route(StateData#state.jid, From, Err), {next_state, normal_state, StateData}; - IJID -> + IJIDs -> Config = StateData#state.config, case Config#config.members_only of true -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation(IJID, member, - StateData), - send_affiliation(IJID, member, - StateData), - store_room(NSD), - {next_state, normal_state, NSD}; - _ -> {next_state, normal_state, StateData} - end; + NSD = process_invitees(IJIDs, StateData), + store_room(NSD), + {next_state, normal_state, NSD}; false -> {next_state, normal_state, StateData} end end; @@ -527,7 +521,8 @@ normal_state({route, From, ToNick, continue_delivery -> case {(StateData#state.config)#config.allow_private_messages, - is_user_online(From, StateData)} + is_user_online(From, StateData) orelse + is_subscriber(From, StateData)} of {true, true} -> case Type of @@ -562,9 +557,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), @@ -678,7 +671,7 @@ handle_event({service_message, Msg}, _StateName, children = [{xmlcdata, Msg}]}]}, send_wrapped_multiple( StateData#state.jid, - StateData#state.users, + get_users_and_subscribers(StateData), MessagePkt, ?NS_MUCSUB_NODES_MESSAGES, StateData), @@ -749,6 +742,10 @@ handle_sync_event({change_state, NewStateData}, _From, handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> NSD = process_item_change(Item, StateData, UJID), {reply, {ok, NSD}, StateName, NSD}; +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) -> SubEl = #xmlel{name = <<"subscribe">>, @@ -928,7 +925,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), @@ -945,11 +942,12 @@ process_groupchat_message(From, #xmlel{name = <<"message">>, attrs = Attrs} = Packet, StateData) -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - 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), @@ -992,7 +990,7 @@ process_groupchat_message(From, 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 -> @@ -1060,9 +1058,16 @@ 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. process_presence(From, Nick, @@ -1070,7 +1075,6 @@ process_presence(From, Nick, StateData) -> Type0 = fxml:get_attr_s(<<"type">>, Attrs0), IsOnline = is_user_online(From, StateData), - IsSubscriber = is_subscriber(From, StateData), if Type0 == <<"">>; IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) -> case ejabberd_hooks:run_fold(muc_filter_presence, @@ -1107,7 +1111,7 @@ process_presence(From, Nick, Status_el -> fxml:get_tag_cdata(Status_el) end, - remove_online_user(From, NewState, IsSubscriber, Reason); + remove_online_user(From, NewState, Reason); <<"error">> -> ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " @@ -1164,27 +1168,15 @@ process_presence(From, Nick, From, Err), StateData; _ -> - case is_initial_presence(From, StateData) of - true -> - subscriber_becomes_available( - From, Nick, Packet, StateData); - false -> - change_nick(From, Nick, StateData) - end + change_nick(From, Nick, StateData) 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 + 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 end, @@ -1202,21 +1194,10 @@ maybe_strip_status_from_presence(From, Packet, StateData) -> _Allowed -> Packet end. -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. - close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent - andalso (?DICT):to_list(StateData1#state.users) == [] - 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", @@ -1226,18 +1207,39 @@ 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). + is_user_online(JID, StateData) -> LJID = jid:tolower(JID), (?DICT):is_key(LJID, StateData#state.users). 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. is_occupant_or_admin(JID, StateData) -> @@ -1414,7 +1416,6 @@ make_reason(Packet, From, StateData, Reason1) -> iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])). expulse_participant(Packet, From, StateData, Reason1) -> - IsSubscriber = is_subscriber(From, StateData), Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, #xmlel{name = <<"presence">>, @@ -1429,7 +1430,7 @@ expulse_participant(Packet, From, StateData, Reason1) -> Reason2}]}]}, StateData), send_new_presence(From, NewState, StateData), - remove_online_user(From, NewState, IsSubscriber). + remove_online_user(From, NewState). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). @@ -1709,8 +1710,7 @@ prepare_room_queue(StateData) -> {empty, _} -> StateData end. -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}} -> @@ -1729,9 +1729,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), @@ -1743,32 +1741,29 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, end, NewStateData. -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. + +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). -remove_online_user(JID, StateData, IsSubscriber) -> - remove_online_user(JID, StateData, IsSubscriber, <<"">>). +remove_online_user(JID, StateData) -> + remove_online_user(JID, StateData, <<"">>). -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) -> +remove_online_user(JID, StateData, Reason) -> LJID = jid:tolower(JID), {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), @@ -1841,10 +1836,13 @@ add_user_presence_un(JID, Presence, StateData) -> %% Find and return a list of the full JIDs of the users of Nick. %% Return jid record. find_jids_by_nick(Nick, StateData) -> - case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [User]} -> [jid:make(User)]; - {ok, Users} -> [jid:make(LJID) || LJID <- Users]; - error -> false + 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 -> false end. %% Find and return the full JID of the user of Nick with @@ -1911,7 +1909,14 @@ is_nick_change(JID, Nick, StateData) -> end. 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))). @@ -2023,8 +2028,7 @@ add_new_user(From, Nick, 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), Shift = count_stanza_shift(Nick, Els, NewState), @@ -2034,9 +2038,7 @@ add_new_user(From, Nick, end, 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 @@ -2276,15 +2278,6 @@ presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). -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. - send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). @@ -2378,7 +2371,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}) -> @@ -2434,7 +2427,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 = fxml:get_tag_attr_s(<<"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">>)) -> @@ -2658,19 +2651,20 @@ send_nick_changing(JID, OldNick, StateData, (_) -> ok end, - (?DICT):to_list(StateData#state.users)). + ?DICT:to_list(get_users_and_subscribers(StateData))). 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 -> @@ -2691,19 +2685,19 @@ send_affiliation(LJID, Affiliation, StateData) -> children = [#xmlel{name = <<"item">>, attrs = ItemAttrs}]}]}, + 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). status_els(IsInitialPresence, JID, #user{jid = JID}, StateData) -> Status = case IsInitialPresence of @@ -3410,7 +3404,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, 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, @@ -3419,7 +3413,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))). get_actor_nick(<<"">>, _StateData) -> <<"">>; @@ -4249,7 +4243,7 @@ send_config_change_info(New, #state{config = Old} = StateData) -> attrs = [{<<"xmlns">>, ?NS_MUC_USER}], children = StatusEls}]}, send_wrapped_multiple(StateData#state.jid, - StateData#state.users, + get_users_and_subscribers(StateData), Message, ?NS_MUCSUB_NODES_CONFIG, StateData). @@ -4265,7 +4259,7 @@ remove_nonmembers(StateData) -> _ -> SD end end, - StateData, (?DICT):to_list(StateData#state.users)). + StateData, (?DICT):to_list(get_users_and_subscribers(StateData))). set_opts([], StateData) -> StateData; set_opts([{Opt, Val} | Opts], StateData) -> @@ -4386,14 +4380,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}; @@ -4408,12 +4405,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(title), ?MAKE_CONFIG_OPT(description), ?MAKE_CONFIG_OPT(allow_change_subj), ?MAKE_CONFIG_OPT(allow_query_users), @@ -4469,7 +4465,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, @@ -4629,9 +4625,9 @@ process_iq_mucsub(From, Packet, Err = ?ERRT_BAD_REQUEST(Lang, <<"Missing 'nick' attribute">>), {error, Err}; Nick when Config#config.allow_subscription -> - 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, @@ -4644,14 +4640,12 @@ process_iq_mucsub(From, Packet, ErrText = <<"That nickname is registered by another person">>, {error, ?ERRT_CONFLICT(Lang, ErrText)}; _ -> - NewStateData = add_online_user( - From, Nick, Role, true, Nodes, StateData), + NewStateData = set_subscriber(From, Nick, Nodes, StateData), {result, subscription_nodes_to_events(Nodes), 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, subscription_nodes_to_events(Nodes), NewStateData}; error -> add_new_user(From, Nick, Packet, StateData) @@ -4664,53 +4658,44 @@ process_iq_mucsub(From, _Packet, #iq{type = set, sub_el = #xmlel{name = <<"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, [], NewStateData}; - error when From#jid.lresource == <<"">> -> - {LUser, LServer, _} = LJID, - NewStateData = - dict:fold( - fun({U, S, _}, #user{jid = J, is_subscriber = true} = User, - AccState) when U == LUser, S == LServer -> - remove_subscription(J, User, AccState); - (_, _, AccState) -> - AccState - end, StateData, StateData#state.users), - store_room(NewStateData), - {result, [], NewStateData}; - _ -> + error -> {result, [], StateData} end; -process_iq_mucsub(_From, _Packet, #iq{type = set, lang = Lang}, _StateData) -> +process_iq_mucsub(From, _Packet, + #iq{type = get, lang = Lang, + sub_el = #xmlel{name = <<"subscriptions">>}}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + Subs = dict:fold( + fun(_, #subscriber{jid = J}, Acc) -> + SJID = jid:to_string(J), + [#xmlel{name = <<"subscription">>, + attrs = [{<<"jid">>, SJID}]}|Acc] + end, [], StateData#state.subscribers), + {result, Subs, StateData}; + true -> + Txt = <<"Moderator privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, Txt)} + end; +process_iq_mucsub(_From, _Packet, #iq{lang = Lang}, _StateData) -> Txt = <<"Unrecognized subscription command">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; -process_iq_mucsub(_From, _Packet, #iq{type = get, lang = Lang}, _StateData) -> - Txt = <<"Value 'get' of 'type' attribute is not allowed">>, {error, ?ERRT_BAD_REQUEST(Lang, Txt)}. -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. - 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. @@ -4943,18 +4928,35 @@ is_invitation(Els) -> end, false, Els). +process_invitees(Invetees, StateDataIni) -> + lists:foldl( + fun(IJID, StateData) -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation(IJID, member, + StateData), + send_affiliation(IJID, member, + StateData), + NSD; + _ -> StateData + end + end, + StateDataIni, + Invetees). + check_invitation(From, Packet, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), CanInvite = (StateData#state.config)#config.allow_user_invites orelse FAffiliation == admin orelse FAffiliation == owner, - InviteEl = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of + + InviteEls = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of false -> Txt1 = <<"No 'x' element found">>, throw({error, ?ERRT_BAD_REQUEST(Lang, Txt1)}); XEl -> - case fxml:get_subtag(XEl, <<"invite">>) of + case fxml:get_subtags(XEl, <<"invite">>) of false -> Txt2 = <<"No 'invite' element found">>, throw({error, ?ERRT_BAD_REQUEST(Lang, Txt2)}); @@ -4962,20 +4964,17 @@ check_invitation(From, Packet, Lang, StateData) -> InviteEl1 end end, - JID = case - jid:from_string(fxml:get_tag_attr_s(<<"to">>, - InviteEl)) - of - error -> - Txt = <<"Incorrect value of 'to' attribute">>, - throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); - JID1 -> JID1 - end, case CanInvite of false -> Txt3 = <<"Invitations are not allowed in this conference">>, throw({error, ?ERRT_NOT_ALLOWED(Lang, Txt3)}); true -> + process_invitations(From, InviteEls, Lang, StateData) + end. + +process_invitations(From, InviteEls, Lang, StateData) -> + lists:map( + fun(InviteEl) -> Reason = fxml:get_path_s(InviteEl, [{elem, <<"reason">>}, cdata]), ContinueEl = case fxml:get_path_s(InviteEl, @@ -5044,9 +5043,19 @@ check_invitation(From, Packet, Lang, StateData) -> <<"">>})}], children = [{xmlcdata, Reason}]}, Body]}, + JID = case + jid:from_string(fxml:get_tag_attr_s(<<"to">>, + InviteEl)) + of + error -> + Txt = <<"Incorrect value of 'to' attribute">>, + throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); + JID1 -> JID1 + end, ejabberd_router:route(StateData#state.jid, JID, Msg), JID - end. + end, + InviteEls). %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. @@ -5171,18 +5180,26 @@ store_room(StateData) -> 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}} -> - case lists:member(Node, Nodes) of - true -> - NewPacket = wrap(From, To, Packet, Node), - ejabberd_router:route(State#state.jid, To, NewPacket); - false -> + 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, JID, Packet, Node), + ejabberd_router:route(State#state.jid, JID, NewPacket); + false -> + ok + end; + _ -> ok end; - _ -> + true -> ejabberd_router:route(From, To, Packet) end. @@ -5203,9 +5220,9 @@ wrap(From, To, Packet, Node) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast -send_multiple(From, Server, Users, Packet) -> - JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], - ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). +%% send_multiple(From, Server, Users, Packet) -> +%% JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], +%% ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). send_wrapped_multiple(From, Users, Packet, Node, State) -> lists:foreach( diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl new file mode 100644 index 000000000..af6dacec4 --- /dev/null +++ b/src/mod_privilege.erl @@ -0,0 +1,363 @@ +%%%-------------------------------------------------------------------------------------- +%%% File : mod_privilege.erl +%%% Author : Anna Mukharram <amuhar3@gmail.com> +%%% Purpose : This module is an implementation for XEP-0356: Privileged Entity +%%%-------------------------------------------------------------------------------------- + +-module(mod_privilege). + +-author('amuhar3@gmail.com'). + +-protocol({xep, 0356, '0.2.1'}). + +-export([advertise_permissions/1, initial_presences/1, process_presence/1, + process_roster_presence/1, compare_presences/2, + process_message/4, process_iq/4]). + +-include("ejabberd_service.hrl"). + +-include("mod_privacy.hrl"). + +%%%-------------------------------------------------------------------------------------- +%%% Functions to advertise services of allowed permission +%%%-------------------------------------------------------------------------------------- + +-spec permissions(binary(), binary(), list()) -> xmlel(). + +permissions(From, To, PrivAccess) -> + Perms = lists:map(fun({Access, Type}) -> + ?DEBUG("Advertise service ~s of allowed permission: ~s = ~s~n", + [To, Access, Type]), + #xmlel{name = <<"perm">>, + attrs = [{<<"access">>, + atom_to_binary(Access,latin1)}, + {<<"type">>, Type}]} + end, PrivAccess), + Stanza = #xmlel{name = <<"privilege">>, + attrs = [{<<"xmlns">> ,?NS_PRIVILEGE}], + children = Perms}, + Id = randoms:get_string(), + #xmlel{name = <<"message">>, + attrs = [{<<"id">>, Id}, {<<"from">>, From}, {<<"to">>, To}], + children = [Stanza]}. + +advertise_permissions(#state{privilege_access = []}) -> ok; +advertise_permissions(StateData) -> + Stanza = + permissions(?MYNAME, StateData#state.host, StateData#state.privilege_access), + ejabberd_service:send_element(StateData, Stanza). + +%%%-------------------------------------------------------------------------------------- +%%% Process presences +%%%-------------------------------------------------------------------------------------- + +initial_presences(StateData) -> + Pids = ejabberd_sm:get_all_pids(), + lists:foreach( + fun(Pid) -> + {User, Server, Resource, PresenceLast} = ejabberd_c2s:get_last_presence(Pid), + From = #jid{user = User, server = Server, resource = Resource}, + To = jid:from_string(StateData#state.host), + PacketNew = jlib:replace_from_to(From, To, PresenceLast), + ejabberd_service:send_element(StateData, PacketNew) + end, Pids). + +%% hook user_send_packet(Packet, C2SState, From, To) -> Packet +%% for Managed Entity Presence +process_presence(Pid) -> + fun(#xmlel{name = <<"presence">>} = Packet, _C2SState, From, _To) -> + case fxml:get_attr_s(<<"type">>, Packet#xmlel.attrs) of + T when (T == <<"">>) or (T == <<"unavailable">>) -> + Pid ! {user_presence, Packet, From}; + _ -> ok + end, + Packet; + (Packet, _C2SState, _From, _To) -> + Packet + end. +%% s2s_receive_packet(From, To, Packet) -> ok +%% for Roster Presence +%% From subscription "from" or "both" +process_roster_presence(Pid) -> + fun(From, To, #xmlel{name = <<"presence">>} = Packet) -> + case fxml:get_attr_s(<<"type">>, Packet#xmlel.attrs) of + T when (T == <<"">>) or (T == <<"unavailable">>) -> + Server = To#jid.server, + User = To#jid.user, + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + Server, #userlist{}, [User, Server]), + case privacy_check_packet(Server, User, PrivList, From, To, Packet, in) of + allow -> + Pid ! {roster_presence, Packet, From}; + _ -> ok + end, + ok; + _ -> ok + end; + (_From, _To, _Packet) -> ok + end. + +%%%-------------------------------------------------------------------------------------- +%%% Manage Roster +%%%-------------------------------------------------------------------------------------- + +process_iq(StateData, FromJID, ToJID, Packet) -> + IQ = jlib:iq_query_or_response_info(Packet), + case IQ of + #iq{xmlns = ?NS_ROSTER} -> + case (ToJID#jid.luser /= <<"">>) and + (FromJID#jid.luser == <<"">>) and + lists:member(ToJID#jid.lserver, ?MYHOSTS) of + true -> + AccessType = + proplists:get_value(roster, StateData#state.privilege_access, none), + case IQ#iq.type of + get when (AccessType == <<"both">>) or (AccessType == <<"get">>) -> + RosterIQ = roster_management(ToJID, FromJID, IQ), + ejabberd_service:send_element(StateData, RosterIQ); + set when (AccessType == <<"both">>) or (AccessType == <<"set">>) -> + %% check if user ToJID exist + #jid{lserver = Server, luser = User} = ToJID, + case ejabberd_auth:is_user_exists(User,Server) of + true -> + ResIQ = roster_management(ToJID, FromJID, IQ), + ejabberd_service:send_element(StateData, ResIQ); + _ -> ok + end; + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + #iq{type = Type, id = Id} when (Type == error) or (Type == result) -> % for XEP-0355 + Hook = {iq, Type, Id}, + Host = ToJID#jid.lserver, + case (ToJID#jid.luser == <<"">>) and + (FromJID#jid.luser == <<"">>) and + lists:member(ToJID#jid.lserver, ?MYHOSTS) of + true -> + case ets:lookup(hooks_tmp, {Hook, Host}) of + [{_, Function, _Timestamp}] -> + catch apply(Function, [Packet]); + [] -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end. + +roster_management(FromJID, ToJID, IQ) -> + ResIQ = mod_roster:process_iq(FromJID, FromJID, IQ), + ResXml = jlib:iq_to_xml(ResIQ), + jlib:replace_from_to(FromJID, ToJID, ResXml). + +%%%-------------------------------------------------------------------------------------- +%%% Message permission +%%%-------------------------------------------------------------------------------------- + +process_message(StateData, FromJID, ToJID, #xmlel{children = Children} = Packet) -> + %% if presence was send from service to server, + case lists:member(ToJID#jid.lserver, ?MYHOSTS) and + (ToJID#jid.luser == <<"">>) and + (FromJID#jid.luser == <<"">>) of %% service + true -> + %% if stanza contains privilege element + case Children of + [#xmlel{name = <<"privilege">>, + attrs = [{<<"xmlns">>, ?NS_PRIVILEGE}], + children = [#xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}], + children = Children2}]}] -> + %% 1 case : privilege service send subscription message + %% on behalf of the client + %% 2 case : privilege service send message on behalf + %% of the client + case Children2 of + %% it isn't case of 0356 extension + [#xmlel{name = <<"presence">>} = Child] -> + forward_subscribe(StateData, Child, Packet); + [#xmlel{name = <<"message">>} = Child] -> %% xep-0356 + forward_message(StateData, Child, Packet); + _ -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"invalid forwarded element">>, + Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), + ejabberd_service:send_element(StateData, Err) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end. + +forward_subscribe(StateData, Presence, Packet) -> + PrivAccess = StateData#state.privilege_access, + T = proplists:get_value(roster, PrivAccess, none), + Type = fxml:get_attr_s(<<"type">>, Presence#xmlel.attrs), + if + ((T == <<"both">>) or (T == <<"set">>)) and (Type == <<"subscribe">>) -> + From = fxml:get_attr_s(<<"from">>, Presence#xmlel.attrs), + FromJ = jid:from_string(From), + To = fxml:get_attr_s(<<"to">>, Presence#xmlel.attrs), + ToJ = case To of + <<"">> -> error; + _ -> jid:from_string(To) + end, + if + (ToJ /= error) and (FromJ /= error) -> + Server = FromJ#jid.lserver, + User = FromJ#jid.luser, + case (FromJ#jid.lresource == <<"">>) and + lists:member(Server, ?MYHOSTS) of + true -> + if + (Server /= ToJ#jid.lserver) or + (User /= ToJ#jid.luser) -> + %% 0356 server MUST NOT allow the privileged entity + %% to do anything that the managed entity could not do + try_roster_subscribe(Server,User, FromJ, ToJ, Presence); + true -> %% we don't want presence sent to self + ok + end; + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Incorrect stanza from/to JID">>, + Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end. + +forward_message(StateData, Message, Packet) -> + PrivAccess = StateData#state.privilege_access, + T = proplists:get_value(message, PrivAccess, none), + if + (T == <<"outgoing">>) -> + From = fxml:get_attr_s(<<"from">>, Message#xmlel.attrs), + FromJ = jid:from_string(From), + To = fxml:get_attr_s(<<"to">>, Message#xmlel.attrs), + ToJ = case To of + <<"">> -> FromJ; + _ -> jid:from_string(To) + end, + if + (ToJ /= error) and (FromJ /= error) -> + Server = FromJ#jid.server, + User = FromJ#jid.user, + case (FromJ#jid.lresource == <<"">>) and + lists:member(Server, ?MYHOSTS) of + true -> + %% there are no restriction on to attribute + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + Server, #userlist{}, + [User, Server]), + check_privacy_route(Server, User, PrivList, + FromJ, ToJ, Message); + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Incorrect stanza from/to JID">>, + Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Err = jlib:make_error_reply(Packet,?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end. + +%%%-------------------------------------------------------------------------------------- +%%% helper functions +%%%-------------------------------------------------------------------------------------- + +compare_presences(undefined, _Presence) -> false; +compare_presences(#xmlel{attrs = Attrs, children = Child}, + #xmlel{attrs = Attrs2, children = Child2}) -> + Id1 = fxml:get_attr_s(<<"id">>, Attrs), + Id2 = fxml:get_attr_s(<<"id">>, Attrs2), + if + (Id1 /= Id2) -> + false; + (Id1 /= <<"">>) and (Id1 == Id2) -> + true; + true -> + case not compare_attrs(Attrs, Attrs2) of + true -> false; + _ -> + compare_elements(Child, Child2) + end + end. + + +compare_elements([],[]) -> true; +compare_elements(Tags1, Tags2) when length(Tags1) == length(Tags2) -> + compare_tags(Tags1,Tags2); +compare_elements(_Tags1, _Tags2) -> false. + +compare_tags([],[]) -> true; +compare_tags([{xmlcdata, CData}|Tags1], [{xmlcdata, CData}|Tags2]) -> + compare_tags(Tags1, Tags2); +compare_tags([{xmlcdata, _CData1}|_Tags1], [{xmlcdata, _CData2}|_Tags2]) -> + false; +compare_tags([#xmlel{} = Stanza1|Tags1], [#xmlel{} = Stanza2|Tags2]) -> + case (Stanza1#xmlel.name == Stanza2#xmlel.name) and + compare_attrs(Stanza1#xmlel.attrs, Stanza2#xmlel.attrs) and + compare_tags(Stanza1#xmlel.children, Stanza2#xmlel.children) of + true -> + compare_tags(Tags1,Tags2); + false -> + false + end. + +%% attr() :: {Name, Value} +-spec compare_attrs([attr()], [attr()]) -> boolean(). +compare_attrs([],[]) -> true; +compare_attrs(Attrs1, Attrs2) when length(Attrs1) == length(Attrs2) -> + lists:foldl(fun(Attr,Acc) -> lists:member(Attr, Attrs2) and Acc end, true, Attrs1); +compare_attrs(_Attrs1, _Attrs2) -> false. + +%% Check if privacy rules allow this delivery +%% from ejabberd_c2s.erl +privacy_check_packet(Server, User, PrivList, From, To, Packet , Dir) -> + ejabberd_hooks:run_fold(privacy_check_packet, + Server, allow, [User, Server, PrivList, + {From, To, Packet}, Dir]). + +check_privacy_route(Server, User, PrivList, From, To, Packet) -> + case privacy_check_packet(Server, User, PrivList, From, To, Packet, out) of + allow -> + ejabberd_router:route(From, To, Packet); + _ -> ok %% who should receive error : service or user? + end. + +try_roster_subscribe(Server,User, From, To, Packet) -> + Access = + gen_mod:get_module_opt(Server, mod_roster, access, + fun(A) when is_atom(A) -> A end, all), + case acl:match_rule(Server, Access, From) of + deny -> + ok; + allow -> + ejabberd_hooks:run(roster_out_subscription, Server, + [User, Server, To, subscribe]), + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + Server, + #userlist{}, + [User, Server]), + check_privacy_route(Server, User, PrivList, From, To, Packet) + end. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 81afc9a06..d64d05737 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -363,8 +363,6 @@ depends(ServerHost, Opts) -> %% The default plugin module is implicit. %% <p>The Erlang code for the plugin is located in a module called %% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p> -%% <p>The modules are initialized in alphetical order and the list is checked -%% and sorted to ensure that each module is initialized only once.</p> %% <p>See {@link node_hometree:init/1} for an example implementation.</p> init_plugins(Host, ServerHost, Opts) -> TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts, diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index fa4af4d57..86375eaec 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -946,8 +946,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 6c7f09780..3399422e5 100644 --- a/src/node_mb.erl +++ b/src/node_mb.erl @@ -38,6 +38,7 @@ %%% plugins: %%% - "flat" %%% - "pep" # Requires mod_caps. +%%% - "mb" %%% pep_mapping: %%% "urn:xmpp:microblog:0": "mb" %%% </pre></p> @@ -154,7 +155,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..a3fdf3aed --- /dev/null +++ b/src/node_mb_sql.erl @@ -0,0 +1,151 @@ +%%%---------------------------------------------------------------------- +%%% File : node_mb_sql.erl +%%% Author : Holger Weiss <holger@zedat.fu-berlin.de> +%%% Purpose : PEP microblogging (XEP-0277) plugin with SQL backend +%%% Created : 6 Sep 2016 by Holger Weiss <holger@zedat.fu-berlin.de> +%%% +%%% +%%% ejabberd, Copyright (C) 2016 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(node_mb_sql). +-behaviour(gen_pubsub_node). +-author('holger@zedat.fu-berlin.de'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-export([init/3, terminate/2, options/0, features/0, + create_node_permission/6, create_node/2, delete_node/1, + purge_node/2, subscribe_node/8, unsubscribe_node/4, + publish_item/7, delete_item/4, remove_extra_items/3, + get_entity_affiliations/2, get_node_affiliations/1, + get_affiliation/2, set_affiliation/3, + get_entity_subscriptions/2, get_node_subscriptions/1, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1]). + +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_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_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/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 9d33d7573..785e74cd7 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -58,6 +58,7 @@ defmodule EjabberdCommandsMockTest do setup do :meck.unload :meck.new(@module, [:non_strict]) + :mnesia.delete_table(:ejabberd_commands) :ejabberd_commands.init end diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs index e6a34f65e..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 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 |