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/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 diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 3b333b3b5..9d127e748 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(), @@ -76,6 +77,7 @@ start(normal, _Args) -> 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(_, _) -> @@ -240,6 +242,18 @@ opt_type(modules) -> end; opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime]. +setup_if_elixir_conf_used() -> + case ejabberd_config:is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config.Store':start_link(); + false -> ok + end. + +register_elixir_config_hooks() -> + case ejabberd_config:is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config':start_hooks(); + false -> ok + end. + start_elixir_application() -> case application:ensure_started(elixir) of ok -> ok; diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index b75883fb2..7d5dfbc0c 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -33,6 +33,7 @@ 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, @@ -147,7 +148,13 @@ read_file(File) -> {include_modules_configs, true}]). read_file(File, Opts) -> - Terms1 = get_plain_terms_file(File, Opts), + Terms1 = 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, Terms_macros = case proplists:get_bool(replace_macros, Opts) of true -> replace_macros(Terms1); false -> Terms1 @@ -1042,6 +1049,10 @@ replace_modules(Modules) -> %% Elixir module naming %% ==================== +is_using_elixir_config() -> + Config = get_ejabberd_config_path(), + 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config). + %% 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/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